trustdex 0.9.0 → 0.10.0
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.
- package/README.ko.md +34 -5
- package/README.md +36 -5
- package/bin/trustdex.mjs +55 -0
- package/docs/ARCHITECTURE.ko.md +8 -0
- package/docs/ARCHITECTURE.md +8 -0
- package/docs/CLAUDE.ko.md +11 -1
- package/docs/CLAUDE.md +11 -1
- package/docs/CODEX.ko.md +13 -4
- package/docs/CODEX.md +14 -4
- package/docs/DEFENSIVE_USE_CASE.ko.md +6 -0
- package/docs/DEFENSIVE_USE_CASE.md +7 -0
- package/docs/EVALUATION.ko.md +18 -0
- package/docs/EVALUATION.md +18 -0
- package/docs/ROADMAP.ko.md +6 -4
- package/docs/ROADMAP.md +6 -4
- package/docs/THREAT_MODEL.ko.md +4 -0
- package/docs/THREAT_MODEL.md +4 -0
- package/examples/adversarial/README.ko.md +1 -1
- package/examples/adversarial/README.md +1 -1
- package/examples/adversarial/bindings/README.ko.md +8 -0
- package/examples/adversarial/bindings/README.md +8 -0
- package/examples/adversarial/bindings/expected.json +12 -0
- package/examples/adversarial/bindings/runtime-after.json +21 -0
- package/examples/adversarial/bindings/runtime-before.json +13 -0
- package/package.json +3 -3
- package/schemas/effective-runtime-tools-diff-v1.schema.json +50 -0
- package/schemas/effective-runtime-tools-v1.schema.json +68 -0
- package/schemas/runtime-tool-inventory-v1.schema.json +27 -0
- package/src/bindings.mjs +4 -0
- package/src/runtime-evidence.mjs +318 -0
package/docs/THREAT_MODEL.md
CHANGED
|
@@ -26,6 +26,8 @@ TrustDex addresses one narrow question:
|
|
|
26
26
|
- precedence changes across explicitly supplied Claude or Codex settings sources
|
|
27
27
|
- automatic MCP tool grants that no longer match the server's policy/provenance result
|
|
28
28
|
- wildcard, unmatched, or ambiguous grants that hide the effective tool boundary
|
|
29
|
+
- explicitly reported runtime inventories that expose blocked, review-required, or unconfigured MCP servers
|
|
30
|
+
- effective runtime inventories expanding by server visibility or tool count
|
|
29
31
|
|
|
30
32
|
## Trust boundaries
|
|
31
33
|
|
|
@@ -54,6 +56,8 @@ TrustDex does **not** treat a familiar project name, star count, package name, o
|
|
|
54
56
|
- authenticating the author of an unsigned permission posture snapshot
|
|
55
57
|
- proving that a binding snapshot reflects the files actually loaded by a running agent
|
|
56
58
|
- intercepting or enforcing individual MCP tool calls at runtime
|
|
59
|
+
- authenticating an inventory producer, proving inventory completeness, or preventing a compromised adapter from lying
|
|
60
|
+
- discovering live Claude/Codex sessions or reading their private state automatically
|
|
57
61
|
|
|
58
62
|
## Important failure mode
|
|
59
63
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
- `expected.json`은 필수 결정과 최소 신호를 기록합니다.
|
|
12
12
|
- `drift-before.json`과 `drift-after.json`은 검토된 패키지에 비밀 정보를 포함하는 환경 키가 추가되는 상황을 모델링합니다.
|
|
13
13
|
- [`permissions/`](permissions/README.ko.md)는 값이 제거된 Claude/Codex 권한 상태 드리프트를 다룹니다.
|
|
14
|
-
- [`bindings/`](bindings/README.ko.md)는 MCP 출처와 자동
|
|
14
|
+
- [`bindings/`](bindings/README.ko.md)는 MCP 출처와 자동 권한 연결 및 명시적 런타임 인벤토리 대조를 다룹니다.
|
|
15
15
|
|
|
16
16
|
모든 패키지 이름은 가상이며, 모든 원격 엔드포인트는 예약된 `.invalid` 최상위 도메인을 사용하고, 자격 증명으로 보이는 모든 값은 자리표시자입니다. 이를 실제 값으로 바꾸지 마세요. TrustDex는 이 파일을 읽지만 나열된 명령을 실행하거나 나열된 엔드포인트에 연결하지 않습니다.
|
|
17
17
|
|
|
@@ -11,7 +11,7 @@ These files provide a reproducible defensive test of TrustDex's policy boundary.
|
|
|
11
11
|
- `expected.json` records the required decisions and minimum signals.
|
|
12
12
|
- `drift-before.json` and `drift-after.json` model a reviewed package gaining a secret-bearing environment key.
|
|
13
13
|
- [`permissions/`](permissions/README.md) covers value-free Claude/Codex permission posture drift.
|
|
14
|
-
- [`bindings/`](bindings/README.md) covers MCP provenance-to-automatic-grant bindings.
|
|
14
|
+
- [`bindings/`](bindings/README.md) covers MCP provenance-to-automatic-grant bindings and explicit runtime inventory reconciliation.
|
|
15
15
|
|
|
16
16
|
All package names are fictional, all remote endpoints use the reserved `.invalid` top-level domain, and all apparent credentials are placeholders. Do not replace them with real values. TrustDex reads these files but never executes the listed commands or contacts the listed endpoints.
|
|
17
17
|
|
|
@@ -13,6 +13,10 @@ node ../../../bin/trustdex.mjs snapshot-claude-bindings ./claude.mcp.json \
|
|
|
13
13
|
node ../../../bin/trustdex.mjs snapshot-codex-bindings ./codex.toml \
|
|
14
14
|
--trust-store ./trust-store.json \
|
|
15
15
|
--out ./codex.bindings.json
|
|
16
|
+
|
|
17
|
+
node ../../../bin/trustdex.mjs snapshot-runtime-tools \
|
|
18
|
+
./claude.bindings.json ./runtime-after.json \
|
|
19
|
+
--out ./runtime.evidence.json
|
|
16
20
|
```
|
|
17
21
|
|
|
18
22
|
검토된 Claude 서버의 범위 제한 승인은 `ALLOW` 결과와 결합됩니다. 출처를 알 수
|
|
@@ -22,3 +26,7 @@ node ../../../bin/trustdex.mjs snapshot-codex-bindings ./codex.toml \
|
|
|
22
26
|
|
|
23
27
|
생성된 보고서는 엔드포인트 경로, 권한 지정자, 도구 이름 및 마커 값을 의도적으로
|
|
24
28
|
제외합니다. `expected.json`은 안정적인 구조적 결과만 기록합니다.
|
|
29
|
+
런타임 인벤토리는 명시적으로 제공되는 값 없는 어댑터 출력입니다. 도구 이름 대신
|
|
30
|
+
서버 이름과 도구 개수만 포함합니다. `runtime-after.json`은 차단된 설정 서버와 예상
|
|
31
|
+
밖 서버가 노출되는 상황을 모델링합니다. TrustDex는 런타임 상태를 자동으로 찾거나
|
|
32
|
+
인벤토리 생성자를 인증하지 않습니다.
|
|
@@ -14,6 +14,10 @@ node ../../../bin/trustdex.mjs snapshot-claude-bindings ./claude.mcp.json \
|
|
|
14
14
|
node ../../../bin/trustdex.mjs snapshot-codex-bindings ./codex.toml \
|
|
15
15
|
--trust-store ./trust-store.json \
|
|
16
16
|
--out ./codex.bindings.json
|
|
17
|
+
|
|
18
|
+
node ../../../bin/trustdex.mjs snapshot-runtime-tools \
|
|
19
|
+
./claude.bindings.json ./runtime-after.json \
|
|
20
|
+
--out ./runtime.evidence.json
|
|
17
21
|
```
|
|
18
22
|
|
|
19
23
|
The reviewed Claude server has a scoped grant bound to an `ALLOW` result. Its
|
|
@@ -23,3 +27,7 @@ review because `default_tools_approval_mode = "approve"` grants every tool.
|
|
|
23
27
|
|
|
24
28
|
Generated reports intentionally omit endpoint paths, permission specifiers, tool
|
|
25
29
|
names, and marker values. `expected.json` records only stable structural outcomes.
|
|
30
|
+
The runtime inventories are explicit, value-free adapter outputs: they contain server
|
|
31
|
+
names and tool counts, not tool names. `runtime-after.json` models a blocked configured
|
|
32
|
+
server and an unexpected server becoming visible. TrustDex does not discover runtime
|
|
33
|
+
state or authenticate the inventory producer.
|
|
@@ -11,5 +11,17 @@
|
|
|
11
11
|
"automaticGrantCount": 2,
|
|
12
12
|
"reviewCount": 1,
|
|
13
13
|
"blockedCount": 1
|
|
14
|
+
},
|
|
15
|
+
"runtimeBefore": {
|
|
16
|
+
"status": "restricted",
|
|
17
|
+
"observedServerCount": 1,
|
|
18
|
+
"matchedCount": 1,
|
|
19
|
+
"notObservedCount": 1
|
|
20
|
+
},
|
|
21
|
+
"runtimeAfter": {
|
|
22
|
+
"status": "block",
|
|
23
|
+
"observedServerCount": 3,
|
|
24
|
+
"blockedCount": 1,
|
|
25
|
+
"unexpectedCount": 1
|
|
14
26
|
}
|
|
15
27
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"kind": "agent-runtime-tool-inventory",
|
|
4
|
+
"runtime": "claude",
|
|
5
|
+
"capturedAt": "2026-10-24T00:05:00.000Z",
|
|
6
|
+
"captureMethod": "runtime-adapter",
|
|
7
|
+
"servers": [
|
|
8
|
+
{
|
|
9
|
+
"name": "reviewed",
|
|
10
|
+
"toolCount": 2
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"name": "unknown",
|
|
14
|
+
"toolCount": 1
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "unexpected",
|
|
18
|
+
"toolCount": 1
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trustdex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "A local-first trust, provenance, and review gate for AI agent tools, MCP servers, skills, and plugins.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"test": "node --test",
|
|
11
|
-
"test:research": "node --test test/research-fixtures.test.mjs test/claude-settings.test.mjs test/codex-settings.test.mjs test/posture.test.mjs test/settings-stack.test.mjs test/bindings.test.mjs",
|
|
12
|
-
"check": "node --check bin/trustdex.mjs && node --check src/bindings.mjs && node --check src/canonical.mjs && node --check src/claude-config.mjs && node --check src/claude-settings.mjs && node --check src/codex-config.mjs && node --check src/codex-settings.mjs && node --check src/extensions.mjs && node --check src/gate.mjs && node --check src/inspect.mjs && node --check src/policy.mjs && node --check src/policy-packs.mjs && node --check src/posture.mjs && node --check src/provenance.mjs && node --check src/provenance-online.mjs && node --check src/review.mjs && node --check src/snapshot.mjs && node --check src/trust-record.mjs",
|
|
11
|
+
"test:research": "node --test test/research-fixtures.test.mjs test/claude-settings.test.mjs test/codex-settings.test.mjs test/posture.test.mjs test/settings-stack.test.mjs test/bindings.test.mjs test/runtime-evidence.test.mjs",
|
|
12
|
+
"check": "node --check bin/trustdex.mjs && node --check src/bindings.mjs && node --check src/runtime-evidence.mjs && node --check src/canonical.mjs && node --check src/claude-config.mjs && node --check src/claude-settings.mjs && node --check src/codex-config.mjs && node --check src/codex-settings.mjs && node --check src/extensions.mjs && node --check src/gate.mjs && node --check src/inspect.mjs && node --check src/policy.mjs && node --check src/policy-packs.mjs && node --check src/posture.mjs && node --check src/provenance.mjs && node --check src/provenance-online.mjs && node --check src/review.mjs && node --check src/snapshot.mjs && node --check src/trust-record.mjs",
|
|
13
13
|
"prepublishOnly": "npm test && npm run check"
|
|
14
14
|
},
|
|
15
15
|
"engines": {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/grigent/trustdex/blob/main/schemas/effective-runtime-tools-diff-v1.schema.json",
|
|
4
|
+
"title": "TrustDex effective runtime tool boundary diff v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "runtime", "status", "baselineDigest", "currentDigest", "summary", "changes"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schemaVersion": { "const": 1 },
|
|
10
|
+
"runtime": { "enum": ["claude", "codex"] },
|
|
11
|
+
"status": { "enum": ["unchanged", "restricted", "needs-review"] },
|
|
12
|
+
"baselineDigest": { "$ref": "#/$defs/digest" },
|
|
13
|
+
"currentDigest": { "$ref": "#/$defs/digest" },
|
|
14
|
+
"summary": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"additionalProperties": false,
|
|
17
|
+
"required": ["expansions", "restrictions", "reviews"],
|
|
18
|
+
"properties": {
|
|
19
|
+
"expansions": { "$ref": "#/$defs/count" },
|
|
20
|
+
"restrictions": { "$ref": "#/$defs/count" },
|
|
21
|
+
"reviews": { "$ref": "#/$defs/count" }
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"changes": {
|
|
25
|
+
"type": "array",
|
|
26
|
+
"items": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"additionalProperties": false,
|
|
29
|
+
"required": ["impact", "type", "name", "field", "before", "after"],
|
|
30
|
+
"properties": {
|
|
31
|
+
"impact": { "enum": ["expansion", "restriction", "review"] },
|
|
32
|
+
"type": {
|
|
33
|
+
"enum": [
|
|
34
|
+
"bindings-changed", "capture-method-changed", "server-added", "server-removed",
|
|
35
|
+
"observation-changed", "tool-count-changed", "binding-state-changed", "server-status-changed"
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
"name": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] },
|
|
39
|
+
"field": { "type": "string", "minLength": 1 },
|
|
40
|
+
"before": {},
|
|
41
|
+
"after": {}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"$defs": {
|
|
47
|
+
"digest": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
48
|
+
"count": { "type": "integer", "minimum": 0 }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/grigent/trustdex/blob/main/schemas/effective-runtime-tools-v1.schema.json",
|
|
4
|
+
"title": "TrustDex effective runtime tool boundary v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"schemaVersion", "kind", "createdAt", "runtime", "capturedAt", "captureMethod",
|
|
9
|
+
"bindingDigest", "inventoryDigest", "status", "summary", "servers", "digest"
|
|
10
|
+
],
|
|
11
|
+
"properties": {
|
|
12
|
+
"schemaVersion": { "const": 1 },
|
|
13
|
+
"kind": { "const": "agent-effective-tool-boundary" },
|
|
14
|
+
"createdAt": { "type": "string", "format": "date-time" },
|
|
15
|
+
"runtime": { "enum": ["claude", "codex"] },
|
|
16
|
+
"capturedAt": { "type": "string", "format": "date-time" },
|
|
17
|
+
"captureMethod": { "enum": ["runtime-export", "runtime-adapter"] },
|
|
18
|
+
"bindingDigest": { "$ref": "#/$defs/digest" },
|
|
19
|
+
"inventoryDigest": { "$ref": "#/$defs/digest" },
|
|
20
|
+
"status": { "enum": ["matched", "restricted", "needs-review", "block"] },
|
|
21
|
+
"summary": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"additionalProperties": false,
|
|
24
|
+
"required": [
|
|
25
|
+
"configuredServerCount", "observedServerCount", "observedToolCount", "matchedCount",
|
|
26
|
+
"reviewCount", "blockedCount", "unexpectedCount", "notObservedCount"
|
|
27
|
+
],
|
|
28
|
+
"properties": {
|
|
29
|
+
"configuredServerCount": { "$ref": "#/$defs/count" },
|
|
30
|
+
"observedServerCount": { "$ref": "#/$defs/count" },
|
|
31
|
+
"observedToolCount": { "$ref": "#/$defs/count" },
|
|
32
|
+
"matchedCount": { "$ref": "#/$defs/count" },
|
|
33
|
+
"reviewCount": { "$ref": "#/$defs/count" },
|
|
34
|
+
"blockedCount": { "$ref": "#/$defs/count" },
|
|
35
|
+
"unexpectedCount": { "$ref": "#/$defs/count" },
|
|
36
|
+
"notObservedCount": { "$ref": "#/$defs/count" }
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"servers": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"items": { "$ref": "#/$defs/server" }
|
|
42
|
+
},
|
|
43
|
+
"digest": { "$ref": "#/$defs/digest" }
|
|
44
|
+
},
|
|
45
|
+
"$defs": {
|
|
46
|
+
"digest": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
47
|
+
"count": { "type": "integer", "minimum": 0 },
|
|
48
|
+
"server": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"additionalProperties": false,
|
|
51
|
+
"required": [
|
|
52
|
+
"name", "configured", "observed", "toolCount", "sourceDecision", "provenanceStatus",
|
|
53
|
+
"automaticGrant", "bindingStatus", "status"
|
|
54
|
+
],
|
|
55
|
+
"properties": {
|
|
56
|
+
"name": { "type": "string", "minLength": 1 },
|
|
57
|
+
"configured": { "type": "boolean" },
|
|
58
|
+
"observed": { "type": "boolean" },
|
|
59
|
+
"toolCount": { "oneOf": [{ "$ref": "#/$defs/count" }, { "type": "null" }] },
|
|
60
|
+
"sourceDecision": { "oneOf": [{ "enum": ["allow", "ask", "block"] }, { "type": "null" }] },
|
|
61
|
+
"provenanceStatus": { "oneOf": [{ "enum": ["unknown", "verified", "mismatch"] }, { "type": "null" }] },
|
|
62
|
+
"automaticGrant": { "oneOf": [{ "enum": ["none", "scoped", "all"] }, { "type": "null" }] },
|
|
63
|
+
"bindingStatus": { "oneOf": [{ "enum": ["not-granted", "bound", "needs-review", "blocked"] }, { "type": "null" }] },
|
|
64
|
+
"status": { "enum": ["matched", "not-observed", "needs-review", "blocked", "unexpected"] }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/grigent/trustdex/blob/main/schemas/runtime-tool-inventory-v1.schema.json",
|
|
4
|
+
"title": "TrustDex value-free runtime tool inventory v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "kind", "runtime", "capturedAt", "captureMethod", "servers"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schemaVersion": { "const": 1 },
|
|
10
|
+
"kind": { "const": "agent-runtime-tool-inventory" },
|
|
11
|
+
"runtime": { "enum": ["claude", "codex"] },
|
|
12
|
+
"capturedAt": { "type": "string", "format": "date-time" },
|
|
13
|
+
"captureMethod": { "enum": ["runtime-export", "runtime-adapter"] },
|
|
14
|
+
"servers": {
|
|
15
|
+
"type": "array",
|
|
16
|
+
"items": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"additionalProperties": false,
|
|
19
|
+
"required": ["name", "toolCount"],
|
|
20
|
+
"properties": {
|
|
21
|
+
"name": { "type": "string", "minLength": 1 },
|
|
22
|
+
"toolCount": { "type": "integer", "minimum": 0 }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/bindings.mjs
CHANGED
|
@@ -214,6 +214,10 @@ function validateSnapshot(snapshot) {
|
|
|
214
214
|
return snapshot;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
export function validateToolPermissionBindingSnapshot(snapshot) {
|
|
218
|
+
return validateSnapshot(snapshot);
|
|
219
|
+
}
|
|
220
|
+
|
|
217
221
|
function createSummary(bindings, input) {
|
|
218
222
|
return {
|
|
219
223
|
serverCount: bindings.length,
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { sha256Json } from './canonical.mjs';
|
|
2
|
+
import { validateToolPermissionBindingSnapshot } from './bindings.mjs';
|
|
3
|
+
|
|
4
|
+
const RUNTIMES = new Set(['claude', 'codex']);
|
|
5
|
+
const CAPTURE_METHODS = new Set(['runtime-export', 'runtime-adapter']);
|
|
6
|
+
const EVIDENCE_STATUSES = new Set(['matched', 'restricted', 'needs-review', 'block']);
|
|
7
|
+
const SERVER_STATUSES = new Set(['matched', 'not-observed', 'needs-review', 'blocked', 'unexpected']);
|
|
8
|
+
const KIND = 'agent-effective-tool-boundary';
|
|
9
|
+
const INVENTORY_KIND = 'agent-runtime-tool-inventory';
|
|
10
|
+
|
|
11
|
+
function requireObject(value, message) {
|
|
12
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(message);
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function requireExactKeys(value, expected, message) {
|
|
17
|
+
const keys = Object.keys(requireObject(value, message));
|
|
18
|
+
if (keys.length !== expected.size || keys.some((key) => !expected.has(key))) throw new Error(message);
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function requireCount(value, field) {
|
|
23
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new Error(`Invalid runtime evidence count: ${field}`);
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function requireDigest(value, field) {
|
|
28
|
+
if (typeof value !== 'string' || !/^[a-f0-9]{64}$/.test(value)) {
|
|
29
|
+
throw new Error(`Invalid runtime evidence digest: ${field}`);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function requireTimestamp(value, field) {
|
|
35
|
+
if (typeof value !== 'string' || Number.isNaN(Date.parse(value)) ||
|
|
36
|
+
new Date(value).toISOString() !== value) {
|
|
37
|
+
throw new Error(`Invalid runtime evidence timestamp: ${field}`);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeInventory(input) {
|
|
43
|
+
const inventory = requireExactKeys(input, new Set([
|
|
44
|
+
'schemaVersion', 'kind', 'runtime', 'capturedAt', 'captureMethod', 'servers'
|
|
45
|
+
]), 'Invalid runtime tool inventory.');
|
|
46
|
+
if (inventory.schemaVersion !== 1 || inventory.kind !== INVENTORY_KIND) {
|
|
47
|
+
throw new Error('Unsupported runtime tool inventory schema.');
|
|
48
|
+
}
|
|
49
|
+
if (!RUNTIMES.has(inventory.runtime)) throw new Error('Invalid runtime tool inventory runtime.');
|
|
50
|
+
requireTimestamp(inventory.capturedAt, 'capturedAt');
|
|
51
|
+
if (!CAPTURE_METHODS.has(inventory.captureMethod)) throw new Error('Invalid runtime tool inventory capture method.');
|
|
52
|
+
if (!Array.isArray(inventory.servers)) throw new Error('Runtime tool inventory is missing servers.');
|
|
53
|
+
|
|
54
|
+
const servers = inventory.servers.map((server) => {
|
|
55
|
+
requireExactKeys(server, new Set(['name', 'toolCount']), 'Invalid runtime tool inventory server.');
|
|
56
|
+
if (typeof server.name !== 'string' || !server.name.trim()) {
|
|
57
|
+
throw new Error('Runtime tool inventory server is missing a name.');
|
|
58
|
+
}
|
|
59
|
+
return { name: server.name, toolCount: requireCount(server.toolCount, 'toolCount') };
|
|
60
|
+
}).sort((left, right) => left.name.localeCompare(right.name));
|
|
61
|
+
if (new Set(servers.map((server) => server.name)).size !== servers.length) {
|
|
62
|
+
throw new Error('Runtime tool inventory server names must be unique.');
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
schemaVersion: 1,
|
|
66
|
+
kind: INVENTORY_KIND,
|
|
67
|
+
runtime: inventory.runtime,
|
|
68
|
+
capturedAt: inventory.capturedAt,
|
|
69
|
+
captureMethod: inventory.captureMethod,
|
|
70
|
+
servers
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function serverStatus(binding, observed) {
|
|
75
|
+
if (!binding) return 'unexpected';
|
|
76
|
+
if (!observed) return 'not-observed';
|
|
77
|
+
if (binding.status === 'blocked' || binding.sourceDecision === 'block' ||
|
|
78
|
+
binding.provenanceStatus === 'mismatch') return 'blocked';
|
|
79
|
+
if (binding.status === 'needs-review' || binding.sourceDecision === 'ask') return 'needs-review';
|
|
80
|
+
return 'matched';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function createSummary(servers) {
|
|
84
|
+
const observedToolCount = servers.reduce((total, server) => total + (server.toolCount || 0), 0);
|
|
85
|
+
requireCount(observedToolCount, 'observedToolCount');
|
|
86
|
+
return {
|
|
87
|
+
configuredServerCount: servers.filter((server) => server.configured).length,
|
|
88
|
+
observedServerCount: servers.filter((server) => server.observed).length,
|
|
89
|
+
observedToolCount,
|
|
90
|
+
matchedCount: servers.filter((server) => server.status === 'matched').length,
|
|
91
|
+
reviewCount: servers.filter((server) => server.status === 'needs-review').length,
|
|
92
|
+
blockedCount: servers.filter((server) => server.status === 'blocked').length,
|
|
93
|
+
unexpectedCount: servers.filter((server) => server.status === 'unexpected').length,
|
|
94
|
+
notObservedCount: servers.filter((server) => server.status === 'not-observed').length
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function evidenceStatus(summary) {
|
|
99
|
+
if (summary.blockedCount || summary.unexpectedCount) return 'block';
|
|
100
|
+
if (summary.reviewCount) return 'needs-review';
|
|
101
|
+
if (summary.notObservedCount) return 'restricted';
|
|
102
|
+
return 'matched';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function evidenceDigest(snapshot) {
|
|
106
|
+
return sha256Json({
|
|
107
|
+
schemaVersion: snapshot.schemaVersion,
|
|
108
|
+
kind: snapshot.kind,
|
|
109
|
+
runtime: snapshot.runtime,
|
|
110
|
+
capturedAt: snapshot.capturedAt,
|
|
111
|
+
captureMethod: snapshot.captureMethod,
|
|
112
|
+
bindingDigest: snapshot.bindingDigest,
|
|
113
|
+
inventoryDigest: snapshot.inventoryDigest,
|
|
114
|
+
status: snapshot.status,
|
|
115
|
+
summary: snapshot.summary,
|
|
116
|
+
servers: snapshot.servers
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function inventoryFromEvidence(snapshot) {
|
|
121
|
+
return {
|
|
122
|
+
schemaVersion: 1,
|
|
123
|
+
kind: INVENTORY_KIND,
|
|
124
|
+
runtime: snapshot.runtime,
|
|
125
|
+
capturedAt: snapshot.capturedAt,
|
|
126
|
+
captureMethod: snapshot.captureMethod,
|
|
127
|
+
servers: snapshot.servers
|
|
128
|
+
.filter((server) => server.observed)
|
|
129
|
+
.map((server) => ({ name: server.name, toolCount: server.toolCount }))
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function createEffectiveRuntimeEvidence(bindingInput, inventoryInput) {
|
|
134
|
+
const bindings = validateToolPermissionBindingSnapshot(bindingInput);
|
|
135
|
+
const inventory = normalizeInventory(inventoryInput);
|
|
136
|
+
if (bindings.runtime !== inventory.runtime) {
|
|
137
|
+
throw new Error('Runtime inventory does not match the binding runtime.');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const configured = new Map(bindings.bindings.map((binding) => [binding.name, binding]));
|
|
141
|
+
const observed = new Map(inventory.servers.map((server) => [server.name, server]));
|
|
142
|
+
const names = [...new Set([...configured.keys(), ...observed.keys()])].sort();
|
|
143
|
+
const servers = names.map((name) => {
|
|
144
|
+
const binding = configured.get(name) || null;
|
|
145
|
+
const runtimeServer = observed.get(name) || null;
|
|
146
|
+
return {
|
|
147
|
+
name,
|
|
148
|
+
configured: binding !== null,
|
|
149
|
+
observed: runtimeServer !== null,
|
|
150
|
+
toolCount: runtimeServer ? runtimeServer.toolCount : null,
|
|
151
|
+
sourceDecision: binding ? binding.sourceDecision : null,
|
|
152
|
+
provenanceStatus: binding ? binding.provenanceStatus : null,
|
|
153
|
+
automaticGrant: binding ? binding.automaticGrant : null,
|
|
154
|
+
bindingStatus: binding ? binding.status : null,
|
|
155
|
+
status: serverStatus(binding, runtimeServer)
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
const summary = createSummary(servers);
|
|
159
|
+
const snapshot = {
|
|
160
|
+
schemaVersion: 1,
|
|
161
|
+
kind: KIND,
|
|
162
|
+
createdAt: new Date().toISOString(),
|
|
163
|
+
runtime: inventory.runtime,
|
|
164
|
+
capturedAt: inventory.capturedAt,
|
|
165
|
+
captureMethod: inventory.captureMethod,
|
|
166
|
+
bindingDigest: bindings.digest,
|
|
167
|
+
inventoryDigest: sha256Json(inventory),
|
|
168
|
+
status: evidenceStatus(summary),
|
|
169
|
+
summary,
|
|
170
|
+
servers
|
|
171
|
+
};
|
|
172
|
+
return { ...snapshot, digest: evidenceDigest(snapshot) };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function validateServer(server) {
|
|
176
|
+
requireExactKeys(server, new Set([
|
|
177
|
+
'name', 'configured', 'observed', 'toolCount', 'sourceDecision', 'provenanceStatus',
|
|
178
|
+
'automaticGrant', 'bindingStatus', 'status'
|
|
179
|
+
]), 'Invalid effective runtime server evidence.');
|
|
180
|
+
if (typeof server.name !== 'string' || !server.name) throw new Error('Runtime server evidence is missing a name.');
|
|
181
|
+
if (typeof server.configured !== 'boolean' || typeof server.observed !== 'boolean') {
|
|
182
|
+
throw new Error('Invalid runtime server configured/observed state.');
|
|
183
|
+
}
|
|
184
|
+
if (server.observed) requireCount(server.toolCount, 'toolCount');
|
|
185
|
+
else if (server.toolCount !== null) throw new Error('Unobserved runtime server must have a null tool count.');
|
|
186
|
+
if (!server.configured) {
|
|
187
|
+
for (const field of ['sourceDecision', 'provenanceStatus', 'automaticGrant', 'bindingStatus']) {
|
|
188
|
+
if (server[field] !== null) throw new Error('Unexpected runtime server has binding metadata.');
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
if (!['allow', 'ask', 'block'].includes(server.sourceDecision) ||
|
|
192
|
+
!['unknown', 'verified', 'mismatch'].includes(server.provenanceStatus) ||
|
|
193
|
+
!['none', 'scoped', 'all'].includes(server.automaticGrant) ||
|
|
194
|
+
!['not-granted', 'bound', 'needs-review', 'blocked'].includes(server.bindingStatus)) {
|
|
195
|
+
throw new Error('Invalid configured runtime server binding metadata.');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (!SERVER_STATUSES.has(server.status) || server.status !== serverStatus(
|
|
199
|
+
server.configured ? {
|
|
200
|
+
status: server.bindingStatus,
|
|
201
|
+
sourceDecision: server.sourceDecision,
|
|
202
|
+
provenanceStatus: server.provenanceStatus
|
|
203
|
+
} : null,
|
|
204
|
+
server.observed ? server : null
|
|
205
|
+
)) throw new Error('Invalid effective runtime server status.');
|
|
206
|
+
return server;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function validateEvidence(snapshot) {
|
|
210
|
+
requireExactKeys(snapshot, new Set([
|
|
211
|
+
'schemaVersion', 'kind', 'createdAt', 'runtime', 'capturedAt', 'captureMethod',
|
|
212
|
+
'bindingDigest', 'inventoryDigest', 'status', 'summary', 'servers', 'digest'
|
|
213
|
+
]), 'Invalid effective runtime evidence snapshot.');
|
|
214
|
+
if (snapshot.schemaVersion !== 1 || snapshot.kind !== KIND) {
|
|
215
|
+
throw new Error('Unsupported effective runtime evidence schema.');
|
|
216
|
+
}
|
|
217
|
+
if (!RUNTIMES.has(snapshot.runtime)) throw new Error('Invalid effective runtime evidence runtime.');
|
|
218
|
+
requireTimestamp(snapshot.createdAt, 'createdAt');
|
|
219
|
+
requireTimestamp(snapshot.capturedAt, 'capturedAt');
|
|
220
|
+
if (!CAPTURE_METHODS.has(snapshot.captureMethod)) throw new Error('Invalid effective runtime capture method.');
|
|
221
|
+
requireDigest(snapshot.bindingDigest, 'bindingDigest');
|
|
222
|
+
requireDigest(snapshot.inventoryDigest, 'inventoryDigest');
|
|
223
|
+
requireDigest(snapshot.digest, 'digest');
|
|
224
|
+
if (!EVIDENCE_STATUSES.has(snapshot.status)) throw new Error('Invalid effective runtime evidence status.');
|
|
225
|
+
if (!Array.isArray(snapshot.servers)) throw new Error('Effective runtime evidence is missing servers.');
|
|
226
|
+
for (const server of snapshot.servers) validateServer(server);
|
|
227
|
+
const names = snapshot.servers.map((server) => server.name);
|
|
228
|
+
if (new Set(names).size !== names.length || [...names].sort().join('\0') !== names.join('\0')) {
|
|
229
|
+
throw new Error('Effective runtime server evidence is not canonical.');
|
|
230
|
+
}
|
|
231
|
+
const summaryFields = [
|
|
232
|
+
'configuredServerCount', 'observedServerCount', 'observedToolCount', 'matchedCount',
|
|
233
|
+
'reviewCount', 'blockedCount', 'unexpectedCount', 'notObservedCount'
|
|
234
|
+
];
|
|
235
|
+
requireExactKeys(snapshot.summary, new Set(summaryFields), 'Invalid effective runtime evidence summary.');
|
|
236
|
+
for (const field of summaryFields) requireCount(snapshot.summary[field], field);
|
|
237
|
+
const summary = createSummary(snapshot.servers);
|
|
238
|
+
if (sha256Json(summary) !== sha256Json(snapshot.summary)) throw new Error('Effective runtime evidence summary mismatch.');
|
|
239
|
+
if (snapshot.inventoryDigest !== sha256Json(inventoryFromEvidence(snapshot))) {
|
|
240
|
+
throw new Error('Effective runtime inventory digest mismatch.');
|
|
241
|
+
}
|
|
242
|
+
if (snapshot.status !== evidenceStatus(summary)) throw new Error('Effective runtime evidence status mismatch.');
|
|
243
|
+
if (snapshot.digest !== evidenceDigest(snapshot)) throw new Error('Effective runtime evidence digest mismatch.');
|
|
244
|
+
return snapshot;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function validateEffectiveRuntimeEvidence(snapshot) {
|
|
248
|
+
return validateEvidence(snapshot);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function addChange(changes, impact, type, name, field, before, after) {
|
|
252
|
+
changes.push({ impact, type, name, field, before, after });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function diffEffectiveRuntimeEvidence(beforeInput, afterInput) {
|
|
256
|
+
const before = validateEvidence(beforeInput);
|
|
257
|
+
const after = validateEvidence(afterInput);
|
|
258
|
+
if (before.runtime !== after.runtime) throw new Error('Cannot compare different runtime evidence types.');
|
|
259
|
+
const changes = [];
|
|
260
|
+
if (before.bindingDigest !== after.bindingDigest) {
|
|
261
|
+
addChange(changes, 'review', 'bindings-changed', null, 'bindingDigest', null, null);
|
|
262
|
+
}
|
|
263
|
+
if (before.captureMethod !== after.captureMethod) {
|
|
264
|
+
addChange(changes, 'review', 'capture-method-changed', null, 'captureMethod', before.captureMethod, after.captureMethod);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const left = new Map(before.servers.map((server) => [server.name, server]));
|
|
268
|
+
const right = new Map(after.servers.map((server) => [server.name, server]));
|
|
269
|
+
const risk = { 'not-observed': 0, matched: 1, 'needs-review': 2, blocked: 3, unexpected: 3 };
|
|
270
|
+
for (const [name, current] of right) {
|
|
271
|
+
const previous = left.get(name);
|
|
272
|
+
if (!previous) {
|
|
273
|
+
addChange(changes, current.observed ? 'expansion' : 'review', 'server-added', name, 'server', null, current.status);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (previous.observed !== current.observed) {
|
|
277
|
+
addChange(changes, current.observed ? 'expansion' : 'restriction', 'observation-changed', name, 'observed', previous.observed, current.observed);
|
|
278
|
+
}
|
|
279
|
+
const beforeCount = previous.toolCount || 0;
|
|
280
|
+
const afterCount = current.toolCount || 0;
|
|
281
|
+
if (beforeCount !== afterCount) {
|
|
282
|
+
addChange(changes, afterCount > beforeCount ? 'expansion' : 'restriction', 'tool-count-changed', name, 'toolCount', previous.toolCount, current.toolCount);
|
|
283
|
+
}
|
|
284
|
+
for (const field of ['configured', 'sourceDecision', 'provenanceStatus', 'automaticGrant', 'bindingStatus']) {
|
|
285
|
+
if (previous[field] !== current[field]) {
|
|
286
|
+
addChange(changes, 'review', 'binding-state-changed', name, field, previous[field], current[field]);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (previous.status !== current.status) {
|
|
290
|
+
addChange(
|
|
291
|
+
changes,
|
|
292
|
+
risk[current.status] > risk[previous.status] ? 'expansion' :
|
|
293
|
+
risk[current.status] < risk[previous.status] ? 'restriction' : 'review',
|
|
294
|
+
'server-status-changed', name, 'status', previous.status, current.status
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
for (const [name, previous] of left) {
|
|
299
|
+
if (!right.has(name)) {
|
|
300
|
+
addChange(changes, previous.observed ? 'restriction' : 'review', 'server-removed', name, 'server', previous.status, null);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const summary = {
|
|
304
|
+
expansions: changes.filter((change) => change.impact === 'expansion').length,
|
|
305
|
+
restrictions: changes.filter((change) => change.impact === 'restriction').length,
|
|
306
|
+
reviews: changes.filter((change) => change.impact === 'review').length
|
|
307
|
+
};
|
|
308
|
+
return {
|
|
309
|
+
schemaVersion: 1,
|
|
310
|
+
runtime: before.runtime,
|
|
311
|
+
status: summary.expansions || summary.reviews ? 'needs-review' :
|
|
312
|
+
summary.restrictions ? 'restricted' : 'unchanged',
|
|
313
|
+
baselineDigest: before.digest,
|
|
314
|
+
currentDigest: after.digest,
|
|
315
|
+
summary,
|
|
316
|
+
changes
|
|
317
|
+
};
|
|
318
|
+
}
|