agent-capmap 0.1.0__py3-none-any.whl
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.dist-info/METADATA +295 -0
- agent_capmap-0.1.0.dist-info/RECORD +48 -0
- agent_capmap-0.1.0.dist-info/WHEEL +4 -0
- agent_capmap-0.1.0.dist-info/entry_points.txt +2 -0
- agent_capmap-0.1.0.dist-info/licenses/LICENSE +21 -0
- agentcapmap/__init__.py +3 -0
- agentcapmap/__main__.py +5 -0
- agentcapmap/baseline.py +125 -0
- agentcapmap/classify/__init__.py +221 -0
- agentcapmap/classify/cache.py +74 -0
- agentcapmap/classify/prompt.py +160 -0
- agentcapmap/classify/provider.py +122 -0
- agentcapmap/classify/reference.py +115 -0
- agentcapmap/classify/reference_labels.json +6135 -0
- agentcapmap/cli.py +392 -0
- agentcapmap/diff.py +103 -0
- agentcapmap/discovery/__init__.py +209 -0
- agentcapmap/discovery/_common.py +297 -0
- agentcapmap/discovery/claude_code.py +258 -0
- agentcapmap/discovery/claude_desktop.py +28 -0
- agentcapmap/discovery/cursor.py +33 -0
- agentcapmap/discovery/mcp_json.py +29 -0
- agentcapmap/discovery/vscode.py +65 -0
- agentcapmap/enumerate/__init__.py +278 -0
- agentcapmap/enumerate/hashing.py +37 -0
- agentcapmap/enumerate/http.py +313 -0
- agentcapmap/enumerate/netguard.py +60 -0
- agentcapmap/enumerate/stdio.py +273 -0
- agentcapmap/enumerate/transport.py +86 -0
- agentcapmap/evaluation.py +498 -0
- agentcapmap/findings.py +129 -0
- agentcapmap/graph/__init__.py +264 -0
- agentcapmap/graph/server_attrs.py +164 -0
- agentcapmap/limits.py +35 -0
- agentcapmap/models.py +186 -0
- agentcapmap/policies/default.yaml +113 -0
- agentcapmap/policy/__init__.py +24 -0
- agentcapmap/policy/engine.py +229 -0
- agentcapmap/policy/loader.py +223 -0
- agentcapmap/py.typed +0 -0
- agentcapmap/redact.py +268 -0
- agentcapmap/report/__init__.py +36 -0
- agentcapmap/report/_text.py +62 -0
- agentcapmap/report/markdown.py +152 -0
- agentcapmap/report/sarif.py +209 -0
- agentcapmap/schemas/policy.schema.json +172 -0
- agentcapmap/schemas/tool-tags.schema.json +57 -0
- agentcapmap/taxonomy.py +56 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agent-capmap
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Inventory the MCP tools reachable by AI agents and flag dangerous capability combinations with policy-as-code.
|
|
5
|
+
Project-URL: Homepage, https://github.com/NKwolv/agent-capmap
|
|
6
|
+
Project-URL: Documentation, https://github.com/NKwolv/agent-capmap#readme
|
|
7
|
+
Project-URL: Issues, https://github.com/NKwolv/agent-capmap/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/NKwolv/agent-capmap/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Security, https://github.com/NKwolv/agent-capmap/security/policy
|
|
10
|
+
Author: NKwolv
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: ai-agents,mcp,model-context-protocol,sarif,security
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: anthropic>=1.11
|
|
24
|
+
Requires-Dist: certifi>=2026.10.19
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Requires-Dist: jsonschema>=4.20
|
|
27
|
+
Requires-Dist: pydantic>=2.7
|
|
28
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: bandit>=1.8; extra == 'dev'
|
|
31
|
+
Requires-Dist: cyclonedx-bom>=7.5; extra == 'dev'
|
|
32
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
33
|
+
Requires-Dist: pip-audit>=2.7; extra == 'dev'
|
|
34
|
+
Requires-Dist: pre-commit>=3.7; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
37
|
+
Requires-Dist: types-jsonschema; extra == 'dev'
|
|
38
|
+
Requires-Dist: types-pyyaml; extra == 'dev'
|
|
39
|
+
Requires-Dist: uv>=0.13.4; extra == 'dev'
|
|
40
|
+
Requires-Dist: virtualenv>=21.14.9; extra == 'dev'
|
|
41
|
+
Requires-Dist: zizmor>=1.0; extra == 'dev'
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
|
|
44
|
+
# agent-capmap
|
|
45
|
+
|
|
46
|
+
**Find out what your AI agents can actually do, and which combinations of their tools
|
|
47
|
+
an attacker could abuse.**
|
|
48
|
+
|
|
49
|
+
AI assistants such as Claude Desktop, Cursor, VS Code and Claude Code get their powers
|
|
50
|
+
from MCP servers: read files, browse the web, send email, run shell commands. Each tool
|
|
51
|
+
can look harmless on its own. The danger is usually the combination: an agent that can
|
|
52
|
+
read private data, read attacker-written content (a web page, an issue, an email), and
|
|
53
|
+
send data out can be talked into leaking that data. That is the "lethal trifecta".
|
|
54
|
+
|
|
55
|
+
agent-capmap inventories every MCP server and tool reachable by each agent, classifies
|
|
56
|
+
what each tool can do, and applies policy-as-code rules to the **combination** of tools
|
|
57
|
+
per agent. Findings come with evidence and can gate CI through SARIF and exit codes.
|
|
58
|
+
|
|
59
|
+
> **Status: pre-release (0.1.0).** All features below work. The classifier's accuracy
|
|
60
|
+
> has not been benchmarked yet; see [Accuracy](#accuracy).
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
Requires Python 3.11+.
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
pipx install agent-capmap # or: uv tool install agent-capmap
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Releases are built and published by GitHub Actions with PyPI trusted publishing, so
|
|
71
|
+
each file on PyPI carries an attestation linking it to the workflow run that built it.
|
|
72
|
+
|
|
73
|
+
For a fully hash-pinned install, with the exact dependency versions CI tests, install
|
|
74
|
+
from a clone instead:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
git clone https://github.com/NKwolv/agent-capmap
|
|
78
|
+
cd agent-capmap
|
|
79
|
+
python -m venv .venv
|
|
80
|
+
. .venv/bin/activate # Windows PowerShell: .venv\Scripts\Activate.ps1
|
|
81
|
+
pip install --require-hashes -r requirements/runtime.lock
|
|
82
|
+
pip install --no-deps .
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
> **Locked-down Windows machines:** if your organization's policy (Device Guard,
|
|
86
|
+
> AppLocker, Smart App Control) blocks `agentcapmap.exe` or `pipx.exe`, run the tool
|
|
87
|
+
> through Python instead, which is the same program. If you installed with
|
|
88
|
+
> `pip install agent-capmap`, run `python -m agentcapmap scan --user`. If you installed
|
|
89
|
+
> with pipx, the package lives in pipx's own environment, so plain `python` will not
|
|
90
|
+
> find it: use that environment's interpreter (`python -m pipx environment --value
|
|
91
|
+
> PIPX_LOCAL_VENVS` shows the folder):
|
|
92
|
+
> `<venvs>\agent-capmap\Scripts\python.exe -m agentcapmap scan --user`. Do not try to
|
|
93
|
+
> bypass the policy; ask your administrator to allow the launcher if you want it.
|
|
94
|
+
|
|
95
|
+
> **Windows:** if `pip install` fails with "No such file or directory" deep inside
|
|
96
|
+
> `site-packages`, the path is too long for Windows' 260-character limit. Clone to a
|
|
97
|
+
> short path (for example `C:\src\agent-capmap`) or enable long path support.
|
|
98
|
+
|
|
99
|
+
## Quickstart (5 minutes, no API key)
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
# 1. See findings on a ready-made inventory of five example agents
|
|
103
|
+
# (from a clone, use examples/demo/inventory.json directly)
|
|
104
|
+
curl -fsSLO https://raw.githubusercontent.com/NKwolv/agent-capmap/main/examples/demo/inventory.json
|
|
105
|
+
agentcapmap evaluate inventory.json --format markdown
|
|
106
|
+
|
|
107
|
+
# 2. List the MCP servers configured on your own machine (reads files only)
|
|
108
|
+
agentcapmap scan --user
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
On Windows PowerShell, use `curl.exe` instead of `curl`.
|
|
112
|
+
|
|
113
|
+
Step 1 should report 2 critical, 2 high and 1 medium finding. Step 2 lists your
|
|
114
|
+
servers with every secret redacted, without starting anything.
|
|
115
|
+
|
|
116
|
+
## Example output
|
|
117
|
+
|
|
118
|
+
```markdown
|
|
119
|
+
### 1. `CAP-001` | Critical
|
|
120
|
+
|
|
121
|
+
`Lethal trifecta: the agent can read private data, ingest attacker-controlled content,
|
|
122
|
+
and send data externally, so injected instructions can exfiltrate data.`
|
|
123
|
+
|
|
124
|
+
- **Agent:** `generic:fixtures/trifecta/mcp.json` at `fixtures/trifecta/mcp.json:3`
|
|
125
|
+
- **Tools:** `github/get_file_contents`, `github/list_issues`, `mailer/send_email`
|
|
126
|
+
|
|
127
|
+
| Server | Tool | Matched | Evidence |
|
|
128
|
+
|---|---|---|---|
|
|
129
|
+
| `github` | `get_file_contents` | `data_access=private_read` | `contents of a file from a private or public repository` |
|
|
130
|
+
| `github` | `list_issues` | `untrusted_input=true` | `including issues opened by anyone` |
|
|
131
|
+
| `mailer` | `send_email` | `egress=external` | `Send an email to any recipient address` |
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
(Abridged from the real output of step 1.)
|
|
135
|
+
|
|
136
|
+
## How it works
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
config files ──► discover ──► enumerate ──► classify ──► policy ──► report
|
|
140
|
+
(read only) (initialize + (LLM tags (YAML (JSON, Markdown,
|
|
141
|
+
tools/list) 6 axes) rules) SARIF, exit code)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
1. **Discover** MCP configs: Claude Desktop, Cursor, VS Code (`.vscode/mcp.json`,
|
|
145
|
+
`settings.json`), Claude Code (`.mcp.json`, and with `--user` your `~/.claude.json`
|
|
146
|
+
user and per-project servers, including permission rules) and generic `mcp.json`.
|
|
147
|
+
Secrets are redacted everywhere.
|
|
148
|
+
2. **Enumerate** (`--enumerate`): start each server and ask only for its tool list.
|
|
149
|
+
Tools are never called.
|
|
150
|
+
3. **Classify** (`--classify`): an LLM tags each tool on six axes and flags tool
|
|
151
|
+
descriptions that try to manipulate the model. Results are cached and validated
|
|
152
|
+
against a JSON schema.
|
|
153
|
+
4. **Evaluate** (`--evaluate`): deterministic rules decide what is dangerous. The LLM
|
|
154
|
+
never makes risk decisions.
|
|
155
|
+
5. **Report**: JSON, Markdown, or SARIF for GitHub code scanning; `--fail-on` sets the
|
|
156
|
+
exit code.
|
|
157
|
+
|
|
158
|
+
### Full scan of your own setup (needs an API key)
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
export ANTHROPIC_API_KEY=<YOUR_API_KEY> # PowerShell: $env:ANTHROPIC_API_KEY = "..."
|
|
162
|
+
agentcapmap scan --user --enumerate --classify --evaluate --format markdown -o report.md
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`--enumerate` **runs the server commands in your configs**: only use it on configs you
|
|
166
|
+
trust. To try the whole pipeline on two known servers first, see
|
|
167
|
+
[`examples/live-trifecta`](https://github.com/NKwolv/agent-capmap/blob/main/examples/README.md).
|
|
168
|
+
|
|
169
|
+
## Capability taxonomy
|
|
170
|
+
|
|
171
|
+
| Axis | Values | Question |
|
|
172
|
+
|---|---|---|
|
|
173
|
+
| `data_access` | none, private_read, secrets_read | What non-public data can it read? |
|
|
174
|
+
| `untrusted_input` | false, true | Does it return content strangers can write? |
|
|
175
|
+
| `egress` | none, internal, external | Can it send data somewhere an outsider can read? |
|
|
176
|
+
| `mutation` | none, reversible_write, destructive | What can it change or delete? |
|
|
177
|
+
| `execution` | none, sandboxed, host | Does it run code, and where? |
|
|
178
|
+
| `privilege` | none, identity_admin | Does it manage users or permissions? |
|
|
179
|
+
|
|
180
|
+
Definitions and edge-case rulings: [docs/taxonomy.md](https://github.com/NKwolv/agent-capmap/blob/main/docs/taxonomy.md).
|
|
181
|
+
|
|
182
|
+
## Policy
|
|
183
|
+
|
|
184
|
+
The built-in [`policies/default.yaml`](https://github.com/NKwolv/agent-capmap/blob/main/policies/default.yaml) has nine rules:
|
|
185
|
+
|
|
186
|
+
| ID | Severity | Fires when an agent has |
|
|
187
|
+
|---|---|---|
|
|
188
|
+
| CAP-001 | critical | private data + untrusted input + external egress (lethal trifecta) |
|
|
189
|
+
| CAP-002 | critical | untrusted input + host code execution |
|
|
190
|
+
| CAP-003 | high | a destructive or host-execution tool with no approval gate |
|
|
191
|
+
| CAP-004 | high | secrets access + external egress |
|
|
192
|
+
| CAP-005 | high | a remote server with no authentication exposing capabilities |
|
|
193
|
+
| CAP-006 | high | tool definitions that changed since the baseline (rug pull) |
|
|
194
|
+
| CAP-007 | medium | a server not pinned to an exact version, or from an unvetted source |
|
|
195
|
+
| CAP-008 | high | a tool description that tries to instruct the model (tool poisoning) |
|
|
196
|
+
| CAP-009 | medium | the same tool name on two servers (shadowing) |
|
|
197
|
+
|
|
198
|
+
Write your own rules in the same format and pass `--policy my-policy.yaml`:
|
|
199
|
+
|
|
200
|
+
```yaml
|
|
201
|
+
- id: ACME-001
|
|
202
|
+
severity: high
|
|
203
|
+
when_server: {remote: true}
|
|
204
|
+
when_agent_has_any:
|
|
205
|
+
- privilege: identity_admin
|
|
206
|
+
message: Remote servers must not manage identities.
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Conditions: `when_agent_has_all`, `when_agent_has_any`, `when_server`,
|
|
210
|
+
`when_hash_changed`, `when_tool_name_shadowed`. The format is defined by
|
|
211
|
+
[`schemas/policy.schema.json`](https://github.com/NKwolv/agent-capmap/blob/main/schemas/policy.schema.json). Rug-pull detection:
|
|
212
|
+
|
|
213
|
+
```sh
|
|
214
|
+
agentcapmap baseline create inventory.json -o baseline.json # once, after review
|
|
215
|
+
agentcapmap evaluate new-inventory.json --baseline baseline.json
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
To see what changed between two runs (for example `main` versus a pull request), save
|
|
219
|
+
each `evaluate` output and compare them:
|
|
220
|
+
|
|
221
|
+
```sh
|
|
222
|
+
agentcapmap diff before.json after.json --format markdown
|
|
223
|
+
agentcapmap diff before.json after.json --fail-on high --fail-on-new-capability
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`diff` lists new and resolved findings and capabilities an agent gained or lost.
|
|
227
|
+
`--fail-on` looks at new findings only, so existing, already-accepted findings do not
|
|
228
|
+
fail the build; `--fail-on-new-capability` fails when an agent gains any capability.
|
|
229
|
+
|
|
230
|
+
## CI and GitHub code scanning
|
|
231
|
+
|
|
232
|
+
```sh
|
|
233
|
+
agentcapmap evaluate inventory.json --format sarif -o agentcapmap.sarif --fail-on high
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Exit codes: `0` passed, `1` findings at or above `--fail-on`, `2` usage or input error.
|
|
237
|
+
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
|
+
and the [example workflow](https://github.com/NKwolv/agent-capmap/blob/main/docs/examples/agentcapmap.yml). By default the Action reads
|
|
239
|
+
configs only, which is safe for pull requests from forks.
|
|
240
|
+
|
|
241
|
+
## Accuracy
|
|
242
|
+
|
|
243
|
+
The classifier is evaluated against hand-labeled tools from real MCP servers (see
|
|
244
|
+
[`eval/`](https://github.com/NKwolv/agent-capmap/blob/main/eval/README.md)). **No accuracy numbers are published yet.** The 381 labels
|
|
245
|
+
(28 servers) were drafted by one AI model and then reviewed by another AI model at the
|
|
246
|
+
maintainer's request; no person has independently reviewed them yet. A model grading a
|
|
247
|
+
model can overstate accuracy, so numbers will be quoted here only after a person has
|
|
248
|
+
reviewed the labels and the run is frozen (`python eval/run_eval.py`). The same labels
|
|
249
|
+
also power classification without an API key for these known tools; see
|
|
250
|
+
[Limitations](#limitations).
|
|
251
|
+
|
|
252
|
+
## Limitations
|
|
253
|
+
|
|
254
|
+
- **Descriptions, not behavior.** Tools are classified by what their definitions say. A
|
|
255
|
+
tool that lies about itself will be mis-tagged; agent-capmap never calls tools.
|
|
256
|
+
- **Classifier errors.** Tags can be wrong, especially for vague descriptions. Every
|
|
257
|
+
tag carries a confidence and an evidence phrase; phrases not found in the definition
|
|
258
|
+
are marked unverified.
|
|
259
|
+
- **Approval gates are mostly invisible.** Claude Code's permission settings are read
|
|
260
|
+
(so its default "ask before each MCP call" counts as a gate), but most other clients do
|
|
261
|
+
not record this, and CAP-003 treats "unknown" as "no gate" by default.
|
|
262
|
+
- **Enumeration runs code.** Starting servers executes the configured commands. A
|
|
263
|
+
started server can also read its parent's environment on Linux.
|
|
264
|
+
- **Coverage.** Config formats evolve; Windsurf is not read yet. MCP servers that do not
|
|
265
|
+
live in config files (Claude Code plugins, claude.ai connectors, IDE built-in servers,
|
|
266
|
+
managed settings) are not seen. WebSocket servers are listed but not enumerated.
|
|
267
|
+
Token scopes are not visible.
|
|
268
|
+
- **Reference labels are AI-reviewed.** For 381 tools from 28 well-known servers, the
|
|
269
|
+
classifier answers from bundled labels (matched by exact definition hash) instead of
|
|
270
|
+
calling a model, so no API key is needed. Those labels were reviewed by an AI model, not
|
|
271
|
+
yet by a person, and a wrong label is returned with full confidence; any change to a
|
|
272
|
+
tool's definition falls back to the cache or the LLM. Treat them as a convenience.
|
|
273
|
+
- **Not assessed is not safe.** Servers that could not be enumerated and tools that
|
|
274
|
+
could not be classified are listed under `not_assessed`, never assumed safe.
|
|
275
|
+
|
|
276
|
+
## Safety
|
|
277
|
+
|
|
278
|
+
Secrets from configs are redacted in all output and logs. Enumeration sends only
|
|
279
|
+
`initialize` and `tools/list`, with a minimal environment for launched servers, no
|
|
280
|
+
credential-forwarding redirects, and size limits on everything a server returns.
|
|
281
|
+
Reports render untrusted text inertly. See [SECURITY.md](https://github.com/NKwolv/agent-capmap/blob/main/SECURITY.md) and the
|
|
282
|
+
[threat model](https://github.com/NKwolv/agent-capmap/blob/main/docs/threat-model.md).
|
|
283
|
+
|
|
284
|
+
## Documentation
|
|
285
|
+
|
|
286
|
+
- [Capability taxonomy](https://github.com/NKwolv/agent-capmap/blob/main/docs/taxonomy.md)
|
|
287
|
+
- [Threat model](https://github.com/NKwolv/agent-capmap/blob/main/docs/threat-model.md): attacks covered, OWASP / MITRE ATLAS mapping
|
|
288
|
+
- [GitHub Action](https://github.com/NKwolv/agent-capmap/blob/main/docs/github-action.md)
|
|
289
|
+
- [Comparison with Snyk Agent Scan (mcp-scan)](https://github.com/NKwolv/agent-capmap/blob/main/docs/vs-mcp-scan.md)
|
|
290
|
+
- [Examples](https://github.com/NKwolv/agent-capmap/blob/main/examples/README.md) · [Evaluation](https://github.com/NKwolv/agent-capmap/blob/main/eval/README.md) · [POC checklist](https://github.com/NKwolv/agent-capmap/blob/main/docs/poc-checklist.md)
|
|
291
|
+
- [Contributing](https://github.com/NKwolv/agent-capmap/blob/main/CONTRIBUTING.md) · [Security policy](https://github.com/NKwolv/agent-capmap/blob/main/SECURITY.md) · [Changelog](https://github.com/NKwolv/agent-capmap/blob/main/CHANGELOG.md)
|
|
292
|
+
|
|
293
|
+
## License
|
|
294
|
+
|
|
295
|
+
MIT. See [LICENSE](https://github.com/NKwolv/agent-capmap/blob/main/LICENSE).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
agentcapmap/__init__.py,sha256=obKLf0ZZ_1JxejXRMSVoQyyXCUUVpKnpg70raMECb8s,92
|
|
2
|
+
agentcapmap/__main__.py,sha256=7ZDXsBQ0WkuCXfmY-_jkFHDuEhAWAJluGZqddbGItYo,63
|
|
3
|
+
agentcapmap/baseline.py,sha256=wUD1fRL40fCcSgfI1PnyXQJeM0yhv-WZw6u1woUkURo,4676
|
|
4
|
+
agentcapmap/cli.py,sha256=OdRJN5_ROn5U9q9ZstDxSIY--o-JGIrgOn5TIfMmDV0,14835
|
|
5
|
+
agentcapmap/diff.py,sha256=XU51_reeUwDgwz14hP3-I2HWaA7B94L6gaWO2khxads,3964
|
|
6
|
+
agentcapmap/evaluation.py,sha256=uxHDh65c9wnVZgsEAXKPdlmuPwG4DCYJLr2iTNs-pbM,17200
|
|
7
|
+
agentcapmap/findings.py,sha256=lZX41bb-ffGlijX64PA_mOa7yzDsvFwZZajLrDMzHrU,3503
|
|
8
|
+
agentcapmap/limits.py,sha256=OE1d4VkXY5ceN_F7wOnUBROS9RzJo4jLVnTkh4wmYYc,1231
|
|
9
|
+
agentcapmap/models.py,sha256=74ZlxchqRzm6m_SNiVmpnSr05dB3v9kT2xtKVHq9Sbg,5931
|
|
10
|
+
agentcapmap/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
agentcapmap/redact.py,sha256=ZAjJVoDZ1rGIjGhwWCi32XWkGtKU4Zl6NUAF8yq4aes,9507
|
|
12
|
+
agentcapmap/taxonomy.py,sha256=Iuw4c_kThiyjrzwHW_O9Yc-7aCOOiZE6K_cyR53O1y4,1869
|
|
13
|
+
agentcapmap/classify/__init__.py,sha256=Mgw5f2loU6FBgpXGWR22LkpykDz0qKEv5OWjNMXnpKs,8606
|
|
14
|
+
agentcapmap/classify/cache.py,sha256=bJzpbNi4LtixN8WsvJOdiZeGftaqihpCB6Vy26soGng,2531
|
|
15
|
+
agentcapmap/classify/prompt.py,sha256=I1kDpKwkdCdOzcF0BpA20LyoHvY3FQBq4_MC0A0CU_c,8682
|
|
16
|
+
agentcapmap/classify/provider.py,sha256=fjGy0VpT_aOhkSvhGaSETaX8Pf-r89OdS_C7mgWHbaA,4878
|
|
17
|
+
agentcapmap/classify/reference.py,sha256=8FXGPrpKZBeviXd-aQfX9jZ2Sqa8SBhPIQHNTE4qE9E,4034
|
|
18
|
+
agentcapmap/classify/reference_labels.json,sha256=EB7LaRAEWrni8azwAYPlHh2oDb881rO_dbwjZ0H6xjk,208335
|
|
19
|
+
agentcapmap/discovery/__init__.py,sha256=a3b4t-otqWLl9XfUOs-SUwZoYMbQ6ttAM4FwsVeegn0,7554
|
|
20
|
+
agentcapmap/discovery/_common.py,sha256=u0WA0IA1023LBsXZnjT-Eao9P2j_X-0l-nV1AwHzjSI,10043
|
|
21
|
+
agentcapmap/discovery/claude_code.py,sha256=Z6AL7a95jAVGIvEN0vyLwtcXRMvrI4OTMppsMCNep-4,10239
|
|
22
|
+
agentcapmap/discovery/claude_desktop.py,sha256=7PsElFL8fD1L4RhxkS7UhZ-euEKYJh48a4rDpfvnLUc,833
|
|
23
|
+
agentcapmap/discovery/cursor.py,sha256=cTMK8hRYi67YnXqOxLcZzD07f6TnQYHVlUPpLXDeUms,1039
|
|
24
|
+
agentcapmap/discovery/mcp_json.py,sha256=aYfWGVHZMtM6miFCYhRax-1RoNRsOcc4Jka-LhI7KWc,936
|
|
25
|
+
agentcapmap/discovery/vscode.py,sha256=eewYklqnyKMOLjYfUnMG4Q1FoW2j3DHw4RaWRC9Ib-Q,2126
|
|
26
|
+
agentcapmap/enumerate/__init__.py,sha256=7891bEb59OuXyu-wJKmrEwXUdREFKVzyHhctycrm8GE,10958
|
|
27
|
+
agentcapmap/enumerate/hashing.py,sha256=5gno5bAiAuvLfKdTx8X4YmKMebkJhlQp0MESmXxb3Zc,1214
|
|
28
|
+
agentcapmap/enumerate/http.py,sha256=Pv3wB1hNZ2Jw2bMO7TxZok4_rzXVVkU8lBg1bKp4q0A,12321
|
|
29
|
+
agentcapmap/enumerate/netguard.py,sha256=Qgi4xYwi1uMu89kRJlKIFk72MW7uq2yISF26M13KknE,2554
|
|
30
|
+
agentcapmap/enumerate/stdio.py,sha256=2Z4Dj7Whun5IB7CrT4OXh6CObb-PBD8WFoW98wN_OE0,10403
|
|
31
|
+
agentcapmap/enumerate/transport.py,sha256=Q55C7k2VnSLgifXGOLbTcTvaFmm6duXczT8lX9zATKI,2771
|
|
32
|
+
agentcapmap/graph/__init__.py,sha256=S7wEDGXrbbE8y93ZhWlHnHnv25PNX7AzOcI4j5OK0CI,10035
|
|
33
|
+
agentcapmap/graph/server_attrs.py,sha256=kxTKBhHjLWIkEz__FCSeMuKBHN-crDgjg_f6DyefJr4,6748
|
|
34
|
+
agentcapmap/policy/__init__.py,sha256=Wgt961Ff76YBcBbmqcLel3Mj9HihrhbEPJVsdCZ0YHI,489
|
|
35
|
+
agentcapmap/policy/engine.py,sha256=S3S_EBI5wS4zxHjWt-XrZ7yDdOzde6SCElENqZcHOqk,8406
|
|
36
|
+
agentcapmap/policy/loader.py,sha256=K2TH0RcDFT_1vextVrcBHbPC17s73DSBjUtcyCErI9c,8033
|
|
37
|
+
agentcapmap/report/__init__.py,sha256=9Dw-xMcpEFbx7_I8C_Y-ITapDubm6SZWVD_bJ6rSXIw,1127
|
|
38
|
+
agentcapmap/report/_text.py,sha256=iGk1PXB6Z_Dwir9nlqFoTVXK9GJsGe0qSsMzJUgcF4g,2099
|
|
39
|
+
agentcapmap/report/markdown.py,sha256=Uo2fZLlJoJWYS7cNDopVyB50aF7-SBZvZth7dEkiRUs,6083
|
|
40
|
+
agentcapmap/report/sarif.py,sha256=lUpOM8UEQ3zaAikyucX_c3P_tbUTZHmuCxDi2hqQVl8,7696
|
|
41
|
+
agentcapmap/policies/default.yaml,sha256=49mVVNkh64uHTrJjBvPozSnoWWdKwmcn08_QzUF8mVY,4325
|
|
42
|
+
agentcapmap/schemas/policy.schema.json,sha256=7UTVJP2QiO54oONKDXp44K7kpVX9o7pg5hvcNkUg8gA,6423
|
|
43
|
+
agentcapmap/schemas/tool-tags.schema.json,sha256=HCiOD69uRap1rVXmflQaUbSdNXMYgwrcNQFzItz91KQ,2171
|
|
44
|
+
agent_capmap-0.1.0.dist-info/METADATA,sha256=qwxAB-bCUuSd7dBoJWPERlA13q40dg1Ni-tQsZyesp4,14678
|
|
45
|
+
agent_capmap-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
46
|
+
agent_capmap-0.1.0.dist-info/entry_points.txt,sha256=olKJIQuXulXL9T9E_SdreA0Ggy-Ylq-NbgVynJTfH6Y,53
|
|
47
|
+
agent_capmap-0.1.0.dist-info/licenses/LICENSE,sha256=FnKTHxvCRPab3DyRc6SWm-NbmCvvAfZ7IrvGSMaXhIo,1063
|
|
48
|
+
agent_capmap-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NKwolv
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
agentcapmap/__init__.py
ADDED
agentcapmap/__main__.py
ADDED
agentcapmap/baseline.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Baseline pinning of tool definition hashes, for rug-pull detection (CAP-006).
|
|
2
|
+
|
|
3
|
+
`agentcapmap baseline create` records, per server, the hash of every tool definition.
|
|
4
|
+
Later scans compare against it: a changed, added or removed tool means the server's
|
|
5
|
+
definitions changed after review.
|
|
6
|
+
|
|
7
|
+
A server is identified by its name, transport and (redacted) launch command or URL, so
|
|
8
|
+
a deliberate config change (e.g. a new pinned version) is a new server, while the same
|
|
9
|
+
config serving different definitions is a change.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import hashlib
|
|
15
|
+
import json
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Annotated, Literal
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel, ConfigDict, Field, StringConstraints, ValidationError
|
|
20
|
+
|
|
21
|
+
from agentcapmap.limits import MAX_CONFIG_BYTES, MAX_TOOLS_PER_SERVER, loads_untrusted
|
|
22
|
+
from agentcapmap.models import EnumerationStatus, Inventory, Server
|
|
23
|
+
|
|
24
|
+
BASELINE_VERSION: Literal[1] = 1
|
|
25
|
+
_MAX_BASELINE_BYTES = 16 * MAX_CONFIG_BYTES
|
|
26
|
+
_MAX_SERVERS = 10_000
|
|
27
|
+
|
|
28
|
+
Sha256Hex = Annotated[str, StringConstraints(pattern=r"^[0-9a-f]{64}$")]
|
|
29
|
+
ToolName = Annotated[str, StringConstraints(min_length=1, max_length=512)]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BaselineError(ValueError):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BaselineServer(BaseModel):
|
|
37
|
+
model_config = ConfigDict(extra="forbid")
|
|
38
|
+
|
|
39
|
+
name: Annotated[str, StringConstraints(min_length=1, max_length=512)]
|
|
40
|
+
tools: dict[ToolName, Sha256Hex] = Field(max_length=MAX_TOOLS_PER_SERVER)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Baseline(BaseModel):
|
|
44
|
+
model_config = ConfigDict(extra="forbid")
|
|
45
|
+
|
|
46
|
+
baseline_version: Literal[1] = BASELINE_VERSION
|
|
47
|
+
servers: dict[Sha256Hex, BaselineServer] = Field(default_factory=dict, max_length=_MAX_SERVERS)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class HashDiff(BaseModel):
|
|
51
|
+
added: list[str] = Field(default_factory=list)
|
|
52
|
+
removed: list[str] = Field(default_factory=list)
|
|
53
|
+
changed: list[str] = Field(default_factory=list)
|
|
54
|
+
# name -> {"baseline": hash, "current": hash} for changed tools
|
|
55
|
+
hashes: dict[str, dict[str, str]] = Field(default_factory=dict)
|
|
56
|
+
|
|
57
|
+
def __bool__(self) -> bool:
|
|
58
|
+
return bool(self.added or self.removed or self.changed)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def server_identity(server: Server) -> str:
|
|
62
|
+
material = json.dumps(
|
|
63
|
+
[server.name, server.transport.value, server.command, server.args, server.url],
|
|
64
|
+
separators=(",", ":"),
|
|
65
|
+
ensure_ascii=False,
|
|
66
|
+
)
|
|
67
|
+
return hashlib.sha256(material.encode("utf-8")).hexdigest()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _current_tools(server: Server) -> dict[str, str] | None:
|
|
71
|
+
enumeration = server.enumeration
|
|
72
|
+
if enumeration is None or enumeration.status is not EnumerationStatus.OK:
|
|
73
|
+
return None
|
|
74
|
+
return {tool.name: tool.definition_hash for tool in enumeration.tools}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def create_baseline(inventory: Inventory) -> Baseline:
|
|
78
|
+
"""Pin every successfully enumerated, enabled server in the inventory."""
|
|
79
|
+
baseline = Baseline()
|
|
80
|
+
for agent in inventory.agents:
|
|
81
|
+
for server in agent.servers:
|
|
82
|
+
tools = _current_tools(server)
|
|
83
|
+
if server.disabled or tools is None:
|
|
84
|
+
continue
|
|
85
|
+
baseline.servers[server_identity(server)] = BaselineServer(
|
|
86
|
+
name=server.name, tools=dict(sorted(tools.items()))
|
|
87
|
+
)
|
|
88
|
+
baseline.servers = dict(sorted(baseline.servers.items()))
|
|
89
|
+
return baseline
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def load_baseline(path: Path) -> Baseline:
|
|
93
|
+
try:
|
|
94
|
+
size = path.stat().st_size
|
|
95
|
+
raw = path.read_bytes() if size <= _MAX_BASELINE_BYTES else b""
|
|
96
|
+
except OSError as exc:
|
|
97
|
+
raise BaselineError(f"cannot read baseline: {exc.strerror}") from None
|
|
98
|
+
if size > _MAX_BASELINE_BYTES:
|
|
99
|
+
raise BaselineError(f"baseline file larger than {_MAX_BASELINE_BYTES} bytes")
|
|
100
|
+
try:
|
|
101
|
+
data = loads_untrusted(raw)
|
|
102
|
+
except ValueError as exc:
|
|
103
|
+
raise BaselineError(f"baseline is not valid JSON: {exc}") from None
|
|
104
|
+
try:
|
|
105
|
+
return Baseline.model_validate(data, strict=True)
|
|
106
|
+
except ValidationError as exc:
|
|
107
|
+
raise BaselineError(
|
|
108
|
+
f"baseline does not match the expected format ({exc.error_count()} error(s))"
|
|
109
|
+
) from None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def diff_server(baseline: Baseline, server: Server) -> HashDiff | None:
|
|
113
|
+
"""None when the server is not in the baseline or could not be enumerated now."""
|
|
114
|
+
pinned = baseline.servers.get(server_identity(server))
|
|
115
|
+
current = _current_tools(server)
|
|
116
|
+
if pinned is None or current is None:
|
|
117
|
+
return None
|
|
118
|
+
before, after = pinned.tools, current
|
|
119
|
+
changed = sorted(n for n in before.keys() & after.keys() if before[n] != after[n])
|
|
120
|
+
return HashDiff(
|
|
121
|
+
added=sorted(after.keys() - before.keys()),
|
|
122
|
+
removed=sorted(before.keys() - after.keys()),
|
|
123
|
+
changed=changed,
|
|
124
|
+
hashes={n: {"baseline": before[n], "current": after[n]} for n in changed},
|
|
125
|
+
)
|