trustdex 0.9.0 → 0.11.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.
Files changed (36) hide show
  1. package/README.ko.md +53 -5
  2. package/README.md +54 -5
  3. package/bin/trustdex.mjs +136 -0
  4. package/docs/ARCHITECTURE.ko.md +12 -0
  5. package/docs/ARCHITECTURE.md +12 -0
  6. package/docs/CLAUDE.ko.md +13 -1
  7. package/docs/CLAUDE.md +14 -1
  8. package/docs/CODEX.ko.md +15 -4
  9. package/docs/CODEX.md +16 -4
  10. package/docs/DEFENSIVE_USE_CASE.ko.md +8 -0
  11. package/docs/DEFENSIVE_USE_CASE.md +9 -0
  12. package/docs/EVALUATION.ko.md +25 -0
  13. package/docs/EVALUATION.md +25 -0
  14. package/docs/ROADMAP.ko.md +10 -4
  15. package/docs/ROADMAP.md +10 -4
  16. package/docs/RUNTIME_EVIDENCE.ko.md +89 -0
  17. package/docs/RUNTIME_EVIDENCE.md +96 -0
  18. package/docs/THREAT_MODEL.ko.md +9 -0
  19. package/docs/THREAT_MODEL.md +9 -0
  20. package/examples/adversarial/README.ko.md +1 -1
  21. package/examples/adversarial/README.md +1 -1
  22. package/examples/adversarial/bindings/README.ko.md +8 -0
  23. package/examples/adversarial/bindings/README.md +8 -0
  24. package/examples/adversarial/bindings/expected.json +12 -0
  25. package/examples/adversarial/bindings/runtime-after.json +21 -0
  26. package/examples/adversarial/bindings/runtime-before.json +13 -0
  27. package/package.json +3 -3
  28. package/schemas/effective-runtime-tools-diff-v1.schema.json +50 -0
  29. package/schemas/effective-runtime-tools-diff-v2.schema.json +51 -0
  30. package/schemas/effective-runtime-tools-v1.schema.json +68 -0
  31. package/schemas/effective-runtime-tools-v2.schema.json +98 -0
  32. package/schemas/runtime-tool-inventory-attestation-v1.schema.json +27 -0
  33. package/schemas/runtime-tool-inventory-v1.schema.json +27 -0
  34. package/src/bindings.mjs +4 -0
  35. package/src/runtime-attestation.mjs +202 -0
  36. package/src/runtime-evidence.mjs +392 -0
@@ -0,0 +1,392 @@
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
+ export function normalizeRuntimeToolInventory(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
+ export function runtimeToolInventoryDigest(input) {
75
+ return sha256Json(normalizeRuntimeToolInventory(input));
76
+ }
77
+
78
+ function serverStatus(binding, observed) {
79
+ if (!binding) return 'unexpected';
80
+ if (!observed) return 'not-observed';
81
+ if (binding.status === 'blocked' || binding.sourceDecision === 'block' ||
82
+ binding.provenanceStatus === 'mismatch') return 'blocked';
83
+ if (binding.status === 'needs-review' || binding.sourceDecision === 'ask') return 'needs-review';
84
+ return 'matched';
85
+ }
86
+
87
+ function createSummary(servers) {
88
+ const observedToolCount = servers.reduce((total, server) => total + (server.toolCount || 0), 0);
89
+ requireCount(observedToolCount, 'observedToolCount');
90
+ return {
91
+ configuredServerCount: servers.filter((server) => server.configured).length,
92
+ observedServerCount: servers.filter((server) => server.observed).length,
93
+ observedToolCount,
94
+ matchedCount: servers.filter((server) => server.status === 'matched').length,
95
+ reviewCount: servers.filter((server) => server.status === 'needs-review').length,
96
+ blockedCount: servers.filter((server) => server.status === 'blocked').length,
97
+ unexpectedCount: servers.filter((server) => server.status === 'unexpected').length,
98
+ notObservedCount: servers.filter((server) => server.status === 'not-observed').length
99
+ };
100
+ }
101
+
102
+ function baseEvidenceStatus(summary) {
103
+ if (summary.blockedCount || summary.unexpectedCount) return 'block';
104
+ if (summary.reviewCount) return 'needs-review';
105
+ if (summary.notObservedCount) return 'restricted';
106
+ return 'matched';
107
+ }
108
+
109
+ function normalizeAuthentication(input, expectedInventoryDigest) {
110
+ if (input === undefined || input === null) {
111
+ return { status: 'unverified', producerKeyId: null, attestationDigest: null };
112
+ }
113
+ requireExactKeys(input, new Set([
114
+ 'valid', 'reason', 'producerKeyId', 'inventoryDigest', 'attestationDigest'
115
+ ]), 'Invalid runtime inventory authentication.');
116
+ if (input.valid !== true || input.reason !== null) {
117
+ throw new Error('Runtime inventory authentication is not verified.');
118
+ }
119
+ if (requireDigest(input.inventoryDigest, 'inventoryDigest') !== expectedInventoryDigest) {
120
+ throw new Error('Runtime inventory authentication digest mismatch.');
121
+ }
122
+ return {
123
+ status: 'verified',
124
+ producerKeyId: requireDigest(input.producerKeyId, 'producerKeyId'),
125
+ attestationDigest: requireDigest(input.attestationDigest, 'attestationDigest')
126
+ };
127
+ }
128
+
129
+ function evidenceStatus(summary, authentication, schemaVersion = 2) {
130
+ if (summary.blockedCount || summary.unexpectedCount) return 'block';
131
+ if (schemaVersion >= 2 && authentication.status !== 'verified') return 'needs-review';
132
+ return baseEvidenceStatus(summary);
133
+ }
134
+
135
+ function evidenceDigest(snapshot) {
136
+ const projected = {
137
+ schemaVersion: snapshot.schemaVersion,
138
+ kind: snapshot.kind,
139
+ runtime: snapshot.runtime,
140
+ capturedAt: snapshot.capturedAt,
141
+ captureMethod: snapshot.captureMethod,
142
+ bindingDigest: snapshot.bindingDigest,
143
+ inventoryDigest: snapshot.inventoryDigest,
144
+ status: snapshot.status,
145
+ summary: snapshot.summary,
146
+ servers: snapshot.servers
147
+ };
148
+ if (snapshot.schemaVersion >= 2) {
149
+ projected.inventoryAuthentication = snapshot.inventoryAuthentication;
150
+ projected.producerKeyId = snapshot.producerKeyId;
151
+ projected.attestationDigest = snapshot.attestationDigest;
152
+ }
153
+ return sha256Json(projected);
154
+ }
155
+
156
+ function inventoryFromEvidence(snapshot) {
157
+ return {
158
+ schemaVersion: 1,
159
+ kind: INVENTORY_KIND,
160
+ runtime: snapshot.runtime,
161
+ capturedAt: snapshot.capturedAt,
162
+ captureMethod: snapshot.captureMethod,
163
+ servers: snapshot.servers
164
+ .filter((server) => server.observed)
165
+ .map((server) => ({ name: server.name, toolCount: server.toolCount }))
166
+ };
167
+ }
168
+
169
+ export function createEffectiveRuntimeEvidence(bindingInput, inventoryInput, authenticationInput = null) {
170
+ const bindings = validateToolPermissionBindingSnapshot(bindingInput);
171
+ const inventory = normalizeRuntimeToolInventory(inventoryInput);
172
+ const inventoryDigest = runtimeToolInventoryDigest(inventory);
173
+ const authentication = normalizeAuthentication(authenticationInput, inventoryDigest);
174
+ if (bindings.runtime !== inventory.runtime) {
175
+ throw new Error('Runtime inventory does not match the binding runtime.');
176
+ }
177
+
178
+ const configured = new Map(bindings.bindings.map((binding) => [binding.name, binding]));
179
+ const observed = new Map(inventory.servers.map((server) => [server.name, server]));
180
+ const names = [...new Set([...configured.keys(), ...observed.keys()])].sort();
181
+ const servers = names.map((name) => {
182
+ const binding = configured.get(name) || null;
183
+ const runtimeServer = observed.get(name) || null;
184
+ return {
185
+ name,
186
+ configured: binding !== null,
187
+ observed: runtimeServer !== null,
188
+ toolCount: runtimeServer ? runtimeServer.toolCount : null,
189
+ sourceDecision: binding ? binding.sourceDecision : null,
190
+ provenanceStatus: binding ? binding.provenanceStatus : null,
191
+ automaticGrant: binding ? binding.automaticGrant : null,
192
+ bindingStatus: binding ? binding.status : null,
193
+ status: serverStatus(binding, runtimeServer)
194
+ };
195
+ });
196
+ const summary = createSummary(servers);
197
+ const snapshot = {
198
+ schemaVersion: 2,
199
+ kind: KIND,
200
+ createdAt: new Date().toISOString(),
201
+ runtime: inventory.runtime,
202
+ capturedAt: inventory.capturedAt,
203
+ captureMethod: inventory.captureMethod,
204
+ bindingDigest: bindings.digest,
205
+ inventoryDigest,
206
+ inventoryAuthentication: authentication.status,
207
+ producerKeyId: authentication.producerKeyId,
208
+ attestationDigest: authentication.attestationDigest,
209
+ status: evidenceStatus(summary, authentication),
210
+ summary,
211
+ servers
212
+ };
213
+ return { ...snapshot, digest: evidenceDigest(snapshot) };
214
+ }
215
+
216
+ function validateServer(server) {
217
+ requireExactKeys(server, new Set([
218
+ 'name', 'configured', 'observed', 'toolCount', 'sourceDecision', 'provenanceStatus',
219
+ 'automaticGrant', 'bindingStatus', 'status'
220
+ ]), 'Invalid effective runtime server evidence.');
221
+ if (typeof server.name !== 'string' || !server.name) throw new Error('Runtime server evidence is missing a name.');
222
+ if (typeof server.configured !== 'boolean' || typeof server.observed !== 'boolean') {
223
+ throw new Error('Invalid runtime server configured/observed state.');
224
+ }
225
+ if (server.observed) requireCount(server.toolCount, 'toolCount');
226
+ else if (server.toolCount !== null) throw new Error('Unobserved runtime server must have a null tool count.');
227
+ if (!server.configured) {
228
+ for (const field of ['sourceDecision', 'provenanceStatus', 'automaticGrant', 'bindingStatus']) {
229
+ if (server[field] !== null) throw new Error('Unexpected runtime server has binding metadata.');
230
+ }
231
+ } else {
232
+ if (!['allow', 'ask', 'block'].includes(server.sourceDecision) ||
233
+ !['unknown', 'verified', 'mismatch'].includes(server.provenanceStatus) ||
234
+ !['none', 'scoped', 'all'].includes(server.automaticGrant) ||
235
+ !['not-granted', 'bound', 'needs-review', 'blocked'].includes(server.bindingStatus)) {
236
+ throw new Error('Invalid configured runtime server binding metadata.');
237
+ }
238
+ }
239
+ if (!SERVER_STATUSES.has(server.status) || server.status !== serverStatus(
240
+ server.configured ? {
241
+ status: server.bindingStatus,
242
+ sourceDecision: server.sourceDecision,
243
+ provenanceStatus: server.provenanceStatus
244
+ } : null,
245
+ server.observed ? server : null
246
+ )) throw new Error('Invalid effective runtime server status.');
247
+ return server;
248
+ }
249
+
250
+ function validateEvidence(snapshot) {
251
+ const baseFields = [
252
+ 'schemaVersion', 'kind', 'createdAt', 'runtime', 'capturedAt', 'captureMethod',
253
+ 'bindingDigest', 'inventoryDigest', 'status', 'summary', 'servers', 'digest'
254
+ ];
255
+ const expectedFields = new Set(snapshot?.schemaVersion === 2
256
+ ? [...baseFields, 'inventoryAuthentication', 'producerKeyId', 'attestationDigest']
257
+ : baseFields);
258
+ requireExactKeys(snapshot, expectedFields, 'Invalid effective runtime evidence snapshot.');
259
+ if (![1, 2].includes(snapshot.schemaVersion) || snapshot.kind !== KIND) {
260
+ throw new Error('Unsupported effective runtime evidence schema.');
261
+ }
262
+ if (!RUNTIMES.has(snapshot.runtime)) throw new Error('Invalid effective runtime evidence runtime.');
263
+ requireTimestamp(snapshot.createdAt, 'createdAt');
264
+ requireTimestamp(snapshot.capturedAt, 'capturedAt');
265
+ if (!CAPTURE_METHODS.has(snapshot.captureMethod)) throw new Error('Invalid effective runtime capture method.');
266
+ requireDigest(snapshot.bindingDigest, 'bindingDigest');
267
+ requireDigest(snapshot.inventoryDigest, 'inventoryDigest');
268
+ requireDigest(snapshot.digest, 'digest');
269
+ let authentication = { status: 'unverified', producerKeyId: null, attestationDigest: null };
270
+ if (snapshot.schemaVersion === 2) {
271
+ if (!['unverified', 'verified'].includes(snapshot.inventoryAuthentication)) {
272
+ throw new Error('Invalid runtime inventory authentication state.');
273
+ }
274
+ if (snapshot.inventoryAuthentication === 'verified') {
275
+ authentication = normalizeAuthentication({
276
+ valid: true,
277
+ reason: null,
278
+ producerKeyId: snapshot.producerKeyId,
279
+ inventoryDigest: snapshot.inventoryDigest,
280
+ attestationDigest: snapshot.attestationDigest
281
+ }, snapshot.inventoryDigest);
282
+ } else if (snapshot.producerKeyId !== null || snapshot.attestationDigest !== null) {
283
+ throw new Error('Unverified runtime inventory cannot retain attestation metadata.');
284
+ }
285
+ }
286
+ if (!EVIDENCE_STATUSES.has(snapshot.status)) throw new Error('Invalid effective runtime evidence status.');
287
+ if (!Array.isArray(snapshot.servers)) throw new Error('Effective runtime evidence is missing servers.');
288
+ for (const server of snapshot.servers) validateServer(server);
289
+ const names = snapshot.servers.map((server) => server.name);
290
+ if (new Set(names).size !== names.length || [...names].sort().join('\0') !== names.join('\0')) {
291
+ throw new Error('Effective runtime server evidence is not canonical.');
292
+ }
293
+ const summaryFields = [
294
+ 'configuredServerCount', 'observedServerCount', 'observedToolCount', 'matchedCount',
295
+ 'reviewCount', 'blockedCount', 'unexpectedCount', 'notObservedCount'
296
+ ];
297
+ requireExactKeys(snapshot.summary, new Set(summaryFields), 'Invalid effective runtime evidence summary.');
298
+ for (const field of summaryFields) requireCount(snapshot.summary[field], field);
299
+ const summary = createSummary(snapshot.servers);
300
+ if (sha256Json(summary) !== sha256Json(snapshot.summary)) throw new Error('Effective runtime evidence summary mismatch.');
301
+ if (snapshot.inventoryDigest !== sha256Json(inventoryFromEvidence(snapshot))) {
302
+ throw new Error('Effective runtime inventory digest mismatch.');
303
+ }
304
+ if (snapshot.status !== evidenceStatus(summary, authentication, snapshot.schemaVersion)) {
305
+ throw new Error('Effective runtime evidence status mismatch.');
306
+ }
307
+ if (snapshot.digest !== evidenceDigest(snapshot)) throw new Error('Effective runtime evidence digest mismatch.');
308
+ return snapshot;
309
+ }
310
+
311
+ export function validateEffectiveRuntimeEvidence(snapshot) {
312
+ return validateEvidence(snapshot);
313
+ }
314
+
315
+ function addChange(changes, impact, type, name, field, before, after) {
316
+ changes.push({ impact, type, name, field, before, after });
317
+ }
318
+
319
+ export function diffEffectiveRuntimeEvidence(beforeInput, afterInput) {
320
+ const before = validateEvidence(beforeInput);
321
+ const after = validateEvidence(afterInput);
322
+ if (before.runtime !== after.runtime) throw new Error('Cannot compare different runtime evidence types.');
323
+ const changes = [];
324
+ if (before.bindingDigest !== after.bindingDigest) {
325
+ addChange(changes, 'review', 'bindings-changed', null, 'bindingDigest', null, null);
326
+ }
327
+ if (before.captureMethod !== after.captureMethod) {
328
+ addChange(changes, 'review', 'capture-method-changed', null, 'captureMethod', before.captureMethod, after.captureMethod);
329
+ }
330
+ const beforeAuthentication = before.schemaVersion === 2 ? before.inventoryAuthentication : 'unverified';
331
+ const afterAuthentication = after.schemaVersion === 2 ? after.inventoryAuthentication : 'unverified';
332
+ if (beforeAuthentication !== afterAuthentication) {
333
+ addChange(changes, 'review', 'inventory-authentication-changed', null, 'inventoryAuthentication', beforeAuthentication, afterAuthentication);
334
+ }
335
+ const beforeKeyId = before.schemaVersion === 2 ? before.producerKeyId : null;
336
+ const afterKeyId = after.schemaVersion === 2 ? after.producerKeyId : null;
337
+ if (beforeKeyId !== afterKeyId) {
338
+ addChange(changes, 'review', 'producer-key-changed', null, 'producerKeyId', beforeKeyId, afterKeyId);
339
+ }
340
+
341
+ const left = new Map(before.servers.map((server) => [server.name, server]));
342
+ const right = new Map(after.servers.map((server) => [server.name, server]));
343
+ const risk = { 'not-observed': 0, matched: 1, 'needs-review': 2, blocked: 3, unexpected: 3 };
344
+ for (const [name, current] of right) {
345
+ const previous = left.get(name);
346
+ if (!previous) {
347
+ addChange(changes, current.observed ? 'expansion' : 'review', 'server-added', name, 'server', null, current.status);
348
+ continue;
349
+ }
350
+ if (previous.observed !== current.observed) {
351
+ addChange(changes, current.observed ? 'expansion' : 'restriction', 'observation-changed', name, 'observed', previous.observed, current.observed);
352
+ }
353
+ const beforeCount = previous.toolCount || 0;
354
+ const afterCount = current.toolCount || 0;
355
+ if (beforeCount !== afterCount) {
356
+ addChange(changes, afterCount > beforeCount ? 'expansion' : 'restriction', 'tool-count-changed', name, 'toolCount', previous.toolCount, current.toolCount);
357
+ }
358
+ for (const field of ['configured', 'sourceDecision', 'provenanceStatus', 'automaticGrant', 'bindingStatus']) {
359
+ if (previous[field] !== current[field]) {
360
+ addChange(changes, 'review', 'binding-state-changed', name, field, previous[field], current[field]);
361
+ }
362
+ }
363
+ if (previous.status !== current.status) {
364
+ addChange(
365
+ changes,
366
+ risk[current.status] > risk[previous.status] ? 'expansion' :
367
+ risk[current.status] < risk[previous.status] ? 'restriction' : 'review',
368
+ 'server-status-changed', name, 'status', previous.status, current.status
369
+ );
370
+ }
371
+ }
372
+ for (const [name, previous] of left) {
373
+ if (!right.has(name)) {
374
+ addChange(changes, previous.observed ? 'restriction' : 'review', 'server-removed', name, 'server', previous.status, null);
375
+ }
376
+ }
377
+ const summary = {
378
+ expansions: changes.filter((change) => change.impact === 'expansion').length,
379
+ restrictions: changes.filter((change) => change.impact === 'restriction').length,
380
+ reviews: changes.filter((change) => change.impact === 'review').length
381
+ };
382
+ return {
383
+ schemaVersion: before.schemaVersion === 2 || after.schemaVersion === 2 ? 2 : 1,
384
+ runtime: before.runtime,
385
+ status: summary.expansions || summary.reviews ? 'needs-review' :
386
+ summary.restrictions ? 'restricted' : 'unchanged',
387
+ baselineDigest: before.digest,
388
+ currentDigest: after.digest,
389
+ summary,
390
+ changes
391
+ };
392
+ }