trustdex 0.7.0 → 0.9.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 (35) hide show
  1. package/README.ko.md +71 -8
  2. package/README.md +75 -8
  3. package/bin/trustdex.mjs +204 -11
  4. package/docs/ARCHITECTURE.ko.md +10 -2
  5. package/docs/ARCHITECTURE.md +15 -4
  6. package/docs/CLAUDE.ko.md +47 -2
  7. package/docs/CLAUDE.md +54 -5
  8. package/docs/CODEX.ko.md +49 -5
  9. package/docs/CODEX.md +59 -11
  10. package/docs/DEFENSIVE_USE_CASE.ko.md +8 -2
  11. package/docs/DEFENSIVE_USE_CASE.md +11 -3
  12. package/docs/EVALUATION.ko.md +29 -1
  13. package/docs/EVALUATION.md +29 -1
  14. package/docs/ROADMAP.ko.md +7 -4
  15. package/docs/ROADMAP.md +7 -4
  16. package/docs/THREAT_MODEL.ko.md +7 -1
  17. package/docs/THREAT_MODEL.md +7 -1
  18. package/examples/adversarial/README.ko.md +2 -0
  19. package/examples/adversarial/README.md +2 -0
  20. package/examples/adversarial/bindings/README.ko.md +24 -0
  21. package/examples/adversarial/bindings/README.md +25 -0
  22. package/examples/adversarial/bindings/claude.mcp.json +12 -0
  23. package/examples/adversarial/bindings/claude.settings.json +9 -0
  24. package/examples/adversarial/bindings/codex.toml +9 -0
  25. package/examples/adversarial/bindings/expected.json +15 -0
  26. package/examples/adversarial/bindings/trust-store.json +15 -0
  27. package/package.json +3 -3
  28. package/schemas/permission-posture-diff-v2.schema.json +37 -0
  29. package/schemas/permission-posture-v2.schema.json +117 -0
  30. package/schemas/tool-permission-bindings-diff-v1.schema.json +70 -0
  31. package/schemas/tool-permission-bindings-v1.schema.json +94 -0
  32. package/src/bindings.mjs +354 -0
  33. package/src/claude-settings.mjs +130 -0
  34. package/src/codex-settings.mjs +81 -3
  35. package/src/posture.mjs +69 -8
package/src/posture.mjs CHANGED
@@ -2,7 +2,8 @@ import { sha256Json } from './canonical.mjs';
2
2
 
3
3
  const ACTION_RANK = Object.freeze({ allow: 0, ask: 1, block: 2 });
4
4
  const SNAPSHOT_KIND = 'agent-permission-posture';
5
- const SNAPSHOT_VERSION = 1;
5
+ const SINGLE_SNAPSHOT_VERSION = 1;
6
+ const STACK_SNAPSHOT_VERSION = 2;
6
7
 
7
8
  const CLAUDE_MODES = new Set([
8
9
  null,
@@ -66,6 +67,10 @@ const RUNTIME_SIGNALS = Object.freeze({
66
67
  'uninspected-codex-sections'
67
68
  ])
68
69
  });
70
+ const SOURCE_SCOPES = Object.freeze({
71
+ claude: Object.freeze(['user', 'project', 'local', 'session', 'managed']),
72
+ codex: Object.freeze(['system', 'managed', 'user', 'profile', 'project', 'cli'])
73
+ });
69
74
 
70
75
  function requireObject(value, message) {
71
76
  if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(message);
@@ -202,14 +207,36 @@ function projectPolicy(policy, runtime) {
202
207
  };
203
208
  }
204
209
 
210
+ function normalizeSettingsContext(context, runtime) {
211
+ requireObject(context, 'Permission posture settings context is missing.');
212
+ assertMetadataKeys(context, new Set(['sourceCount', 'activeSourceCount', 'sourceCounts']));
213
+ const sourceCount = requireCount(context.sourceCount, 'sourceCount');
214
+ const activeSourceCount = requireCount(context.activeSourceCount, 'activeSourceCount');
215
+ if (activeSourceCount > sourceCount) {
216
+ throw new Error('Permission posture active source count exceeds total source count.');
217
+ }
218
+ const inputCounts = requireObject(context.sourceCounts, 'Permission posture source counts are missing.');
219
+ const scopes = SOURCE_SCOPES[runtime];
220
+ assertMetadataKeys(inputCounts, new Set(scopes));
221
+ const sourceCounts = Object.fromEntries(
222
+ scopes.map((scope) => [scope, requireCount(inputCounts[scope], `sourceCounts.${scope}`)])
223
+ );
224
+ if (Object.values(sourceCounts).reduce((total, count) => total + count, 0) !== sourceCount) {
225
+ throw new Error('Permission posture source counts do not match the total source count.');
226
+ }
227
+ return { sourceCount, activeSourceCount, sourceCounts };
228
+ }
229
+
205
230
  function snapshotDigest(snapshot) {
206
- return sha256Json({
231
+ const projected = {
207
232
  schemaVersion: snapshot.schemaVersion,
208
233
  kind: snapshot.kind,
209
234
  runtime: snapshot.runtime,
210
235
  policyDigest: snapshot.policyDigest,
211
236
  posture: snapshot.posture
212
- });
237
+ };
238
+ if (snapshot.schemaVersion === STACK_SNAPSHOT_VERSION) projected.sources = snapshot.sources;
239
+ return sha256Json(projected);
213
240
  }
214
241
 
215
242
  export function createPermissionPostureSnapshot(result, policy = {}) {
@@ -218,9 +245,9 @@ export function createPermissionPostureSnapshot(result, policy = {}) {
218
245
  if (!Array.isArray(result.signals) || result.signals.some((item) => typeof item !== 'string')) {
219
246
  throw new Error('Permission posture result has invalid signals.');
220
247
  }
221
- const runtime = result.kind === 'claude-settings'
248
+ const runtime = ['claude-settings', 'claude-settings-stack'].includes(result.kind)
222
249
  ? 'claude'
223
- : result.kind === 'codex-settings' ? 'codex' : null;
250
+ : ['codex-settings', 'codex-settings-stack'].includes(result.kind) ? 'codex' : null;
224
251
  if (!runtime) throw new Error('Permission posture supports Claude or Codex settings results only.');
225
252
  if (result.signals.some((signal) => !RUNTIME_SIGNALS[runtime].has(signal))) {
226
253
  throw new Error('Permission posture result contains an unsupported signal.');
@@ -234,20 +261,23 @@ export function createPermissionPostureSnapshot(result, policy = {}) {
234
261
  ? normalizeClaudeMetadata(metadata)
235
262
  : normalizeCodexMetadata(metadata)
236
263
  };
264
+ const isStack = result.kind.endsWith('-stack');
237
265
  const snapshot = {
238
- schemaVersion: SNAPSHOT_VERSION,
266
+ schemaVersion: isStack ? STACK_SNAPSHOT_VERSION : SINGLE_SNAPSHOT_VERSION,
239
267
  kind: SNAPSHOT_KIND,
240
268
  createdAt: new Date().toISOString(),
241
269
  runtime,
242
270
  policyDigest: sha256Json(projectPolicy(policy, runtime)),
243
271
  posture
244
272
  };
273
+ if (isStack) snapshot.sources = normalizeSettingsContext(result.settingsContext, runtime);
245
274
  return { ...snapshot, digest: snapshotDigest(snapshot) };
246
275
  }
247
276
 
248
277
  function validateSnapshot(snapshot) {
249
278
  requireObject(snapshot, 'Invalid permission posture snapshot.');
250
- if (snapshot.schemaVersion !== SNAPSHOT_VERSION || snapshot.kind !== SNAPSHOT_KIND) {
279
+ if (![SINGLE_SNAPSHOT_VERSION, STACK_SNAPSHOT_VERSION].includes(snapshot.schemaVersion) ||
280
+ snapshot.kind !== SNAPSHOT_KIND) {
251
281
  throw new Error('Unsupported permission posture snapshot schema.');
252
282
  }
253
283
  if (!['claude', 'codex'].includes(snapshot.runtime)) throw new Error('Invalid permission posture runtime.');
@@ -272,6 +302,14 @@ function validateSnapshot(snapshot) {
272
302
  const normalized = normalizeCodexSnapshotMetadata(metadata);
273
303
  if (sha256Json(normalized) !== sha256Json(metadata)) throw new Error('Non-canonical Codex posture metadata.');
274
304
  }
305
+ if (snapshot.schemaVersion === STACK_SNAPSHOT_VERSION) {
306
+ const normalized = normalizeSettingsContext(snapshot.sources, snapshot.runtime);
307
+ if (sha256Json(normalized) !== sha256Json(snapshot.sources)) {
308
+ throw new Error('Non-canonical permission posture source metadata.');
309
+ }
310
+ } else if (snapshot.sources !== undefined) {
311
+ throw new Error('Permission posture v1 does not support source metadata.');
312
+ }
275
313
  if (snapshot.digest !== snapshotDigest(snapshot)) throw new Error('Permission posture snapshot digest mismatch.');
276
314
  return snapshot;
277
315
  }
@@ -496,6 +534,29 @@ export function diffPermissionPostureSnapshots(beforeInput, afterInput) {
496
534
  if (before.runtime !== after.runtime) throw new Error('Cannot compare different permission posture runtimes.');
497
535
 
498
536
  const changes = [];
537
+ if (before.schemaVersion !== after.schemaVersion) {
538
+ addChange(
539
+ changes,
540
+ 'review',
541
+ 'value-changed',
542
+ 'schemaVersion',
543
+ before.schemaVersion,
544
+ after.schemaVersion
545
+ );
546
+ } else if (before.schemaVersion === STACK_SNAPSHOT_VERSION) {
547
+ for (const field of ['sourceCount', 'activeSourceCount']) {
548
+ if (before.sources[field] !== after.sources[field]) {
549
+ addChange(changes, 'review', 'count-changed', `sources.${field}`, before.sources[field], after.sources[field]);
550
+ }
551
+ }
552
+ for (const scope of SOURCE_SCOPES[before.runtime]) {
553
+ const previous = before.sources.sourceCounts[scope];
554
+ const current = after.sources.sourceCounts[scope];
555
+ if (previous !== current) {
556
+ addChange(changes, 'review', 'count-changed', `sources.${scope}`, previous, current);
557
+ }
558
+ }
559
+ }
499
560
  if (before.policyDigest !== after.policyDigest) {
500
561
  changes.push({ impact: 'review', type: 'policy-changed', field: 'policyDigest' });
501
562
  }
@@ -529,7 +590,7 @@ export function diffPermissionPostureSnapshots(beforeInput, afterInput) {
529
590
  ? 'needs-review'
530
591
  : summary.restrictions ? 'restricted' : 'unchanged';
531
592
  return {
532
- schemaVersion: SNAPSHOT_VERSION,
593
+ schemaVersion: Math.max(before.schemaVersion, after.schemaVersion),
533
594
  runtime: before.runtime,
534
595
  status,
535
596
  baselineDigest: before.digest,