Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,25 @@ on:
permissions: {}

jobs:
verify-tag:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Check out repository without persisted credentials
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false

- name: Verify release tag and protected-main ancestry
run: |
PACKAGE_VERSION="$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')"
test "v${PACKAGE_VERSION}" = "${GITHUB_REF_NAME}"
git merge-base --is-ancestor "${GITHUB_SHA}" refs/remotes/origin/main

release:
needs: verify-tag
runs-on: ubuntu-latest
permissions:
contents: write
Expand All @@ -17,13 +35,14 @@ jobs:
artifact-metadata: write
steps:
- name: Check out repository
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
submodules: recursive

- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: pip
Expand Down Expand Up @@ -123,22 +142,22 @@ jobs:
)

- name: Attest build provenance
uses: actions/attest@v4
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4
with:
subject-path: |
dist/*.whl
dist/*.tar.gz

- name: Attest release SBOM
uses: actions/attest@v4
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4
with:
subject-path: |
dist/*.whl
dist/*.tar.gz
sbom-path: dist/bluedot-ares-${{ github.ref_name }}.cdx.json

- name: Upload Python distributions for optional PyPI publication
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: python-distributions-${{ github.ref_name }}
path: |
Expand All @@ -148,7 +167,7 @@ jobs:
retention-days: 30

- name: Upload complete release payload
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ares-release-${{ github.ref_name }}
path: dist/*
Expand Down Expand Up @@ -177,13 +196,13 @@ jobs:
id-token: write
steps:
- name: Download Python distributions
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: python-distributions-${{ github.ref_name }}
path: dist

- name: Publish with PyPI Trusted Publishing
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
with:
packages-dir: dist/
print-hash: true
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ All notable changes to Ares are documented here.
- Added CodeQL analysis for pull requests, pushes to `main`, a weekly schedule, and manual runs.
- Enabled GitHub private vulnerability reporting, aligning repository settings with `SECURITY.md`.
- Moved the GhostMCP submodule to its maintained organization repository and advanced it to the audited TLS 1.2 floor and dependency-lock revisions.
- Advanced the optional and vendored GhostMCP dependency to 0.2.1 so the public extra resolves to the maintained release identity.
- Bound release tags to commits reachable from protected `main` and pinned every release-workflow action to an immutable commit.
- Clarified the OpenAI authorization URL constant name and added a regression proving that interactive login output contains no authorization code, PKCE verifier, or OAuth token.
- Removed the nonfunctional repository-local scheduled Codex Security workflow; durable organization-level scanning remains an operational follow-up.
- Bumped the Python distribution, runtime, documentation, and release-gate identity to `1.1.1`.
Expand Down
2 changes: 1 addition & 1 deletion docs/mission-swarm.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ graph TD
C -->|Iterates & Validates| T[Runnable Tasks]
T -->|Executes Tool| D[Tool Dispatcher]
D -->|Approved call + mission ID| R[Registry Tools]
R -->|Manifest and engagement policy| G[GhostMCP v0.2.0]
R -->|Manifest and engagement policy| G[GhostMCP v0.2.1]
R -->|Returns findings| D
D -->|Indexes memory| DB
C -->|Gathers findings & evidence| RP[Report Renderer]
Expand Down
2 changes: 1 addition & 1 deletion docs/mission-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ These are registered wrappers that execute local command line utilities if they

## 4. GhostMCP contract

Ares pins GhostMCP `v0.2.0` and consumes its tool manifest schema `1.0`.
Ares pins GhostMCP `v0.2.1` and consumes its tool manifest schema `1.0`.
Manifest risk, capability, availability, target-field, and route metadata are
preserved through both in-process and external-stdio transports.

Expand Down
1 change: 1 addition & 0 deletions docs/releases/v1.1.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ The lifecycle fix does not rewrite historical state automatically. Back up `~/.a
- CodeQL now analyzes Python changes on pull requests, pushes to `main`, a weekly schedule, and manual runs.
- GitHub private vulnerability reporting is enabled, aligning the repository setting with `SECURITY.md`.
- The GhostMCP submodule now follows its maintained organization repository and includes an explicit TLS 1.2 minimum plus its regression test.
- The optional and vendored GhostMCP dependency now use 0.2.1, and the release workflow rejects tags whose commit is not reachable from protected `main`.
- Interactive OpenAI login still prints the browser authorization request for headless fallback, with regression coverage proving that no authorization code, PKCE verifier, or OAuth token is printed.
- The nonfunctional repository-local scheduled Codex Security workflow was retired. Durable organization-level Codex Security scanning remains a separate operational task.
- Optional PyPI publication remains restricted to `BlueDot-IT/Ares`, gated by `PYPI_PUBLISH_ENABLED`, protected by the `pypi` environment, and authenticated with GitHub OIDC Trusted Publishing.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Security = "https://github.com/BlueDot-IT/Ares/security/policy"
[project.optional-dependencies]
anthropic = ["anthropic"]
gemini = ["google-auth", "google-auth-oauthlib", "google-genai"]
ghostmcp = ["ghostmcp-server==0.2.0", "mcp>=1.28.1"]
ghostmcp = ["ghostmcp-server==0.2.1", "mcp>=1.28.1"]
dev = ["pytest", "build", "twine"]

[project.scripts]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
EXPECTED_VERSION = "1.1.1"


class ReleaseMetadataTests(unittest.TestCase):
class AresReleaseMetadataTests(unittest.TestCase):
def test_release_identity_is_consistent(self):
project = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8"))["project"]
readme = (ROOT / "README.md").read_text(encoding="utf-8")
Expand Down Expand Up @@ -43,6 +43,56 @@ def test_release_workflow_uses_notes_for_the_verified_tag(self):
self.assertIn('--notes-file "$RELEASE_NOTES"', workflow)
self.assertIsNone(re.search(r"--notes-file\s+docs/releases/v\d", workflow))

def test_release_workflow_binds_tag_to_protected_main(self):
workflow = (ROOT / ".github/workflows/release.yml").read_text(encoding="utf-8")
_, verification_and_release = workflow.split(
"\n verify-tag:\n",
maxsplit=1,
)
verification_job, release_and_publish = verification_and_release.split(
"\n release:\n",
maxsplit=1,
)
release_job, _ = release_and_publish.split(
"\n publish-pypi:\n",
maxsplit=1,
)

self.assertIn("permissions:\n contents: read", verification_job)
self.assertIn("fetch-depth: 0", verification_job)
self.assertIn("persist-credentials: false", verification_job)
self.assertIn(
'git merge-base --is-ancestor "${GITHUB_SHA}" '
"refs/remotes/origin/main",
verification_job,
)
self.assertIn("needs: verify-tag", release_job)
self.assertNotIn("git merge-base --is-ancestor", release_job)
self.assertIn("persist-credentials: false", release_job)

def test_release_workflow_pins_every_external_action(self):
workflow = (ROOT / ".github/workflows/release.yml").read_text(encoding="utf-8")
action_refs = re.findall(r"^\s*uses:\s*([^\s#]+)", workflow, re.MULTILINE)

self.assertTrue(action_refs)
for action_ref in action_refs:
self.assertRegex(action_ref, r"@(?:[0-9a-f]{40})$")

def test_ghostmcp_release_dependency_matches_vendored_source(self):
project = tomllib.loads(
(ROOT / "pyproject.toml").read_text(encoding="utf-8")
)["project"]
vendored_project = tomllib.loads(
(ROOT / "vendor/ghostmcp/pyproject.toml").read_text(
encoding="utf-8"
)
)["project"]

self.assertIn(
f"ghostmcp-server=={vendored_project['version']}",
project["optional-dependencies"]["ghostmcp"],
)

def test_trusted_publisher_remains_explicitly_gated(self):
workflow = (ROOT / ".github/workflows/release.yml").read_text(encoding="utf-8")
_, publish_job = workflow.split("\n publish-pypi:\n", maxsplit=1)
Expand All @@ -53,7 +103,11 @@ def test_trusted_publisher_remains_explicitly_gated(self):
)
self.assertIn("environment:\n name: pypi", publish_job)
self.assertIn("permissions:\n id-token: write", publish_job)
self.assertIn("uses: pypa/gh-action-pypi-publish@release/v1", publish_job)
self.assertIn(
"uses: pypa/gh-action-pypi-publish@"
"dc37677b2e1c63e2034f94d8a5b11f265b73ba33",
publish_job,
)
self.assertNotIn("PYPI_API_TOKEN", workflow)


Expand Down
4 changes: 2 additions & 2 deletions tests/test_ghostmcp_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def test_vendored_runner_exposes_versioned_security_manifest(self):
security = runner.tools["runtime_probe"]["security"]

self.assertEqual(security["manifest_schema"], "1.0")
self.assertEqual(security["server_version"], "0.2.0")
self.assertEqual(security["server_version"], "0.2.1")
self.assertEqual(security["risk"], "passive")
self.assertIn("discovery", security["capabilities"])
runner.close()
Expand All @@ -46,7 +46,7 @@ def test_external_bridge_preserves_security_manifest(self):
security = runner.tools["runtime_probe"]["security"]

self.assertEqual(security["manifest_schema"], "1.0")
self.assertEqual(security["server_version"], "0.2.0")
self.assertEqual(security["server_version"], "0.2.1")
runner.close()

def test_register_ghostmcp_tools_passes_scope_policy_to_default_runner(self):
Expand Down