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,209 @@
|
|
|
1
|
+
"""SARIF 2.1.0 output for GitHub code scanning.
|
|
2
|
+
|
|
3
|
+
- Rules come from the policy; GitHub ranks them by `security-severity`.
|
|
4
|
+
- Each result points at the config file and line where the (first) server is defined.
|
|
5
|
+
- Locations must be repository-relative. Absolute paths, `..` segments and URLs are
|
|
6
|
+
never emitted; GitHub rejects results without a location, so such findings are left
|
|
7
|
+
out of the SARIF (still present in JSON/Markdown) and a notification says so.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from typing import Any
|
|
14
|
+
from urllib.parse import quote
|
|
15
|
+
|
|
16
|
+
from agentcapmap import __version__
|
|
17
|
+
from agentcapmap.findings import Evaluation, Finding, RuleInfo, ServerLocation, Severity
|
|
18
|
+
from agentcapmap.report._text import clean
|
|
19
|
+
|
|
20
|
+
SARIF_SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json"
|
|
21
|
+
INFORMATION_URI = "https://github.com/NKwolv/agent-capmap"
|
|
22
|
+
FINGERPRINT_KEY = "agentcapmapFinding/v1"
|
|
23
|
+
|
|
24
|
+
_LEVEL = {
|
|
25
|
+
Severity.CRITICAL: "error",
|
|
26
|
+
Severity.HIGH: "error",
|
|
27
|
+
Severity.MEDIUM: "warning",
|
|
28
|
+
Severity.LOW: "note",
|
|
29
|
+
Severity.INFO: "note",
|
|
30
|
+
}
|
|
31
|
+
# GitHub buckets: >= 9.0 critical, 7.0-8.9 high, 4.0-6.9 medium, 0.1-3.9 low.
|
|
32
|
+
_SECURITY_SEVERITY = {
|
|
33
|
+
Severity.CRITICAL: "9.5",
|
|
34
|
+
Severity.HIGH: "8.0",
|
|
35
|
+
Severity.MEDIUM: "5.5",
|
|
36
|
+
Severity.LOW: "3.0",
|
|
37
|
+
Severity.INFO: "0.0",
|
|
38
|
+
}
|
|
39
|
+
_WINDOWS_DRIVE = re.compile(r"^[A-Za-z]:")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def safe_uri(path: str | None) -> str | None:
|
|
43
|
+
"""A repository-relative URI reference, or None if the path is not safely relative."""
|
|
44
|
+
if not path:
|
|
45
|
+
return None
|
|
46
|
+
normalized = path.replace("\\", "/")
|
|
47
|
+
if normalized.startswith("/") or _WINDOWS_DRIVE.match(normalized) or "://" in normalized:
|
|
48
|
+
return None
|
|
49
|
+
segments = normalized.split("/")
|
|
50
|
+
if any(seg in ("", ".", "..") for seg in segments):
|
|
51
|
+
return None
|
|
52
|
+
return quote(normalized, safe="/-._~")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _location(loc: ServerLocation | None, path: str, line: int | None) -> dict[str, Any] | None:
|
|
56
|
+
uri = safe_uri(loc.path if loc else path)
|
|
57
|
+
if uri is None:
|
|
58
|
+
return None
|
|
59
|
+
physical: dict[str, Any] = {"artifactLocation": {"uri": uri, "uriBaseId": "%SRCROOT%"}}
|
|
60
|
+
start = loc.line if loc else line
|
|
61
|
+
if start and start > 0:
|
|
62
|
+
physical["region"] = {"startLine": start}
|
|
63
|
+
return {"physicalLocation": physical}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _rule(rule: RuleInfo) -> dict[str, Any]:
|
|
67
|
+
message = clean(rule.message, 1000)
|
|
68
|
+
refs = ", ".join(clean(m, 64) for m in rule.maps_to)
|
|
69
|
+
help_text = message + (f" Maps to: {refs}." if refs else "")
|
|
70
|
+
return {
|
|
71
|
+
"id": rule.id,
|
|
72
|
+
"name": rule.id.replace("-", ""),
|
|
73
|
+
"shortDescription": {"text": clean(rule.message.split(". ")[0], 200)},
|
|
74
|
+
"fullDescription": {"text": message},
|
|
75
|
+
"help": {"text": help_text},
|
|
76
|
+
"helpUri": f"{INFORMATION_URI}/blob/main/docs/threat-model.md",
|
|
77
|
+
"defaultConfiguration": {"level": _LEVEL[rule.severity]},
|
|
78
|
+
"properties": {
|
|
79
|
+
"tags": ["security", "mcp", *(clean(m, 64) for m in rule.maps_to)],
|
|
80
|
+
"precision": "high",
|
|
81
|
+
"problem.severity": "error" if _LEVEL[rule.severity] == "error" else "warning",
|
|
82
|
+
"security-severity": _SECURITY_SEVERITY[rule.severity],
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _message(finding: Finding) -> str:
|
|
88
|
+
parts = [clean(finding.message, 600), f"Agent: {clean(finding.agent_id, 200)}."]
|
|
89
|
+
if finding.tools:
|
|
90
|
+
parts.append(f"Tools: {clean(', '.join(finding.tools), 400)}.")
|
|
91
|
+
evidence = [
|
|
92
|
+
f"{clean(ev.tool or ev.server, 80)} [{clean(k, 40)}]: {clean(v, 160)}"
|
|
93
|
+
for ev in finding.evidence
|
|
94
|
+
for k, v in ev.phrases.items()
|
|
95
|
+
]
|
|
96
|
+
if evidence:
|
|
97
|
+
parts.append(
|
|
98
|
+
"Evidence: " + "; ".join(evidence[:8]) + ("; ..." if len(evidence) > 8 else "")
|
|
99
|
+
)
|
|
100
|
+
return " ".join(parts)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _result(finding: Finding, rule_index: int) -> dict[str, Any] | None:
|
|
104
|
+
first = finding.server_locations[0] if finding.server_locations else None
|
|
105
|
+
path = finding.location.path if finding.location else finding.config_path
|
|
106
|
+
line = finding.location.line if finding.location else None
|
|
107
|
+
primary = _location(first, path, line)
|
|
108
|
+
if primary is None:
|
|
109
|
+
return None
|
|
110
|
+
related = []
|
|
111
|
+
for index, loc in enumerate(finding.server_locations[1:], start=1):
|
|
112
|
+
if (location := _location(loc, loc.path, loc.line)) is not None:
|
|
113
|
+
location["id"] = index
|
|
114
|
+
location["message"] = {"text": f"server {clean(loc.server, 100)}"}
|
|
115
|
+
related.append(location)
|
|
116
|
+
result: dict[str, Any] = {
|
|
117
|
+
"ruleId": finding.rule_id,
|
|
118
|
+
"ruleIndex": rule_index,
|
|
119
|
+
"level": _LEVEL[finding.severity],
|
|
120
|
+
"message": {"text": _message(finding)},
|
|
121
|
+
"locations": [primary],
|
|
122
|
+
"partialFingerprints": {FINGERPRINT_KEY: finding.fingerprint},
|
|
123
|
+
"properties": {
|
|
124
|
+
"severity": finding.severity.value,
|
|
125
|
+
"agent": clean(finding.agent_id, 200),
|
|
126
|
+
"servers": [clean(s, 100) for s in finding.servers],
|
|
127
|
+
"tools": [clean(t, 200) for t in finding.tools],
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
if related:
|
|
131
|
+
result["relatedLocations"] = related
|
|
132
|
+
return result
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def render(evaluation: Evaluation) -> dict[str, Any]:
|
|
136
|
+
rules = [r for r in evaluation.rules if r.enabled]
|
|
137
|
+
rule_index = {r.id: i for i, r in enumerate(rules)}
|
|
138
|
+
results: list[dict[str, Any]] = []
|
|
139
|
+
omitted = 0
|
|
140
|
+
for finding in evaluation.findings:
|
|
141
|
+
result = _result(finding, rule_index[finding.rule_id])
|
|
142
|
+
if result is None:
|
|
143
|
+
omitted += 1
|
|
144
|
+
else:
|
|
145
|
+
results.append(result)
|
|
146
|
+
|
|
147
|
+
notifications: list[dict[str, Any]] = []
|
|
148
|
+
if evaluation.not_assessed:
|
|
149
|
+
notifications.append(
|
|
150
|
+
{
|
|
151
|
+
"level": "warning",
|
|
152
|
+
"descriptor": {"id": "not-assessed"},
|
|
153
|
+
"message": {
|
|
154
|
+
"text": f"{len(evaluation.not_assessed)} server(s) or tool(s) were not "
|
|
155
|
+
"assessed (not enumerated or not classified); findings may be incomplete."
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
for skipped in evaluation.skipped_rules:
|
|
160
|
+
notifications.append(
|
|
161
|
+
{
|
|
162
|
+
"level": "note",
|
|
163
|
+
"descriptor": {"id": "rule-skipped"},
|
|
164
|
+
"message": {
|
|
165
|
+
"text": f"Rule {skipped.rule_id} skipped: {clean(skipped.reason, 200)}"
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
if omitted:
|
|
170
|
+
notifications.append(
|
|
171
|
+
{
|
|
172
|
+
"level": "warning",
|
|
173
|
+
"descriptor": {"id": "result-omitted"},
|
|
174
|
+
"message": {
|
|
175
|
+
"text": f"{omitted} finding(s) omitted from SARIF because their config "
|
|
176
|
+
"file is outside the repository; see the JSON or Markdown report."
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
"$schema": SARIF_SCHEMA,
|
|
183
|
+
"version": "2.1.0",
|
|
184
|
+
"runs": [
|
|
185
|
+
{
|
|
186
|
+
"tool": {
|
|
187
|
+
"driver": {
|
|
188
|
+
"name": "agent-capmap",
|
|
189
|
+
"version": __version__,
|
|
190
|
+
"semanticVersion": __version__,
|
|
191
|
+
"informationUri": INFORMATION_URI,
|
|
192
|
+
"rules": [_rule(r) for r in rules],
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
"invocations": [
|
|
196
|
+
{
|
|
197
|
+
"executionSuccessful": True,
|
|
198
|
+
"toolExecutionNotifications": notifications,
|
|
199
|
+
}
|
|
200
|
+
],
|
|
201
|
+
"results": results,
|
|
202
|
+
"properties": {
|
|
203
|
+
"policy": evaluation.policy.model_dump(mode="json"),
|
|
204
|
+
"summary": evaluation.summary,
|
|
205
|
+
"baselineUsed": evaluation.baseline_used,
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
],
|
|
209
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/NKwolv/agent-capmap/schemas/policy.schema.json",
|
|
4
|
+
"title": "agent-capmap policy",
|
|
5
|
+
"description": "Deterministic rules evaluated per agent over the union of its tools. Policy format version 1, taxonomy version 1.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["version", "taxonomy_version", "rules"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"version": { "const": 1 },
|
|
11
|
+
"taxonomy_version": { "const": "1" },
|
|
12
|
+
"trusted_sources": {
|
|
13
|
+
"description": "Vetted server sources. Glob patterns over source ids: npm:<pkg>, pypi:<pkg>, docker:<image>, url:<host>, local:<path>. When set, servers matching none are 'untrusted'.",
|
|
14
|
+
"type": "array",
|
|
15
|
+
"maxItems": 1000,
|
|
16
|
+
"items": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"minLength": 5,
|
|
19
|
+
"maxLength": 256,
|
|
20
|
+
"pattern": "^(npm|pypi|docker|url|local):[^\\s]+$"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"rules": {
|
|
24
|
+
"type": "array",
|
|
25
|
+
"minItems": 1,
|
|
26
|
+
"maxItems": 500,
|
|
27
|
+
"items": { "$ref": "#/$defs/rule" }
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"$defs": {
|
|
31
|
+
"rule": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"additionalProperties": false,
|
|
34
|
+
"required": ["id", "severity", "message"],
|
|
35
|
+
"properties": {
|
|
36
|
+
"id": { "type": "string", "pattern": "^[A-Z][A-Z0-9-]{1,31}$" },
|
|
37
|
+
"severity": { "enum": ["critical", "high", "medium", "low", "info"] },
|
|
38
|
+
"message": { "type": "string", "minLength": 1, "maxLength": 500 },
|
|
39
|
+
"enabled": { "type": "boolean" },
|
|
40
|
+
"maps_to": {
|
|
41
|
+
"type": "array",
|
|
42
|
+
"maxItems": 20,
|
|
43
|
+
"items": { "type": "string", "pattern": "^[A-Za-z0-9.:_-]{1,64}$" }
|
|
44
|
+
},
|
|
45
|
+
"when_agent_has_all": { "$ref": "#/$defs/tool_predicates" },
|
|
46
|
+
"when_agent_has_any": { "$ref": "#/$defs/tool_predicates" },
|
|
47
|
+
"when_server": {
|
|
48
|
+
"oneOf": [
|
|
49
|
+
{ "$ref": "#/$defs/server_predicate" },
|
|
50
|
+
{
|
|
51
|
+
"type": "array",
|
|
52
|
+
"minItems": 1,
|
|
53
|
+
"maxItems": 20,
|
|
54
|
+
"items": { "$ref": "#/$defs/server_predicate" }
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
"when_hash_changed": { "const": true },
|
|
59
|
+
"when_tool_name_shadowed": { "const": true }
|
|
60
|
+
},
|
|
61
|
+
"anyOf": [
|
|
62
|
+
{ "required": ["when_agent_has_all"] },
|
|
63
|
+
{ "required": ["when_agent_has_any"] },
|
|
64
|
+
{ "required": ["when_server"] },
|
|
65
|
+
{ "required": ["when_hash_changed"] },
|
|
66
|
+
{ "required": ["when_tool_name_shadowed"] }
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
"tool_predicates": {
|
|
70
|
+
"type": "array",
|
|
71
|
+
"minItems": 1,
|
|
72
|
+
"maxItems": 20,
|
|
73
|
+
"items": { "$ref": "#/$defs/tool_predicate" }
|
|
74
|
+
},
|
|
75
|
+
"tool_predicate": {
|
|
76
|
+
"description": "All fields must match on the same tool. A list means any of the values.",
|
|
77
|
+
"type": "object",
|
|
78
|
+
"additionalProperties": false,
|
|
79
|
+
"minProperties": 1,
|
|
80
|
+
"properties": {
|
|
81
|
+
"data_access": { "$ref": "#/$defs/data_access" },
|
|
82
|
+
"untrusted_input": { "$ref": "#/$defs/boolean_or_list" },
|
|
83
|
+
"egress": { "$ref": "#/$defs/egress" },
|
|
84
|
+
"mutation": { "$ref": "#/$defs/mutation" },
|
|
85
|
+
"execution": { "$ref": "#/$defs/execution" },
|
|
86
|
+
"privilege": { "$ref": "#/$defs/privilege" },
|
|
87
|
+
"suspicious_description": { "type": "boolean" },
|
|
88
|
+
"any_capability": { "type": "boolean" },
|
|
89
|
+
"approval_gate": { "$ref": "#/$defs/approval_gate" }
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"server_predicate": {
|
|
93
|
+
"description": "All fields must match on the same server. A list means any of the values.",
|
|
94
|
+
"type": "object",
|
|
95
|
+
"additionalProperties": false,
|
|
96
|
+
"minProperties": 1,
|
|
97
|
+
"properties": {
|
|
98
|
+
"auth": { "$ref": "#/$defs/auth" },
|
|
99
|
+
"transport": { "$ref": "#/$defs/transport" },
|
|
100
|
+
"remote": { "type": "boolean" },
|
|
101
|
+
"version_pinned": { "$ref": "#/$defs/version_pinned" },
|
|
102
|
+
"source_trust": { "$ref": "#/$defs/source_trust" }
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"boolean_or_list": {
|
|
106
|
+
"oneOf": [
|
|
107
|
+
{ "type": "boolean" },
|
|
108
|
+
{ "type": "array", "minItems": 1, "maxItems": 2, "uniqueItems": true, "items": { "type": "boolean" } }
|
|
109
|
+
]
|
|
110
|
+
},
|
|
111
|
+
"data_access": {
|
|
112
|
+
"oneOf": [
|
|
113
|
+
{ "enum": ["none", "private_read", "secrets_read"] },
|
|
114
|
+
{ "type": "array", "minItems": 1, "uniqueItems": true, "items": { "enum": ["none", "private_read", "secrets_read"] } }
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
"egress": {
|
|
118
|
+
"oneOf": [
|
|
119
|
+
{ "enum": ["none", "internal", "external"] },
|
|
120
|
+
{ "type": "array", "minItems": 1, "uniqueItems": true, "items": { "enum": ["none", "internal", "external"] } }
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
"mutation": {
|
|
124
|
+
"oneOf": [
|
|
125
|
+
{ "enum": ["none", "reversible_write", "destructive"] },
|
|
126
|
+
{ "type": "array", "minItems": 1, "uniqueItems": true, "items": { "enum": ["none", "reversible_write", "destructive"] } }
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
"execution": {
|
|
130
|
+
"oneOf": [
|
|
131
|
+
{ "enum": ["none", "sandboxed", "host"] },
|
|
132
|
+
{ "type": "array", "minItems": 1, "uniqueItems": true, "items": { "enum": ["none", "sandboxed", "host"] } }
|
|
133
|
+
]
|
|
134
|
+
},
|
|
135
|
+
"privilege": {
|
|
136
|
+
"oneOf": [
|
|
137
|
+
{ "enum": ["none", "identity_admin"] },
|
|
138
|
+
{ "type": "array", "minItems": 1, "uniqueItems": true, "items": { "enum": ["none", "identity_admin"] } }
|
|
139
|
+
]
|
|
140
|
+
},
|
|
141
|
+
"approval_gate": {
|
|
142
|
+
"oneOf": [
|
|
143
|
+
{ "enum": ["required", "none", "unknown"] },
|
|
144
|
+
{ "type": "array", "minItems": 1, "uniqueItems": true, "items": { "enum": ["required", "none", "unknown"] } }
|
|
145
|
+
]
|
|
146
|
+
},
|
|
147
|
+
"auth": {
|
|
148
|
+
"oneOf": [
|
|
149
|
+
{ "enum": ["none", "static", "oauth", "unknown"] },
|
|
150
|
+
{ "type": "array", "minItems": 1, "uniqueItems": true, "items": { "enum": ["none", "static", "oauth", "unknown"] } }
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
"transport": {
|
|
154
|
+
"oneOf": [
|
|
155
|
+
{ "enum": ["stdio", "http", "sse", "ws"] },
|
|
156
|
+
{ "type": "array", "minItems": 1, "uniqueItems": true, "items": { "enum": ["stdio", "http", "sse", "ws"] } }
|
|
157
|
+
]
|
|
158
|
+
},
|
|
159
|
+
"version_pinned": {
|
|
160
|
+
"oneOf": [
|
|
161
|
+
{ "enum": ["pinned", "unpinned", "local", "not_applicable", "unknown"] },
|
|
162
|
+
{ "type": "array", "minItems": 1, "uniqueItems": true, "items": { "enum": ["pinned", "unpinned", "local", "not_applicable", "unknown"] } }
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
"source_trust": {
|
|
166
|
+
"oneOf": [
|
|
167
|
+
{ "enum": ["trusted", "untrusted", "unknown"] },
|
|
168
|
+
{ "type": "array", "minItems": 1, "uniqueItems": true, "items": { "enum": ["trusted", "untrusted", "unknown"] } }
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/NKwolv/agent-capmap/schemas/tool-tags.schema.json",
|
|
4
|
+
"title": "agent-capmap tool classification",
|
|
5
|
+
"description": "Classifier output for one MCP tool. taxonomy_version 1.",
|
|
6
|
+
"x-taxonomy-version": "1",
|
|
7
|
+
"type": "object",
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"required": ["tool", "tags", "confidence", "evidence", "suspicious_description"],
|
|
10
|
+
"properties": {
|
|
11
|
+
"tool": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"description": "Exact tool name being classified."
|
|
14
|
+
},
|
|
15
|
+
"tags": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"additionalProperties": false,
|
|
18
|
+
"required": ["data_access", "untrusted_input", "egress", "mutation", "execution", "privilege"],
|
|
19
|
+
"properties": {
|
|
20
|
+
"data_access": { "type": "string", "enum": ["none", "private_read", "secrets_read"] },
|
|
21
|
+
"untrusted_input": { "type": "boolean" },
|
|
22
|
+
"egress": { "type": "string", "enum": ["none", "internal", "external"] },
|
|
23
|
+
"mutation": { "type": "string", "enum": ["none", "reversible_write", "destructive"] },
|
|
24
|
+
"execution": { "type": "string", "enum": ["none", "sandboxed", "host"] },
|
|
25
|
+
"privilege": { "type": "string", "enum": ["none", "identity_admin"] }
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"confidence": {
|
|
29
|
+
"type": "number",
|
|
30
|
+
"minimum": 0,
|
|
31
|
+
"maximum": 1,
|
|
32
|
+
"description": "Overall confidence in the tags, 0 to 1."
|
|
33
|
+
},
|
|
34
|
+
"evidence": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"additionalProperties": false,
|
|
37
|
+
"description": "Per-axis phrase from the tool's description or input schema justifying the tag. Required for every non-default tag.",
|
|
38
|
+
"properties": {
|
|
39
|
+
"data_access": { "type": "string" },
|
|
40
|
+
"untrusted_input": { "type": "string" },
|
|
41
|
+
"egress": { "type": "string" },
|
|
42
|
+
"mutation": { "type": "string" },
|
|
43
|
+
"execution": { "type": "string" },
|
|
44
|
+
"privilege": { "type": "string" }
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"suspicious_description": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"additionalProperties": false,
|
|
50
|
+
"required": ["flag", "reason"],
|
|
51
|
+
"properties": {
|
|
52
|
+
"flag": { "type": "boolean" },
|
|
53
|
+
"reason": { "type": "string" }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
agentcapmap/taxonomy.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Capability taxonomy v1: the six per-tool axes and their default (no-capability) values.
|
|
2
|
+
|
|
3
|
+
The JSON schema in `schemas/tool-tags.schema.json` is the source of truth for the
|
|
4
|
+
allowed values; this module mirrors it for typed access and loads the schema file.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from functools import cache
|
|
11
|
+
from importlib import resources
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Final
|
|
14
|
+
|
|
15
|
+
TAXONOMY_VERSION: Final = "1"
|
|
16
|
+
|
|
17
|
+
AXES: Final = ("data_access", "untrusted_input", "egress", "mutation", "execution", "privilege")
|
|
18
|
+
|
|
19
|
+
# The value meaning "this tool has no capability on this axis".
|
|
20
|
+
DEFAULTS: Final[dict[str, str | bool]] = {
|
|
21
|
+
"data_access": "none",
|
|
22
|
+
"untrusted_input": False,
|
|
23
|
+
"egress": "none",
|
|
24
|
+
"mutation": "none",
|
|
25
|
+
"execution": "none",
|
|
26
|
+
"privilege": "none",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_SCHEMA_FILE = "tool-tags.schema.json"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def data_file(directory: str, name: str) -> str:
|
|
33
|
+
"""Read a bundled data file (schemas/, policies/): from the wheel, else the checkout."""
|
|
34
|
+
packaged = resources.files("agentcapmap").joinpath(directory, name)
|
|
35
|
+
if packaged.is_file():
|
|
36
|
+
return packaged.read_text(encoding="utf-8")
|
|
37
|
+
repo_copy = Path(__file__).resolve().parents[2] / directory / name
|
|
38
|
+
return repo_copy.read_text(encoding="utf-8")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@cache
|
|
42
|
+
def tool_tags_schema() -> dict[str, Any]:
|
|
43
|
+
schema: dict[str, Any] = json.loads(data_file("schemas", _SCHEMA_FILE))
|
|
44
|
+
if schema.get("x-taxonomy-version") != TAXONOMY_VERSION:
|
|
45
|
+
raise RuntimeError(
|
|
46
|
+
f"{_SCHEMA_FILE} is taxonomy version {schema.get('x-taxonomy-version')!r}, "
|
|
47
|
+
f"code expects {TAXONOMY_VERSION!r}"
|
|
48
|
+
)
|
|
49
|
+
return schema
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def axis_values(axis: str) -> list[str | bool]:
|
|
53
|
+
spec = tool_tags_schema()["properties"]["tags"]["properties"][axis]
|
|
54
|
+
if spec["type"] == "boolean":
|
|
55
|
+
return [False, True]
|
|
56
|
+
return list(spec["enum"])
|