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.
Files changed (183) hide show
  1. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/CHANGELOG.md +13 -0
  2. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/CONTRIBUTING.md +34 -11
  3. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/PKG-INFO +3 -1
  4. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/README.md +2 -0
  5. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/action.yml +92 -0
  6. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/docs/examples/agentcapmap.yml +3 -0
  7. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/docs/github-action.md +26 -0
  8. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/examples/demo/inventory.json +1 -1
  9. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/pyproject.toml +1 -1
  10. agent_capmap-0.1.1/scripts/prepare_release.py +158 -0
  11. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/__init__.py +1 -1
  12. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/diff.py +41 -1
  13. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_diff.py +26 -0
  14. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_fixtures.py +2 -2
  15. agent_capmap-0.1.1/tests/test_release_script.py +84 -0
  16. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.env.example +0 -0
  17. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.gitattributes +0 -0
  18. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  19. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.github/ISSUE_TEMPLATE/classification.yml +0 -0
  20. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.github/ISSUE_TEMPLATE/client_support.yml +0 -0
  21. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  22. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.github/dependabot.yml +0 -0
  23. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.github/pull_request_template.md +0 -0
  24. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.github/workflows/action-selftest.yml +0 -0
  25. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.github/workflows/ci.yml +0 -0
  26. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.github/workflows/codeql.yml +0 -0
  27. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.github/workflows/release.yml +0 -0
  28. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.gitignore +0 -0
  29. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.gitleaks.toml +0 -0
  30. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.gitleaksignore +0 -0
  31. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/.pre-commit-config.yaml +0 -0
  32. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/AGENTS.md +0 -0
  33. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/CLAUDE.md +0 -0
  34. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/CODE_OF_CONDUCT.md +0 -0
  35. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/LICENSE +0 -0
  36. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/SECURITY.md +0 -0
  37. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/docs/blog-outline.md +0 -0
  38. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/docs/poc-checklist.md +0 -0
  39. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/docs/taxonomy.md +0 -0
  40. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/docs/threat-model.md +0 -0
  41. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/docs/vs-mcp-scan.md +0 -0
  42. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/README.md +0 -0
  43. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/agents.json +0 -0
  44. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/apply_labels.py +0 -0
  45. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/capture.py +0 -0
  46. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/catalog.json +0 -0
  47. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/aws-api.json +0 -0
  48. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/aws-iam.json +0 -0
  49. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/azure.json +0 -0
  50. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/brave-search.json +0 -0
  51. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/commands.json +0 -0
  52. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/context7.json +0 -0
  53. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/deepwiki.json +0 -0
  54. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/desktop-commander.json +0 -0
  55. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/exa.json +0 -0
  56. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/fetch.json +0 -0
  57. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/filesystem.json +0 -0
  58. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/firecrawl.json +0 -0
  59. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/git.json +0 -0
  60. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/github.json +0 -0
  61. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/gitlab.json +0 -0
  62. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/heroku.json +0 -0
  63. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/memory.json +0 -0
  64. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/notion.json +0 -0
  65. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/obsidian.json +0 -0
  66. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/playwright.json +0 -0
  67. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/postgres.json +0 -0
  68. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/qdrant.json +0 -0
  69. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/sentry.json +0 -0
  70. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/sequential-thinking.json +0 -0
  71. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/slack.json +0 -0
  72. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/supabase.json +0 -0
  73. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/tavily.json +0 -0
  74. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/labeled_servers/time.json +0 -0
  75. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/eval/run_eval.py +0 -0
  76. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/examples/README.md +0 -0
  77. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/examples/live-trifecta/mcp.json +0 -0
  78. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/examples/live-trifecta/sandbox/README.md +0 -0
  79. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/README.md +0 -0
  80. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/clean/expected.json +0 -0
  81. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/clean/mcp.json +0 -0
  82. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/clean/tools.json +0 -0
  83. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/exec-injection/expected.json +0 -0
  84. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/exec-injection/mcp.json +0 -0
  85. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/exec-injection/tools.json +0 -0
  86. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/no-approval-destructive/expected.json +0 -0
  87. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/no-approval-destructive/mcp.json +0 -0
  88. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/no-approval-destructive/tools.json +0 -0
  89. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/overscoped-token/expected.json +0 -0
  90. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/overscoped-token/mcp.json +0 -0
  91. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/overscoped-token/tools.json +0 -0
  92. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/poisoned-description/expected.json +0 -0
  93. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/poisoned-description/mcp.json +0 -0
  94. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/poisoned-description/tools.json +0 -0
  95. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/rug-pull/baseline.json +0 -0
  96. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/rug-pull/expected.json +0 -0
  97. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/rug-pull/mcp.json +0 -0
  98. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/rug-pull/tools.json +0 -0
  99. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/shadowing/expected.json +0 -0
  100. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/shadowing/mcp.json +0 -0
  101. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/shadowing/tools.json +0 -0
  102. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/trifecta/expected.json +0 -0
  103. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/trifecta/mcp.json +0 -0
  104. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/fixtures/trifecta/tools.json +0 -0
  105. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/policies/default.yaml +0 -0
  106. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/requirements/build.in +0 -0
  107. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/requirements/build.lock +0 -0
  108. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/requirements/dev.lock +0 -0
  109. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/requirements/runtime.lock +0 -0
  110. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/schemas/policy.schema.json +0 -0
  111. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/schemas/tool-tags.schema.json +0 -0
  112. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/scripts/build_reference_labels.py +0 -0
  113. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/scripts/check.py +0 -0
  114. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/__main__.py +0 -0
  115. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/baseline.py +0 -0
  116. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/classify/__init__.py +0 -0
  117. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/classify/cache.py +0 -0
  118. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/classify/prompt.py +0 -0
  119. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/classify/provider.py +0 -0
  120. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/classify/reference.py +0 -0
  121. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/classify/reference_labels.json +0 -0
  122. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/cli.py +0 -0
  123. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/discovery/__init__.py +0 -0
  124. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/discovery/_common.py +0 -0
  125. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/discovery/claude_code.py +0 -0
  126. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/discovery/claude_desktop.py +0 -0
  127. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/discovery/cursor.py +0 -0
  128. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/discovery/mcp_json.py +0 -0
  129. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/discovery/vscode.py +0 -0
  130. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/enumerate/__init__.py +0 -0
  131. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/enumerate/hashing.py +0 -0
  132. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/enumerate/http.py +0 -0
  133. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/enumerate/netguard.py +0 -0
  134. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/enumerate/stdio.py +0 -0
  135. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/enumerate/transport.py +0 -0
  136. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/evaluation.py +0 -0
  137. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/findings.py +0 -0
  138. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/graph/__init__.py +0 -0
  139. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/graph/server_attrs.py +0 -0
  140. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/limits.py +0 -0
  141. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/models.py +0 -0
  142. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/policy/__init__.py +0 -0
  143. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/policy/engine.py +0 -0
  144. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/policy/loader.py +0 -0
  145. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/py.typed +0 -0
  146. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/redact.py +0 -0
  147. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/report/__init__.py +0 -0
  148. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/report/_text.py +0 -0
  149. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/report/markdown.py +0 -0
  150. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/report/sarif.py +0 -0
  151. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/src/agentcapmap/taxonomy.py +0 -0
  152. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/__init__.py +0 -0
  153. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/conftest.py +0 -0
  154. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/fake_llm.py +0 -0
  155. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/fake_secrets.py +0 -0
  156. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/fixtures/configs/claude_code/.mcp.json +0 -0
  157. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/fixtures/configs/claude_desktop/claude_desktop_config.json +0 -0
  158. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/fixtures/configs/cursor/.cursor/mcp.json +0 -0
  159. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/fixtures/configs/generic/mcp.json +0 -0
  160. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/fixtures/configs/vscode/.vscode/mcp.json +0 -0
  161. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/fixtures/configs/vscode/.vscode/settings.json +0 -0
  162. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/fixtures/sarif/SOURCE.md +0 -0
  163. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/fixtures/sarif/sarif-schema-2.1.0.json +0 -0
  164. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/mock_mcp_server.py +0 -0
  165. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/scenarios.py +0 -0
  166. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_baseline.py +0 -0
  167. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_classify.py +0 -0
  168. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_claude_code.py +0 -0
  169. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_cli.py +0 -0
  170. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_discovery.py +0 -0
  171. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_enumerate_http.py +0 -0
  172. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_enumerate_stdio.py +0 -0
  173. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_evaluation.py +0 -0
  174. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_hardening.py +0 -0
  175. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_hashing.py +0 -0
  176. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_jsonc.py +0 -0
  177. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_netguard.py +0 -0
  178. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_policy.py +0 -0
  179. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_provider.py +0 -0
  180. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_redact.py +0 -0
  181. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_reference.py +0 -0
  182. {agent_capmap-0.1.0 → agent_capmap-0.1.1}/tests/test_report.py +0 -0
  183. {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
- 1. Set `version` in `pyproject.toml` and move the `[Unreleased]` entries in
158
- `CHANGELOG.md` under a new `[X.Y.Z] - YYYY-MM-DD` heading. Merge that to `main`.
159
- 2. Tag the merge commit and push the tag: `git tag vX.Y.Z && git push origin vX.Y.Z`.
160
- 3. `.github/workflows/release.yml` checks that the tag matches the version, runs the
161
- tests and pip-audit, builds the sdist and wheel from the hash-pinned build lock,
162
- and waits for approval on the `pypi` environment. After approval it publishes to
163
- PyPI with trusted publishing (no stored token; PyPI attestations are generated
164
- automatically) and creates the GitHub release with the same files.
165
- 4. Update the `uses: NKwolv/agent-capmap@<sha>` pin in
166
- `docs/examples/agentcapmap.yml` to the tagged commit.
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.0
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 |
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema_version": "1",
3
- "tool_version": "0.1.0",
3
+ "tool_version": "0.1.1",
4
4
  "agents": [
5
5
  {
6
6
  "id": "generic:fixtures/clean/mcp.json",
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agent-capmap"
7
- version = "0.1.0"
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())
@@ -1,3 +1,3 @@
1
1
  """agent-capmap: map the MCP capabilities reachable by AI agents."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.1.1"
@@ -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