agent-capmap 0.1.0__tar.gz → 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/CHANGELOG.md +13 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/CONTRIBUTING.md +34 -11
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/PKG-INFO +3 -1
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/README.md +2 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/action.yml +92 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/docs/examples/agentcapmap.yml +3 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/docs/github-action.md +26 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/examples/demo/inventory.json +1 -1
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/pyproject.toml +1 -1
- agent_capmap-0.1.1/scripts/prepare_release.py +158 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/__init__.py +1 -1
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/diff.py +41 -1
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_diff.py +26 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_fixtures.py +2 -2
- agent_capmap-0.1.1/tests/test_release_script.py +84 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.env.example +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.gitattributes +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.github/ISSUE_TEMPLATE/classification.yml +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.github/ISSUE_TEMPLATE/client_support.yml +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.github/dependabot.yml +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.github/pull_request_template.md +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.github/workflows/action-selftest.yml +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.github/workflows/ci.yml +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.github/workflows/codeql.yml +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.github/workflows/release.yml +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.gitignore +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.gitleaks.toml +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.gitleaksignore +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.pre-commit-config.yaml +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/AGENTS.md +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/CLAUDE.md +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/CODE_OF_CONDUCT.md +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/LICENSE +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/SECURITY.md +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/docs/blog-outline.md +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/docs/poc-checklist.md +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/docs/taxonomy.md +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/docs/threat-model.md +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/docs/vs-mcp-scan.md +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/README.md +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/agents.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/apply_labels.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/capture.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/catalog.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/aws-api.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/aws-iam.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/azure.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/brave-search.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/commands.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/context7.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/deepwiki.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/desktop-commander.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/exa.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/fetch.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/filesystem.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/firecrawl.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/git.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/github.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/gitlab.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/heroku.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/memory.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/notion.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/obsidian.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/playwright.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/postgres.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/qdrant.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/sentry.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/sequential-thinking.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/slack.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/supabase.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/tavily.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/time.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/run_eval.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/examples/README.md +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/examples/live-trifecta/mcp.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/examples/live-trifecta/sandbox/README.md +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/README.md +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/clean/expected.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/clean/mcp.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/clean/tools.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/exec-injection/expected.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/exec-injection/mcp.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/exec-injection/tools.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/no-approval-destructive/expected.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/no-approval-destructive/mcp.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/no-approval-destructive/tools.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/overscoped-token/expected.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/overscoped-token/mcp.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/overscoped-token/tools.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/poisoned-description/expected.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/poisoned-description/mcp.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/poisoned-description/tools.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/rug-pull/baseline.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/rug-pull/expected.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/rug-pull/mcp.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/rug-pull/tools.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/shadowing/expected.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/shadowing/mcp.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/shadowing/tools.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/trifecta/expected.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/trifecta/mcp.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/trifecta/tools.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/policies/default.yaml +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/requirements/build.in +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/requirements/build.lock +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/requirements/dev.lock +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/requirements/runtime.lock +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/schemas/policy.schema.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/schemas/tool-tags.schema.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/scripts/build_reference_labels.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/scripts/check.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/__main__.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/baseline.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/classify/__init__.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/classify/cache.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/classify/prompt.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/classify/provider.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/classify/reference.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/classify/reference_labels.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/cli.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/discovery/__init__.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/discovery/_common.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/discovery/claude_code.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/discovery/claude_desktop.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/discovery/cursor.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/discovery/mcp_json.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/discovery/vscode.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/enumerate/__init__.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/enumerate/hashing.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/enumerate/http.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/enumerate/netguard.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/enumerate/stdio.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/enumerate/transport.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/evaluation.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/findings.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/graph/__init__.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/graph/server_attrs.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/limits.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/models.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/policy/__init__.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/policy/engine.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/policy/loader.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/py.typed +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/redact.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/report/__init__.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/report/_text.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/report/markdown.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/report/sarif.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/taxonomy.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/__init__.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/conftest.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/fake_llm.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/fake_secrets.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/fixtures/configs/claude_code/.mcp.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/fixtures/configs/claude_desktop/claude_desktop_config.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/fixtures/configs/cursor/.cursor/mcp.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/fixtures/configs/generic/mcp.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/fixtures/configs/vscode/.vscode/mcp.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/fixtures/configs/vscode/.vscode/settings.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/fixtures/sarif/SOURCE.md +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/fixtures/sarif/sarif-schema-2.1.0.json +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/mock_mcp_server.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/scenarios.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_baseline.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_classify.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_claude_code.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_cli.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_discovery.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_enumerate_http.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_enumerate_stdio.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_evaluation.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_hardening.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_hashing.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_jsonc.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_netguard.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_policy.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_provider.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_redact.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_reference.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_report.py +0 -0
- {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_server_attrs.py +0 -0
|
@@ -6,6 +6,19 @@ All notable changes to this project are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.1] - 2026-11-08
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Pull request comment** (`comment: "true"` in the GitHub Action): scans the base commit
|
|
14
|
+
too and posts one updating comment with new findings, newly unassessed servers, gained
|
|
15
|
+
capabilities and resolved findings. Config-only (never starts servers), never combined
|
|
16
|
+
with `enumerate`, skipped gracefully on forks. `agentcapmap diff` now also reports
|
|
17
|
+
`new_not_assessed` and starts its Markdown with a summary line.
|
|
18
|
+
- `scripts/prepare_release.py` bumps the version everywhere and dates the changelog;
|
|
19
|
+
a test keeps `pyproject.toml` and `__version__` equal. The releasing steps in
|
|
20
|
+
CONTRIBUTING.md now cover patch releases and the PyPI/GitHub setup.
|
|
21
|
+
|
|
9
22
|
## [0.1.0] - 2026-11-08
|
|
10
23
|
|
|
11
24
|
First release, newest first within each section. Accuracy is not benchmarked yet and the
|
|
@@ -154,19 +154,42 @@ Adding, removing or redefining an axis or value is a breaking change:
|
|
|
154
154
|
|
|
155
155
|
## Releasing (maintainers)
|
|
156
156
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
157
|
+
One-time setup (already done for the 0.1.0 release):
|
|
158
|
+
|
|
159
|
+
- On PyPI, a trusted publisher for this repository: workflow `release.yml`,
|
|
160
|
+
environment `pypi`.
|
|
161
|
+
- On GitHub, a `pypi` environment with a **required reviewer**, so every release needs
|
|
162
|
+
an explicit approval before it is published.
|
|
163
|
+
|
|
164
|
+
For every release, including patch releases:
|
|
165
|
+
|
|
166
|
+
1. Put the changes under `## [Unreleased]` in `CHANGELOG.md`. If you reviewed or changed
|
|
167
|
+
eval labels, run `python scripts/build_reference_labels.py` and commit the result:
|
|
168
|
+
the labels ship in the package, so a label change needs a new release.
|
|
169
|
+
2. `python scripts/prepare_release.py X.Y.Z` bumps the version in `pyproject.toml` and
|
|
170
|
+
`src/agentcapmap/__init__.py` (a test keeps them equal) and dates the changelog. It
|
|
171
|
+
refuses on a dirty tree, a version that is not higher, an empty `[Unreleased]`, or
|
|
172
|
+
stale reference labels, and it never commits or pushes (add `--dry-run` to preview).
|
|
173
|
+
3. Regenerate the demo inventory, which stamps the version:
|
|
174
|
+
`AGENTCAPMAP_UPDATE_GOLDEN=1 python -m pytest tests/test_fixtures.py` (PowerShell:
|
|
175
|
+
`$env:AGENTCAPMAP_UPDATE_GOLDEN=1; python -m pytest tests/test_fixtures.py`). Then
|
|
176
|
+
`python scripts/check.py`, then commit, push, and wait for CI on that commit.
|
|
177
|
+
4. Tag that exact commit and push the tag:
|
|
178
|
+
`git tag -a vX.Y.Z -m "agent-capmap X.Y.Z" && git push origin vX.Y.Z`.
|
|
179
|
+
5. `.github/workflows/release.yml` checks that the tag matches the version, runs the
|
|
180
|
+
tests and pip-audit, builds the sdist and wheel from the hash-pinned build lock, and
|
|
181
|
+
waits for approval on the `pypi` environment. After approval it publishes to PyPI
|
|
182
|
+
with trusted publishing (no stored token; attestations are automatic) and creates
|
|
183
|
+
the GitHub release with the same files.
|
|
184
|
+
6. Check the result: the version is on PyPI, and `pip install agent-capmap==X.Y.Z` in a
|
|
185
|
+
fresh virtual environment runs `python -m agentcapmap --version`.
|
|
186
|
+
7. Update the `uses: NKwolv/agent-capmap@<sha>` pin in `docs/examples/agentcapmap.yml`
|
|
187
|
+
to the tagged commit.
|
|
167
188
|
|
|
168
189
|
A version on PyPI can never be re-uploaded. If a release is broken, yank it on PyPI
|
|
169
|
-
and publish a new patch version.
|
|
190
|
+
and publish a new patch version. If only the publish step failed (for example the
|
|
191
|
+
trusted publisher was not set up yet), fix the cause and re-run the failed job; the tag
|
|
192
|
+
does not need to change.
|
|
170
193
|
|
|
171
194
|
## Reporting security issues
|
|
172
195
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agent-capmap
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Inventory the MCP tools reachable by AI agents and flag dangerous capability combinations with policy-as-code.
|
|
5
5
|
Project-URL: Homepage, https://github.com/NKwolv/agent-capmap
|
|
6
6
|
Project-URL: Documentation, https://github.com/NKwolv/agent-capmap#readme
|
|
@@ -234,6 +234,8 @@ agentcapmap evaluate inventory.json --format sarif -o agentcapmap.sarif --fail-o
|
|
|
234
234
|
```
|
|
235
235
|
|
|
236
236
|
Exit codes: `0` passed, `1` findings at or above `--fail-on`, `2` usage or input error.
|
|
237
|
+
On pull requests the action can also post one comment that shows only what the PR
|
|
238
|
+
changed (`comment: "true"`; see the action docs).
|
|
237
239
|
The repository is also a GitHub Action: see [docs/github-action.md](https://github.com/NKwolv/agent-capmap/blob/main/docs/github-action.md)
|
|
238
240
|
and the [example workflow](https://github.com/NKwolv/agent-capmap/blob/main/docs/examples/agentcapmap.yml). By default the Action reads
|
|
239
241
|
configs only, which is safe for pull requests from forks.
|
|
@@ -191,6 +191,8 @@ agentcapmap evaluate inventory.json --format sarif -o agentcapmap.sarif --fail-o
|
|
|
191
191
|
```
|
|
192
192
|
|
|
193
193
|
Exit codes: `0` passed, `1` findings at or above `--fail-on`, `2` usage or input error.
|
|
194
|
+
On pull requests the action can also post one comment that shows only what the PR
|
|
195
|
+
changed (`comment: "true"`; see the action docs).
|
|
194
196
|
The repository is also a GitHub Action: see [docs/github-action.md](https://github.com/NKwolv/agent-capmap/blob/main/docs/github-action.md)
|
|
195
197
|
and the [example workflow](https://github.com/NKwolv/agent-capmap/blob/main/docs/examples/agentcapmap.yml). By default the Action reads
|
|
196
198
|
configs only, which is safe for pull requests from forks.
|
|
@@ -40,6 +40,17 @@ inputs:
|
|
|
40
40
|
cache-dir:
|
|
41
41
|
description: Classification cache directory (restore it with actions/cache).
|
|
42
42
|
default: ""
|
|
43
|
+
comment:
|
|
44
|
+
description: >-
|
|
45
|
+
On pull_request events, compare with the base branch and post what changed (new
|
|
46
|
+
findings, newly unassessed servers, gained capabilities) as one comment that is
|
|
47
|
+
updated on later pushes (true/false). Config-only: it cannot be combined with
|
|
48
|
+
enumerate. Needs `pull-requests: write` and a checkout with `fetch-depth: 0`.
|
|
49
|
+
Forks get a read-only token, so there the diff goes to the job summary only.
|
|
50
|
+
default: "false"
|
|
51
|
+
github-token:
|
|
52
|
+
description: Token used to post the comment (only needs pull-requests write).
|
|
53
|
+
default: ${{ github.token }}
|
|
43
54
|
sarif-file:
|
|
44
55
|
description: Where to write the SARIF report, relative to the workspace.
|
|
45
56
|
default: agentcapmap.sarif
|
|
@@ -135,3 +146,84 @@ runs:
|
|
|
135
146
|
gate=(report "$evaluation" --fail-on "$INPUT_FAIL_ON" -o "${RUNNER_TEMP}/agentcapmap-gate.json")
|
|
136
147
|
[[ "$INPUT_FAIL_ON_NOT_ASSESSED" == "true" ]] && gate+=(--fail-on-not-assessed)
|
|
137
148
|
"$PY" -m agentcapmap "${gate[@]}"
|
|
149
|
+
|
|
150
|
+
- name: Compare with the base branch and comment
|
|
151
|
+
if: ${{ always() && inputs.comment == 'true' && github.event_name == 'pull_request' }}
|
|
152
|
+
shell: bash
|
|
153
|
+
env:
|
|
154
|
+
PY: ${{ steps.install.outputs.python }}
|
|
155
|
+
GH_TOKEN: ${{ inputs.github-token }}
|
|
156
|
+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
157
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
158
|
+
REPO: ${{ github.repository }}
|
|
159
|
+
WORKSPACE: ${{ github.workspace }}
|
|
160
|
+
INPUT_PATH: ${{ inputs.path }}
|
|
161
|
+
INPUT_POLICY: ${{ inputs.policy }}
|
|
162
|
+
INPUT_BASELINE: ${{ inputs.baseline }}
|
|
163
|
+
INPUT_ENUMERATE: ${{ inputs.enumerate }}
|
|
164
|
+
run: |
|
|
165
|
+
set -uo pipefail
|
|
166
|
+
fail() { echo "::error title=agent-capmap::$1"; exit 2; }
|
|
167
|
+
soft() { echo "::warning title=agent-capmap::$1"; exit 0; }
|
|
168
|
+
[[ "$INPUT_ENUMERATE" == "false" ]] || fail "comment is for config-only pull request checks: enumerate runs the commands in the configs"
|
|
169
|
+
[[ "$BASE_SHA" =~ ^[0-9a-f]{40}$ ]] || fail "unexpected base commit"
|
|
170
|
+
[[ "$PR_NUMBER" =~ ^[0-9]+$ ]] || fail "unexpected pull request number"
|
|
171
|
+
[[ "$REPO" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] || fail "unexpected repository name"
|
|
172
|
+
|
|
173
|
+
head_eval="${RUNNER_TEMP}/agentcapmap-evaluation.json"
|
|
174
|
+
base_dir="${RUNNER_TEMP}/agentcapmap-base"
|
|
175
|
+
empty_dir="${RUNNER_TEMP}/agentcapmap-empty"
|
|
176
|
+
base_eval="${RUNNER_TEMP}/agentcapmap-base-evaluation.json"
|
|
177
|
+
diff_json="${RUNNER_TEMP}/agentcapmap-diff.json"
|
|
178
|
+
diff_md="${RUNNER_TEMP}/agentcapmap-diff.md"
|
|
179
|
+
body="${RUNNER_TEMP}/agentcapmap-comment.md"
|
|
180
|
+
abs() { if [[ "$1" == /* ]]; then printf '%s' "$1"; else printf '%s/%s' "$WORKSPACE" "$1"; fi; }
|
|
181
|
+
|
|
182
|
+
[[ -f "$head_eval" ]] || soft "no evaluation to compare (the scan step did not finish)"
|
|
183
|
+
git -C "$WORKSPACE" cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null || soft "base commit not available: use actions/checkout with fetch-depth: 0 to enable the comparison"
|
|
184
|
+
git -C "$WORKSPACE" worktree add --detach "$base_dir" "$BASE_SHA" >/dev/null 2>&1 || soft "could not check out the base commit"
|
|
185
|
+
trap 'git -C "$WORKSPACE" worktree remove --force "$base_dir" >/dev/null 2>&1 || true' EXIT
|
|
186
|
+
|
|
187
|
+
# The head's policy and baseline are used for both sides, so only the pull
|
|
188
|
+
# request's own changes show up. A path that does not exist on the base yet
|
|
189
|
+
# (the PR adds the first MCP config there) is compared against nothing.
|
|
190
|
+
scan_target="$INPUT_PATH"
|
|
191
|
+
if [[ ! -e "$base_dir/$INPUT_PATH" ]]; then mkdir -p "$empty_dir"; scan_target="$empty_dir"; fi
|
|
192
|
+
args=(scan "$scan_target" --evaluate -o "$base_eval")
|
|
193
|
+
[[ -n "$INPUT_POLICY" ]] && args+=(--policy "$(abs "$INPUT_POLICY")")
|
|
194
|
+
[[ -n "$INPUT_BASELINE" ]] && args+=(--baseline "$(abs "$INPUT_BASELINE")")
|
|
195
|
+
(cd "$base_dir" && "$PY" -m agentcapmap "${args[@]}") || soft "could not scan the base commit"
|
|
196
|
+
|
|
197
|
+
"$PY" -m agentcapmap diff "$base_eval" "$head_eval" --format json -o "$diff_json" || fail "diff failed"
|
|
198
|
+
"$PY" -m agentcapmap diff "$base_eval" "$head_eval" --format markdown -o "$diff_md" || fail "diff failed"
|
|
199
|
+
cat "$diff_md" >> "$GITHUB_STEP_SUMMARY"
|
|
200
|
+
|
|
201
|
+
# Build the comment body (marker first, trimmed to GitHub's size limit) and say
|
|
202
|
+
# whether anything changed.
|
|
203
|
+
changed=$("$PY" - "$diff_json" "$diff_md" "$body" <<'EOF'
|
|
204
|
+
import json, sys
|
|
205
|
+
data = json.load(open(sys.argv[1], encoding="utf-8"))
|
|
206
|
+
text = open(sys.argv[2], encoding="utf-8").read()
|
|
207
|
+
if len(text) > 60000:
|
|
208
|
+
text = text[:60000] + "\n\n_Truncated; see the job summary._\n"
|
|
209
|
+
marker = "<!-- agent-capmap-diff -->"
|
|
210
|
+
open(sys.argv[3], "w", encoding="utf-8").write(marker + "\n" + text)
|
|
211
|
+
keys = ("new_findings", "resolved_findings", "gained_capabilities",
|
|
212
|
+
"lost_capabilities", "new_not_assessed")
|
|
213
|
+
print(1 if any(data[k] for k in keys) else 0)
|
|
214
|
+
EOF
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
command -v gh >/dev/null 2>&1 || soft "gh is not installed on this runner; the diff is in the job summary"
|
|
218
|
+
# Only update a comment this action's own bot wrote, never one that merely
|
|
219
|
+
# contains the marker.
|
|
220
|
+
existing=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments?per_page=100" --paginate --jq '.[] | select(.user.login == "github-actions[bot]" and (.body | contains("<!-- agent-capmap-diff -->"))) | .id' 2>/dev/null | head -n1 || true)
|
|
221
|
+
if [[ "$changed" != "1" && -z "$existing" ]]; then
|
|
222
|
+
echo "No changes compared with the base; not commenting."
|
|
223
|
+
exit 0
|
|
224
|
+
fi
|
|
225
|
+
if [[ -n "$existing" && "$existing" =~ ^[0-9]+$ ]]; then
|
|
226
|
+
gh api --method PATCH "repos/${REPO}/issues/comments/${existing}" -F body=@"$body" >/dev/null || soft "could not update the comment (forks get a read-only token); the diff is in the job summary"
|
|
227
|
+
else
|
|
228
|
+
gh api --method POST "repos/${REPO}/issues/${PR_NUMBER}/comments" -F body=@"$body" >/dev/null || soft "could not post the comment (forks get a read-only token); the diff is in the job summary"
|
|
229
|
+
fi
|
|
@@ -31,15 +31,18 @@ jobs:
|
|
|
31
31
|
permissions:
|
|
32
32
|
contents: read # check out the code
|
|
33
33
|
security-events: write # upload SARIF to code scanning
|
|
34
|
+
pull-requests: write # post the "what changed" comment
|
|
34
35
|
steps:
|
|
35
36
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
36
37
|
with:
|
|
37
38
|
persist-credentials: false
|
|
39
|
+
fetch-depth: 0 # the comment compares with the base commit
|
|
38
40
|
- id: capmap
|
|
39
41
|
uses: NKwolv/agent-capmap@<AGENT_CAPMAP_SHA>
|
|
40
42
|
with:
|
|
41
43
|
path: .
|
|
42
44
|
fail-on: high
|
|
45
|
+
comment: "true" # posts new findings and newly added servers on the PR
|
|
43
46
|
- name: Upload SARIF
|
|
44
47
|
if: always() && steps.capmap.outputs.sarif-file != ''
|
|
45
48
|
uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
|
@@ -20,6 +20,8 @@ A complete example is in [`examples/agentcapmap.yml`](examples/agentcapmap.yml).
|
|
|
20
20
|
| `classify` | `false` | Classify tools with the LLM. Needs `enumerate: true` and `ANTHROPIC_API_KEY` (unless `offline`). |
|
|
21
21
|
| `offline` | `false` | Classify from the cache only. |
|
|
22
22
|
| `cache-dir` | none | Classification cache directory; restore it with `actions/cache`. |
|
|
23
|
+
| `comment` | `false` | On `pull_request`, post what changed against the base branch as one comment (see below). Config-only; cannot be combined with `enumerate`. |
|
|
24
|
+
| `github-token` | `github.token` | Token used to post the comment. |
|
|
23
25
|
| `sarif-file` | `agentcapmap.sarif` | Where to write SARIF (relative path inside the workspace). |
|
|
24
26
|
| `python-version` | `3.12` | Python used to run agent-capmap. |
|
|
25
27
|
|
|
@@ -29,6 +31,30 @@ Exit status: the action fails when a finding is at or above `fail-on` (or, with
|
|
|
29
31
|
`fail-on-not-assessed`, when anything was not assessed). The SARIF file and summary
|
|
30
32
|
are written first, so upload them in a step with `if: always()`.
|
|
31
33
|
|
|
34
|
+
## Pull request comment
|
|
35
|
+
|
|
36
|
+
With `comment: "true"`, the action scans the base commit as well, compares the two with
|
|
37
|
+
`agentcapmap diff`, and posts a single comment that lists **new findings**, **newly
|
|
38
|
+
unassessed servers** (in config-only mode this is how an added MCP server shows up),
|
|
39
|
+
gained capabilities and resolved findings. The comment is updated on every push instead
|
|
40
|
+
of adding new ones, and nothing is posted when nothing changed and there is no earlier
|
|
41
|
+
comment. The same text goes to the job summary.
|
|
42
|
+
|
|
43
|
+
Requirements:
|
|
44
|
+
|
|
45
|
+
- `permissions: pull-requests: write` on the job (plus `contents: read`).
|
|
46
|
+
- `actions/checkout` with `fetch-depth: 0`, so the base commit is available. Without it
|
|
47
|
+
the comparison is skipped with a warning.
|
|
48
|
+
- The `pull_request` event. Do not use `pull_request_target` to get write access on
|
|
49
|
+
forks: that runs with secrets against untrusted code. On a fork the token is read-only,
|
|
50
|
+
so the comment cannot be posted and the diff appears in the job summary only.
|
|
51
|
+
- Not with `enumerate: "true"`: the comment mode never starts servers, so it is safe on
|
|
52
|
+
pull requests.
|
|
53
|
+
|
|
54
|
+
The head's policy and baseline are used for both sides, so only the pull request's own
|
|
55
|
+
changes appear. A bot comment is only ever updated if the `github-actions[bot]` user
|
|
56
|
+
wrote it and it carries the action's hidden marker.
|
|
57
|
+
|
|
32
58
|
## Choosing a mode
|
|
33
59
|
|
|
34
60
|
| Mode | When | What it catches |
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agent-capmap"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.1"
|
|
8
8
|
description = "Inventory the MCP tools reachable by AI agents and flag dangerous capability combinations with policy-as-code."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Prepare a release: bump the version everywhere and date the changelog.
|
|
2
|
+
|
|
3
|
+
python scripts/prepare_release.py 0.1.1 # edit the files
|
|
4
|
+
python scripts/prepare_release.py 0.1.1 --dry-run # only report what would change
|
|
5
|
+
|
|
6
|
+
It refuses to run unless the working tree is clean, the version is a plain X.Y.Z that
|
|
7
|
+
is higher than the current one, `[Unreleased]` in CHANGELOG.md has entries, and the
|
|
8
|
+
bundled reference labels match `eval/labeled_servers/`. It never commits, tags or
|
|
9
|
+
pushes: it prints the remaining steps for you to run.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import datetime
|
|
16
|
+
import re
|
|
17
|
+
import subprocess # nosec B404
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
22
|
+
PYPROJECT = ROOT / "pyproject.toml"
|
|
23
|
+
INIT = ROOT / "src" / "agentcapmap" / "__init__.py"
|
|
24
|
+
CHANGELOG = ROOT / "CHANGELOG.md"
|
|
25
|
+
|
|
26
|
+
_SEMVER = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$")
|
|
27
|
+
_PYPROJECT_VERSION = re.compile(r'^version = "([^"]+)"$', re.MULTILINE)
|
|
28
|
+
_INIT_VERSION = re.compile(r'^__version__ = "([^"]+)"$', re.MULTILINE)
|
|
29
|
+
_UNRELEASED = "## [Unreleased]\n"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ReleaseError(ValueError):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse_version(text: str) -> tuple[int, int, int]:
|
|
37
|
+
match = _SEMVER.match(text)
|
|
38
|
+
if not match:
|
|
39
|
+
raise ReleaseError(f"{text!r} is not a plain X.Y.Z version")
|
|
40
|
+
major, minor, patch = (int(part) for part in match.groups())
|
|
41
|
+
return major, minor, patch
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def current_version(pyproject: str, init: str) -> str:
|
|
45
|
+
in_pyproject = _PYPROJECT_VERSION.search(pyproject)
|
|
46
|
+
in_init = _INIT_VERSION.search(init)
|
|
47
|
+
if not in_pyproject or not in_init:
|
|
48
|
+
raise ReleaseError("cannot find the version in pyproject.toml or __init__.py")
|
|
49
|
+
if in_pyproject.group(1) != in_init.group(1):
|
|
50
|
+
raise ReleaseError(
|
|
51
|
+
f"pyproject.toml says {in_pyproject.group(1)} but __init__.py says "
|
|
52
|
+
f"{in_init.group(1)}: fix that first"
|
|
53
|
+
)
|
|
54
|
+
return in_pyproject.group(1)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def unreleased_entries(changelog: str) -> str:
|
|
58
|
+
"""The text under `## [Unreleased]`, up to the next `## ` heading."""
|
|
59
|
+
start = changelog.find(_UNRELEASED)
|
|
60
|
+
if start == -1:
|
|
61
|
+
raise ReleaseError("CHANGELOG.md has no '## [Unreleased]' heading")
|
|
62
|
+
body = changelog[start + len(_UNRELEASED) :]
|
|
63
|
+
end = body.find("\n## ")
|
|
64
|
+
return (body if end == -1 else body[:end]).strip()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def bump(
|
|
68
|
+
version: str, date: str, pyproject: str, init: str, changelog: str
|
|
69
|
+
) -> tuple[str, str, str]:
|
|
70
|
+
"""Return the new text of (pyproject.toml, __init__.py, CHANGELOG.md)."""
|
|
71
|
+
old = current_version(pyproject, init)
|
|
72
|
+
if parse_version(version) <= parse_version(old):
|
|
73
|
+
raise ReleaseError(f"{version} is not higher than the current version {old}")
|
|
74
|
+
if not unreleased_entries(changelog):
|
|
75
|
+
raise ReleaseError("nothing under '## [Unreleased]' in CHANGELOG.md: describe the changes")
|
|
76
|
+
if f"## [{version}]" in changelog:
|
|
77
|
+
raise ReleaseError(f"CHANGELOG.md already has a section for {version}")
|
|
78
|
+
return (
|
|
79
|
+
_PYPROJECT_VERSION.sub(f'version = "{version}"', pyproject, count=1),
|
|
80
|
+
_INIT_VERSION.sub(f'__version__ = "{version}"', init, count=1),
|
|
81
|
+
changelog.replace(_UNRELEASED, f"{_UNRELEASED}\n## [{version}] - {date}\n", 1),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _git(*args: str) -> str:
|
|
86
|
+
# Fixed argument lists, no shell, no user input.
|
|
87
|
+
result = subprocess.run( # noqa: S603 # nosec B603
|
|
88
|
+
["git", *args], # noqa: S607 # nosec B607
|
|
89
|
+
cwd=ROOT,
|
|
90
|
+
capture_output=True,
|
|
91
|
+
text=True,
|
|
92
|
+
check=True,
|
|
93
|
+
)
|
|
94
|
+
return result.stdout
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _reference_labels_stale() -> bool:
|
|
98
|
+
sys.path.insert(0, str(ROOT / "scripts"))
|
|
99
|
+
import build_reference_labels as build # type: ignore[import-not-found]
|
|
100
|
+
|
|
101
|
+
from agentcapmap.classify.reference import dump_bundle
|
|
102
|
+
from agentcapmap.evaluation import load_labeled_servers
|
|
103
|
+
|
|
104
|
+
expected = dump_bundle(build.build_bundle(load_labeled_servers(build.LABELS_DIR)))
|
|
105
|
+
current = build.BUNDLE_PATH.read_text(encoding="utf-8") if build.BUNDLE_PATH.is_file() else ""
|
|
106
|
+
return bool(current != expected)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def main() -> int:
|
|
110
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0] if __doc__ else None)
|
|
111
|
+
parser.add_argument("version", help="new version, X.Y.Z")
|
|
112
|
+
parser.add_argument("--date", default=datetime.date.today().isoformat(), help="YYYY-MM-DD")
|
|
113
|
+
parser.add_argument("--dry-run", action="store_true", help="report only; change nothing")
|
|
114
|
+
args = parser.parse_args()
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", args.date):
|
|
118
|
+
raise ReleaseError("--date must be YYYY-MM-DD")
|
|
119
|
+
if _git("status", "--porcelain").strip():
|
|
120
|
+
raise ReleaseError("the working tree is not clean: commit or stash first")
|
|
121
|
+
if _reference_labels_stale():
|
|
122
|
+
raise ReleaseError(
|
|
123
|
+
"reference_labels.json is out of date: run scripts/build_reference_labels.py"
|
|
124
|
+
)
|
|
125
|
+
new = bump(
|
|
126
|
+
args.version,
|
|
127
|
+
args.date,
|
|
128
|
+
PYPROJECT.read_text(encoding="utf-8"),
|
|
129
|
+
INIT.read_text(encoding="utf-8"),
|
|
130
|
+
CHANGELOG.read_text(encoding="utf-8"),
|
|
131
|
+
)
|
|
132
|
+
except (ReleaseError, subprocess.CalledProcessError) as exc:
|
|
133
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
134
|
+
return 1
|
|
135
|
+
|
|
136
|
+
if args.dry_run:
|
|
137
|
+
print(
|
|
138
|
+
f"would release {args.version} ({args.date}): pyproject.toml, __init__.py, CHANGELOG.md"
|
|
139
|
+
)
|
|
140
|
+
return 0
|
|
141
|
+
for path, text in zip((PYPROJECT, INIT, CHANGELOG), new, strict=True):
|
|
142
|
+
path.write_text(text, encoding="utf-8", newline="\n")
|
|
143
|
+
print(
|
|
144
|
+
f"Prepared {args.version}. Next:\n"
|
|
145
|
+
" 1. AGENTCAPMAP_UPDATE_GOLDEN=1 python -m pytest tests/test_fixtures.py "
|
|
146
|
+
"(the demo inventory stamps the version), then python scripts/check.py\n"
|
|
147
|
+
f' 2. git commit -am "Release {args.version}" && git push origin main\n'
|
|
148
|
+
" 3. wait for CI on that commit to pass\n"
|
|
149
|
+
f' 4. git tag -a v{args.version} -m "agent-capmap {args.version}" '
|
|
150
|
+
f"&& git push origin v{args.version}\n"
|
|
151
|
+
" 5. approve the 'pypi' deployment, then confirm the release on PyPI\n"
|
|
152
|
+
" 6. update the NKwolv/agent-capmap@<sha> pin in docs/examples/agentcapmap.yml"
|
|
153
|
+
)
|
|
154
|
+
return 0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
if __name__ == "__main__":
|
|
158
|
+
sys.exit(main())
|
|
@@ -8,7 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
from pydantic import BaseModel, Field
|
|
10
10
|
|
|
11
|
-
from agentcapmap.findings import Evaluation, Finding, Severity
|
|
11
|
+
from agentcapmap.findings import Evaluation, Finding, NotAssessed, Severity
|
|
12
12
|
from agentcapmap.report._text import md_code
|
|
13
13
|
|
|
14
14
|
_MAX_ROWS = 100
|
|
@@ -25,6 +25,19 @@ class EvaluationDiff(BaseModel):
|
|
|
25
25
|
resolved_findings: list[Finding] = Field(default_factory=list)
|
|
26
26
|
gained_capabilities: list[CapabilityChange] = Field(default_factory=list)
|
|
27
27
|
lost_capabilities: list[CapabilityChange] = Field(default_factory=list)
|
|
28
|
+
# Servers or tools that could not be assessed now but were not before: in config-only
|
|
29
|
+
# mode this is how a newly added MCP server shows up.
|
|
30
|
+
new_not_assessed: list[NotAssessed] = Field(default_factory=list)
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def changed(self) -> bool:
|
|
34
|
+
return bool(
|
|
35
|
+
self.new_findings
|
|
36
|
+
or self.resolved_findings
|
|
37
|
+
or self.gained_capabilities
|
|
38
|
+
or self.lost_capabilities
|
|
39
|
+
or self.new_not_assessed
|
|
40
|
+
)
|
|
28
41
|
|
|
29
42
|
|
|
30
43
|
def _capabilities(evaluation: Evaluation) -> dict[tuple[str, str], list[str]]:
|
|
@@ -49,11 +62,15 @@ def diff_evaluations(old: Evaluation, new: Evaluation) -> EvaluationDiff:
|
|
|
49
62
|
old_prints = {f.fingerprint for f in old.findings}
|
|
50
63
|
new_prints = {f.fingerprint for f in new.findings}
|
|
51
64
|
old_caps, new_caps = _capabilities(old), _capabilities(new)
|
|
65
|
+
seen = {(n.agent_id, n.server, n.tool) for n in old.not_assessed}
|
|
52
66
|
return EvaluationDiff(
|
|
53
67
|
new_findings=[f for f in new.findings if f.fingerprint not in old_prints],
|
|
54
68
|
resolved_findings=[f for f in old.findings if f.fingerprint not in new_prints],
|
|
55
69
|
gained_capabilities=_changes(new_caps, old_caps),
|
|
56
70
|
lost_capabilities=_changes(old_caps, new_caps),
|
|
71
|
+
new_not_assessed=[
|
|
72
|
+
n for n in new.not_assessed if (n.agent_id, n.server, n.tool) not in seen
|
|
73
|
+
],
|
|
57
74
|
)
|
|
58
75
|
|
|
59
76
|
|
|
@@ -94,10 +111,33 @@ def _finding_lines(title: str, findings: list[Finding]) -> list[str]:
|
|
|
94
111
|
return [*lines, ""]
|
|
95
112
|
|
|
96
113
|
|
|
114
|
+
def _not_assessed_lines(items: list[NotAssessed]) -> list[str]:
|
|
115
|
+
lines = [f"## Newly not assessed ({len(items)})", ""]
|
|
116
|
+
if not items:
|
|
117
|
+
return [*lines, "None.", ""]
|
|
118
|
+
for item in items[:_MAX_ROWS]:
|
|
119
|
+
where = f"{item.server}/{item.tool}" if item.tool else item.server
|
|
120
|
+
lines.append(f"- {md_code(where, 160)}: {md_code(item.reason, 200)}")
|
|
121
|
+
if len(items) > _MAX_ROWS:
|
|
122
|
+
lines.append(f"- ... and {len(items) - _MAX_ROWS} more")
|
|
123
|
+
return [*lines, ""]
|
|
124
|
+
|
|
125
|
+
|
|
97
126
|
def render_markdown(diff: EvaluationDiff) -> str:
|
|
98
127
|
lines = ["# agent-capmap diff", ""]
|
|
128
|
+
if diff.changed:
|
|
129
|
+
lines += [
|
|
130
|
+
f"{len(diff.new_findings)} new finding(s), "
|
|
131
|
+
f"{len(diff.gained_capabilities)} gained capability(ies), "
|
|
132
|
+
f"{len(diff.new_not_assessed)} newly not assessed, "
|
|
133
|
+
f"{len(diff.resolved_findings)} resolved.",
|
|
134
|
+
"",
|
|
135
|
+
]
|
|
136
|
+
else:
|
|
137
|
+
lines += ["No changes compared with the base.", ""]
|
|
99
138
|
lines += _finding_lines("New findings", diff.new_findings)
|
|
100
139
|
lines += _capability_lines("Gained capabilities", diff.gained_capabilities)
|
|
140
|
+
lines += _not_assessed_lines(diff.new_not_assessed)
|
|
101
141
|
lines += _finding_lines("Resolved findings", diff.resolved_findings)
|
|
102
142
|
lines += _capability_lines("Lost capabilities", diff.lost_capabilities)
|
|
103
143
|
return "\n".join(lines)
|
|
@@ -92,3 +92,29 @@ def test_markdown_renders_hostile_text_inertly(risky: Evaluation) -> None:
|
|
|
92
92
|
text = render_markdown(diff)
|
|
93
93
|
assert "`[click](http://evil) <b>x</b>`" in text
|
|
94
94
|
assert text.startswith("# agent-capmap diff")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_newly_not_assessed_servers_are_reported(risky: Evaluation) -> None:
|
|
98
|
+
from agentcapmap.findings import NotAssessed
|
|
99
|
+
|
|
100
|
+
base = risky.model_copy(update={"not_assessed": []})
|
|
101
|
+
added = NotAssessed(
|
|
102
|
+
agent_id="a", server="new-server", reason="tools not enumerated (run scan with --enumerate)"
|
|
103
|
+
)
|
|
104
|
+
head = risky.model_copy(update={"not_assessed": [added]})
|
|
105
|
+
diff = diff_evaluations(base, head)
|
|
106
|
+
assert diff.new_not_assessed == [added]
|
|
107
|
+
# Already-unassessed items are not "new".
|
|
108
|
+
assert diff_evaluations(head, head).new_not_assessed == []
|
|
109
|
+
assert "`new-server`" in render_markdown(diff)
|
|
110
|
+
assert not should_fail_diff(diff, Severity.INFO, True) # informational only
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_changed_flag_and_summary_line(risky: Evaluation) -> None:
|
|
114
|
+
same = diff_evaluations(risky, risky)
|
|
115
|
+
assert same.changed is False
|
|
116
|
+
assert "No changes compared with the base." in render_markdown(same)
|
|
117
|
+
different = diff_evaluations(_emptied(risky), risky)
|
|
118
|
+
assert different.changed is True
|
|
119
|
+
summary = render_markdown(different).splitlines()[2]
|
|
120
|
+
assert summary.startswith(f"{len(different.new_findings)} new finding(s), ")
|
|
@@ -60,7 +60,7 @@ def test_golden_findings(name: str) -> None:
|
|
|
60
60
|
actual = _golden_view(_evaluate(name))
|
|
61
61
|
golden = FIXTURES / name / "expected.json"
|
|
62
62
|
if UPDATE_GOLDEN:
|
|
63
|
-
golden.write_text(json.dumps(actual, indent=2) + "\n", encoding="utf-8")
|
|
63
|
+
golden.write_text(json.dumps(actual, indent=2) + "\n", encoding="utf-8", newline="\n")
|
|
64
64
|
assert actual == json.loads(golden.read_text(encoding="utf-8"))
|
|
65
65
|
|
|
66
66
|
|
|
@@ -163,7 +163,7 @@ def build_demo_inventory() -> str:
|
|
|
163
163
|
def test_demo_inventory_is_current() -> None:
|
|
164
164
|
expected = build_demo_inventory()
|
|
165
165
|
if UPDATE_GOLDEN:
|
|
166
|
-
DEMO.write_text(expected, encoding="utf-8")
|
|
166
|
+
DEMO.write_text(expected, encoding="utf-8", newline="\n")
|
|
167
167
|
assert DEMO.read_text(encoding="utf-8") == expected, "run with AGENTCAPMAP_UPDATE_GOLDEN=1"
|
|
168
168
|
|
|
169
169
|
|