trustdex 0.4.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.
@@ -0,0 +1,498 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import process from 'node:process';
5
+
6
+ import { inspectMcpConfig } from '../src/inspect.mjs';
7
+ import { parseCodexMcpToml, gateCodexToml } from '../src/codex-config.mjs';
8
+ import { inspectSkillText, inspectPluginManifest } from '../src/extensions.mjs';
9
+ import { evaluateAll } from '../src/policy.mjs';
10
+ import { getPolicyPack, mergePolicy } from '../src/policy-packs.mjs';
11
+ import { parseTrustStore, attachProvenanceAll } from '../src/provenance.mjs';
12
+ import {
13
+ inspectGitHubRepository,
14
+ inspectNpmPackage,
15
+ inspectGitHubReleaseSignature,
16
+ inspectMcpRegistryServer,
17
+ provenanceObservationToClaim,
18
+ upsertTrustStoreClaim,
19
+ verifyArtifactBytes
20
+ } from '../src/provenance-online.mjs';
21
+ import { createSnapshot, diffSnapshots } from '../src/snapshot.mjs';
22
+ import { gateMcpConfig } from '../src/gate.mjs';
23
+ import { assessReview } from '../src/review.mjs';
24
+ import {
25
+ generateTrustKeyPair,
26
+ createTrustRecord,
27
+ verifyTrustRecord
28
+ } from '../src/trust-record.mjs';
29
+
30
+ const VERSION = '0.4.0';
31
+
32
+ function usage() {
33
+ console.log(`TrustDex v${VERSION}
34
+
35
+ Usage:
36
+ trustdex inspect <mcp.json> [--pack strict|official-first|development]
37
+ [--policy policy.json] [--trust-store trust-store.json] [--json]
38
+
39
+ trustdex inspect-skill <SKILL.md> [--pack ...] [--policy policy.json] [--json]
40
+ trustdex inspect-plugin <plugin.json> [--pack ...] [--policy policy.json] [--json]
41
+ trustdex inspect-codex <config.toml> [--pack ...] [--policy policy.json]
42
+ [--trust-store trust-store.json] [--json]
43
+
44
+ trustdex gate <mcp.json> --out gated.json [--pack ...] [--policy policy.json]
45
+ [--trust-store trust-store.json] [--include-ask] [--json]
46
+ trustdex gate-codex <config.toml> --out gated.toml [--pack ...] [--policy policy.json]
47
+ [--trust-store trust-store.json] [--include-ask]
48
+
49
+ trustdex provenance github <owner/repo> [--json]
50
+ trustdex provenance github-release <owner/repo> [--json]
51
+ trustdex provenance npm <package> [--json]
52
+ trustdex provenance mcp <namespace/server> [--json]
53
+ trustdex trust-source github <owner/repo> --publisher <name> --out trust-store.json
54
+ trustdex trust-source github-release <owner/repo> --publisher <name> --out trust-store.json
55
+ trustdex trust-source npm <package> --publisher <name> --out trust-store.json
56
+ [--artifact downloaded-package.tgz]
57
+ trustdex trust-source mcp <namespace/server> --publisher <name> --out trust-store.json
58
+ [--artifact downloaded-package]
59
+
60
+ trustdex approve <mcp.json> --private trustdex-private.pem --dir .trustdex/review
61
+ [--pack ...] [--policy policy.json] [--trust-store trust-store.json]
62
+ trustdex recheck <mcp.json> --review-dir .trustdex/review --public trustdex-public.pem [--json]
63
+
64
+ trustdex snapshot <mcp.json> --out snapshot.json [--pack ...]
65
+ [--policy policy.json] [--trust-store trust-store.json]
66
+
67
+ trustdex diff <before.json> <after.json> [--json]
68
+ trustdex policy <strict|official-first|development>
69
+
70
+ trustdex keygen --private trustdex-private.pem --public trustdex-public.pem
71
+ trustdex sign <snapshot.json> --private trustdex-private.pem --out trust-record.json
72
+ [--policy policy.json] [--source-ref <ref>]
73
+ trustdex verify <trust-record.json> --public trustdex-public.pem
74
+ [--snapshot snapshot.json] [--policy policy.json]
75
+
76
+ Most commands are local-only. The provenance and trust-source commands perform explicit
77
+ network lookups to GitHub or npm when you invoke them.`);
78
+ }
79
+
80
+ async function readJson(file) {
81
+ return JSON.parse(await fs.readFile(file, 'utf8'));
82
+ }
83
+
84
+ async function readJsonIfExists(file, fallback = null) {
85
+ try {
86
+ return await readJson(file);
87
+ } catch (error) {
88
+ if (error?.code === 'ENOENT') return fallback;
89
+ throw error;
90
+ }
91
+ }
92
+
93
+ async function writeFileSafe(file, content, options = {}) {
94
+ const directory = path.dirname(path.resolve(file));
95
+ await fs.mkdir(directory, { recursive: true });
96
+ await fs.writeFile(file, content, options);
97
+ }
98
+
99
+ function getFlag(args, name) {
100
+ const index = args.indexOf(name);
101
+ return index >= 0 ? args[index + 1] : null;
102
+ }
103
+
104
+ function hasFlag(args, name) {
105
+ return args.includes(name);
106
+ }
107
+
108
+ function label(action) {
109
+ if (action === 'allow') return 'ALLOW';
110
+ if (action === 'ask') return 'ASK';
111
+ return 'BLOCK';
112
+ }
113
+
114
+ async function loadPolicy(args) {
115
+ const pack = getPolicyPack(getFlag(args, '--pack') || 'strict');
116
+ const policyPath = getFlag(args, '--policy');
117
+ return policyPath ? mergePolicy(pack, await readJson(policyPath)) : pack;
118
+ }
119
+
120
+ async function loadTrustStore(args) {
121
+ const file = getFlag(args, '--trust-store');
122
+ return file ? parseTrustStore(await readJson(file)) : null;
123
+ }
124
+
125
+ function printResults(results, asJson = false) {
126
+ if (asJson) {
127
+ console.log(JSON.stringify(results, null, 2));
128
+ return;
129
+ }
130
+
131
+ for (const result of results) {
132
+ console.log(`${label(result.action).padEnd(5)} ${result.name}`);
133
+ console.log(` source: ${JSON.stringify(result.source)}`);
134
+ if (result.fingerprint) console.log(` fingerprint: ${result.fingerprint}`);
135
+ if (result.provenance) console.log(` provenance: ${JSON.stringify(result.provenance)}`);
136
+ if (result.signals?.length) console.log(` signals: ${result.signals.join(', ')}`);
137
+ for (const reason of result.reasons || []) console.log(` - ${reason}`);
138
+ }
139
+ }
140
+
141
+ function setInspectExitCode(results) {
142
+ if (results.some((result) => result.action === 'block')) process.exitCode = 2;
143
+ else if (results.some((result) => result.action === 'ask')) process.exitCode = 1;
144
+ }
145
+
146
+ async function evaluateMcp(configPath, args) {
147
+ const config = await readJson(configPath);
148
+ const inspected = inspectMcpConfig(config);
149
+ const store = await loadTrustStore(args);
150
+ const withProvenance = store ? attachProvenanceAll(inspected, store) : inspected;
151
+ const policy = await loadPolicy(args);
152
+ return {
153
+ config,
154
+ policy,
155
+ store,
156
+ results: evaluateAll(withProvenance, policy)
157
+ };
158
+ }
159
+
160
+ async function evaluateCodex(configPath, args) {
161
+ const text = await fs.readFile(configPath, 'utf8');
162
+ const config = parseCodexMcpToml(text);
163
+ const inspected = inspectMcpConfig(config);
164
+ const store = await loadTrustStore(args);
165
+ const withProvenance = store ? attachProvenanceAll(inspected, store) : inspected;
166
+ const policy = await loadPolicy(args);
167
+ return {
168
+ text,
169
+ config,
170
+ policy,
171
+ store,
172
+ results: evaluateAll(withProvenance, policy)
173
+ };
174
+ }
175
+
176
+ async function inspectSingle(item, args) {
177
+ const results = evaluateAll([item], await loadPolicy(args));
178
+ printResults(results, hasFlag(args, '--json'));
179
+ setInspectExitCode(results);
180
+ }
181
+
182
+ function printObservation(observation, asJson) {
183
+ if (asJson) {
184
+ console.log(JSON.stringify(observation, null, 2));
185
+ return;
186
+ }
187
+ console.log(`${observation.adapter.toUpperCase()} ${observation.subject}`);
188
+ console.log(`Publisher hint: ${observation.publisherHint || 'none'}`);
189
+ console.log(`Evidence: ${observation.evidence.kind} ${observation.evidence.reference}`);
190
+ console.log('Observation only. Nothing is trusted until you explicitly create a trust-store claim.');
191
+ }
192
+
193
+ async function lookupProvenance(adapter, subject) {
194
+ if (adapter === 'github') return inspectGitHubRepository(subject);
195
+ if (adapter === 'github-release') return inspectGitHubReleaseSignature(subject);
196
+ if (adapter === 'npm') return inspectNpmPackage(subject);
197
+ if (adapter === 'mcp') return inspectMcpRegistryServer(subject);
198
+ throw new Error('provenance adapter must be "github", "github-release", "npm", or "mcp".');
199
+ }
200
+
201
+ function printChanges(changes) {
202
+ for (const change of changes) {
203
+ console.log(`${change.type}: ${change.name}${change.signals ? ` (${change.signals.join(', ')})` : ''}`);
204
+ }
205
+ }
206
+
207
+ async function main() {
208
+ const args = process.argv.slice(2);
209
+ const command = args[0];
210
+
211
+ if (!command || command === '-h' || command === '--help') {
212
+ usage();
213
+ return;
214
+ }
215
+
216
+ if (command === '--version' || command === '-V') {
217
+ console.log(VERSION);
218
+ return;
219
+ }
220
+
221
+ if (command === 'policy') {
222
+ console.log(JSON.stringify(getPolicyPack(args[1] || 'strict'), null, 2));
223
+ return;
224
+ }
225
+
226
+ if (command === 'inspect') {
227
+ const configPath = args[1];
228
+ if (!configPath) throw new Error('inspect requires a path to an MCP JSON file.');
229
+
230
+ const { results } = await evaluateMcp(configPath, args);
231
+ printResults(results, hasFlag(args, '--json'));
232
+ setInspectExitCode(results);
233
+ return;
234
+ }
235
+
236
+ if (command === 'inspect-skill') {
237
+ const skillPath = args[1];
238
+ if (!skillPath) throw new Error('inspect-skill requires a path to SKILL.md.');
239
+ await inspectSingle(inspectSkillText(await fs.readFile(skillPath, 'utf8'), skillPath), args);
240
+ return;
241
+ }
242
+
243
+ if (command === 'inspect-plugin') {
244
+ const pluginPath = args[1];
245
+ if (!pluginPath) throw new Error('inspect-plugin requires a path to plugin.json.');
246
+ await inspectSingle(inspectPluginManifest(await readJson(pluginPath), pluginPath), args);
247
+ return;
248
+ }
249
+
250
+ if (command === 'inspect-codex') {
251
+ const configPath = args[1];
252
+ if (!configPath) throw new Error('inspect-codex requires a path to Codex config.toml.');
253
+ const { results } = await evaluateCodex(configPath, args);
254
+ printResults(results, hasFlag(args, '--json'));
255
+ setInspectExitCode(results);
256
+ return;
257
+ }
258
+
259
+ if (command === 'gate-codex') {
260
+ const configPath = args[1];
261
+ const out = getFlag(args, '--out');
262
+ if (!configPath || !out) throw new Error('gate-codex requires <config.toml> and --out <gated.toml>.');
263
+ const { text, results } = await evaluateCodex(configPath, args);
264
+ const gated = gateCodexToml(text, results, { includeAsk: hasFlag(args, '--include-ask') });
265
+ await writeFileSafe(out, gated);
266
+ console.log(`Wrote ${out}`);
267
+ const allowed = results.filter((result) => result.action === 'allow').map((result) => result.name);
268
+ const review = results.filter((result) => result.action === 'ask').map((result) => result.name);
269
+ const blocked = results.filter((result) => result.action === 'block').map((result) => result.name);
270
+ console.log(`Allowed: ${allowed.length ? allowed.join(', ') : 'none'}`);
271
+ if (review.length) console.log(`Needs review: ${review.join(', ')}`);
272
+ if (blocked.length) console.log(`Blocked: ${blocked.join(', ')}`);
273
+ if (review.length && !hasFlag(args, '--include-ask')) process.exitCode = 1;
274
+ return;
275
+ }
276
+
277
+ if (command === 'provenance') {
278
+ const adapter = args[1];
279
+ const subject = args[2];
280
+ if (!adapter || !subject) throw new Error('provenance requires <github|github-release|npm|mcp> <subject>.');
281
+ printObservation(await lookupProvenance(adapter, subject), hasFlag(args, '--json'));
282
+ return;
283
+ }
284
+
285
+ if (command === 'trust-source') {
286
+ const adapter = args[1];
287
+ const subject = args[2];
288
+ const publisher = getFlag(args, '--publisher');
289
+ const out = getFlag(args, '--out');
290
+ if (!adapter || !subject || !publisher || !out) {
291
+ throw new Error('trust-source requires <github|github-release|npm|mcp> <subject>, --publisher <name>, and --out <trust-store.json>.');
292
+ }
293
+
294
+ const observation = await lookupProvenance(adapter, subject);
295
+ const artifactPath = getFlag(args, '--artifact');
296
+ const artifactVerification = artifactPath
297
+ ? verifyArtifactBytes(observation, await fs.readFile(artifactPath))
298
+ : null;
299
+ const claim = provenanceObservationToClaim(observation, publisher, { artifactVerification });
300
+ const existing = await readJsonIfExists(out, { version: 1, claims: [] });
301
+ const store = upsertTrustStoreClaim(existing, claim);
302
+ parseTrustStore(store);
303
+ await writeFileSafe(out, `${JSON.stringify(store, null, 2)}\n`);
304
+ console.log(`Trusted ${claim.type} ${claim.subject} as "${claim.publisher}" using ${claim.evidence.kind} evidence.`);
305
+ if (artifactVerification) {
306
+ console.log(`Verified artifact integrity: ${artifactVerification.algorithm} ${artifactVerification.digest}`);
307
+ }
308
+ console.log(`Wrote ${out}`);
309
+ return;
310
+ }
311
+
312
+ if (command === 'gate') {
313
+ const configPath = args[1];
314
+ const out = getFlag(args, '--out');
315
+ if (!configPath || !out) throw new Error('gate requires <mcp.json> and --out <gated.json>.');
316
+
317
+ const { config, results } = await evaluateMcp(configPath, args);
318
+ const gated = gateMcpConfig(config, results, { includeAsk: hasFlag(args, '--include-ask') });
319
+ await writeFileSafe(out, `${JSON.stringify(gated.config, null, 2)}\n`);
320
+
321
+ if (hasFlag(args, '--json')) console.log(JSON.stringify(gated.summary, null, 2));
322
+ else {
323
+ console.log(`Wrote ${out}`);
324
+ console.log(`Exposed ${gated.summary.exposed}/${gated.summary.total} MCP servers.`);
325
+ if (gated.summary.needsReview.length) console.log(`Needs review: ${gated.summary.needsReview.join(', ')}`);
326
+ if (gated.summary.blocked.length) console.log(`Blocked: ${gated.summary.blocked.join(', ')}`);
327
+ }
328
+
329
+ if (gated.summary.needsReview.length && !hasFlag(args, '--include-ask')) process.exitCode = 1;
330
+ return;
331
+ }
332
+
333
+ if (command === 'approve') {
334
+ const configPath = args[1];
335
+ const privatePath = getFlag(args, '--private');
336
+ const reviewDir = getFlag(args, '--dir');
337
+ if (!configPath || !privatePath || !reviewDir) {
338
+ throw new Error('approve requires <mcp.json>, --private <key.pem>, and --dir <review-directory>.');
339
+ }
340
+
341
+ const { policy, store, results } = await evaluateMcp(configPath, args);
342
+ if (results.some((result) => result.action === 'block')) {
343
+ throw new Error('Refusing to approve a configuration containing BLOCK decisions. Fix the policy or source first.');
344
+ }
345
+
346
+ const snapshot = createSnapshot(results);
347
+ const record = createTrustRecord(snapshot, await fs.readFile(privatePath, 'utf8'), {
348
+ policy,
349
+ sourceRef: path.resolve(configPath)
350
+ });
351
+
352
+ await writeFileSafe(path.join(reviewDir, 'snapshot.json'), `${JSON.stringify(snapshot, null, 2)}\n`);
353
+ await writeFileSafe(path.join(reviewDir, 'trust-record.json'), `${JSON.stringify(record, null, 2)}\n`);
354
+ await writeFileSafe(path.join(reviewDir, 'effective-policy.json'), `${JSON.stringify(policy, null, 2)}\n`);
355
+ if (store) {
356
+ await writeFileSafe(path.join(reviewDir, 'trust-store.json'), `${JSON.stringify(store, null, 2)}\n`);
357
+ }
358
+
359
+ console.log(`APPROVED ${configPath}`);
360
+ console.log(`Digest: ${snapshot.digest}`);
361
+ console.log(`Review bundle: ${reviewDir}`);
362
+ return;
363
+ }
364
+
365
+ if (command === 'recheck') {
366
+ const configPath = args[1];
367
+ const reviewDir = getFlag(args, '--review-dir');
368
+ const publicPath = getFlag(args, '--public');
369
+ if (!configPath || !reviewDir || !publicPath) {
370
+ throw new Error('recheck requires <mcp.json>, --review-dir <directory>, and --public <key.pem>.');
371
+ }
372
+
373
+ const baseline = await readJson(path.join(reviewDir, 'snapshot.json'));
374
+ const record = await readJson(path.join(reviewDir, 'trust-record.json'));
375
+ const policy = await readJson(path.join(reviewDir, 'effective-policy.json'));
376
+ const storedTrustStore = path.join(reviewDir, 'trust-store.json');
377
+ const trustStoreExists = await readJsonIfExists(storedTrustStore, null);
378
+
379
+ const evalArgs = [...args, '--policy', path.join(reviewDir, 'effective-policy.json')];
380
+ if (trustStoreExists && !getFlag(args, '--trust-store')) {
381
+ evalArgs.push('--trust-store', storedTrustStore);
382
+ }
383
+ const { results } = await evaluateMcp(configPath, evalArgs);
384
+ const current = createSnapshot(results);
385
+ const verdict = assessReview({
386
+ record,
387
+ publicKey: await fs.readFile(publicPath, 'utf8'),
388
+ baseline,
389
+ current,
390
+ policy
391
+ });
392
+
393
+ if (hasFlag(args, '--json')) {
394
+ console.log(JSON.stringify({ ...verdict, currentDigest: current.digest, approvedDigest: baseline.digest }, null, 2));
395
+ } else if (verdict.status === 'approved') {
396
+ console.log('APPROVED: current state matches the signed review.');
397
+ console.log(`Digest: ${current.digest}`);
398
+ } else if (verdict.status === 'needs-review') {
399
+ console.log('NEEDS_REVIEW: trust-relevant state changed.');
400
+ printChanges(verdict.changes);
401
+ } else {
402
+ console.log(`INVALID: ${verdict.reason}`);
403
+ }
404
+
405
+ if (verdict.status === 'needs-review') process.exitCode = 1;
406
+ if (verdict.status === 'invalid') process.exitCode = 2;
407
+ return;
408
+ }
409
+
410
+ if (command === 'snapshot') {
411
+ const configPath = args[1];
412
+ const out = getFlag(args, '--out');
413
+ if (!configPath || !out) throw new Error('snapshot requires <mcp.json> and --out <snapshot.json>.');
414
+
415
+ const { results } = await evaluateMcp(configPath, args);
416
+ const snapshot = createSnapshot(results);
417
+ await writeFileSafe(out, `${JSON.stringify(snapshot, null, 2)}\n`);
418
+ console.log(`Wrote ${out}\nDigest: ${snapshot.digest}`);
419
+ return;
420
+ }
421
+
422
+ if (command === 'diff') {
423
+ const beforePath = args[1];
424
+ const afterPath = args[2];
425
+ if (!beforePath || !afterPath) throw new Error('diff requires <before.json> <after.json>.');
426
+
427
+ const changes = diffSnapshots(await readJson(beforePath), await readJson(afterPath));
428
+ if (hasFlag(args, '--json')) console.log(JSON.stringify(changes, null, 2));
429
+ else if (!changes.length) console.log('No trust-relevant changes detected.');
430
+ else printChanges(changes);
431
+
432
+ if (changes.some((change) =>
433
+ ['server-added', 'signals-added', 'decision-changed', 'source-changed', 'provenance-changed', 'fingerprint-changed'].includes(change.type)
434
+ )) process.exitCode = 1;
435
+ return;
436
+ }
437
+
438
+ if (command === 'keygen') {
439
+ const privatePath = getFlag(args, '--private');
440
+ const publicPath = getFlag(args, '--public');
441
+ if (!privatePath || !publicPath) throw new Error('keygen requires --private <file> and --public <file>.');
442
+
443
+ const keys = generateTrustKeyPair();
444
+ await writeFileSafe(privatePath, keys.privateKey, { mode: 0o600 });
445
+ await writeFileSafe(publicPath, keys.publicKey);
446
+ console.log(`Wrote ${privatePath} and ${publicPath}`);
447
+ return;
448
+ }
449
+
450
+ if (command === 'sign') {
451
+ const snapshotPath = args[1];
452
+ const privatePath = getFlag(args, '--private');
453
+ const out = getFlag(args, '--out');
454
+ if (!snapshotPath || !privatePath || !out) {
455
+ throw new Error('sign requires <snapshot.json>, --private <key.pem>, and --out <record.json>.');
456
+ }
457
+
458
+ const policyPath = getFlag(args, '--policy');
459
+ const record = createTrustRecord(
460
+ await readJson(snapshotPath),
461
+ await fs.readFile(privatePath, 'utf8'),
462
+ {
463
+ policy: policyPath ? await readJson(policyPath) : null,
464
+ sourceRef: getFlag(args, '--source-ref')
465
+ }
466
+ );
467
+ await writeFileSafe(out, `${JSON.stringify(record, null, 2)}\n`);
468
+ console.log(`Wrote ${out}`);
469
+ return;
470
+ }
471
+
472
+ if (command === 'verify') {
473
+ const recordPath = args[1];
474
+ const publicPath = getFlag(args, '--public');
475
+ if (!recordPath || !publicPath) throw new Error('verify requires <record.json> and --public <key.pem>.');
476
+
477
+ const snapshotPath = getFlag(args, '--snapshot');
478
+ const policyPath = getFlag(args, '--policy');
479
+ const verdict = verifyTrustRecord(
480
+ await readJson(recordPath),
481
+ await fs.readFile(publicPath, 'utf8'),
482
+ {
483
+ snapshot: snapshotPath ? await readJson(snapshotPath) : null,
484
+ policy: policyPath ? await readJson(policyPath) : null
485
+ }
486
+ );
487
+ console.log(verdict.valid ? 'VALID' : `INVALID: ${verdict.reason}`);
488
+ if (!verdict.valid) process.exitCode = 2;
489
+ return;
490
+ }
491
+
492
+ throw new Error(`Unknown command: ${command}`);
493
+ }
494
+
495
+ main().catch((error) => {
496
+ console.error(`trustdex: ${error.message}`);
497
+ process.exitCode = 2;
498
+ });
@@ -0,0 +1,43 @@
1
+ # Architecture
2
+
3
+ TrustDex is a dependency-free Node.js CLI that separates observation, policy, gating, and review evidence.
4
+
5
+ ## Data flow
6
+
7
+ 1. An adapter reads an MCP JSON file, a focused Codex TOML subset, a skill, or a plugin manifest.
8
+ 2. Inspection normalizes the source and emits observable signals. Secret values are not retained in reports.
9
+ 3. Optional trust-store claims attach explicit provenance evidence.
10
+ 4. The policy engine produces ALLOW, ASK, or BLOCK.
11
+ 5. A gate emits only the approved configuration.
12
+ 6. Snapshots and signed trust records make later drift reviewable.
13
+
14
+ The gate is the enforcement boundary. Reports alone do not prevent an agent from loading an unreviewed extension.
15
+
16
+ ## Modules
17
+
18
+ | Area | Responsibility |
19
+ | --- | --- |
20
+ | src/inspect.mjs | MCP source classification, redaction, fingerprinting, and signals |
21
+ | src/codex-config.mjs | Focused fail-closed Codex MCP parsing and section filtering |
22
+ | src/extensions.mjs | Skill and plugin manifest inspection |
23
+ | src/provenance.mjs | Explicit local trust-store claims |
24
+ | src/provenance-online.mjs | User-invoked GitHub, npm, and MCP Registry observations plus local artifact-digest verification |
25
+ | src/policy.mjs | Monotonic policy evaluation; signals may raise severity |
26
+ | src/gate.mjs | Output only approved MCP entries |
27
+ | src/snapshot.mjs | Deterministic trust-relevant state and drift |
28
+ | src/trust-record.mjs | Ed25519 review-record signing and verification |
29
+ | src/review.mjs | Signed-baseline comparison |
30
+
31
+ ## Security invariants
32
+
33
+ - Inspection must not execute the inspected extension.
34
+ - Secret values must not be printed or stored in snapshots.
35
+ - Unknown evidence must not become trust.
36
+ - A blocking signal must not be lowered by provenance.
37
+ - Unsupported security-relevant syntax must fail closed.
38
+ - Network lookup is explicit and observation is not approval.
39
+ - ALLOW is a policy result, not a safety certification.
40
+
41
+ ## Dependency policy
42
+
43
+ Runtime code currently uses only Node.js built-ins. A new dependency requires an issue explaining why the feature cannot reasonably be implemented with the platform, the package's maintenance and security posture, and the effect on the supply-chain boundary.
@@ -0,0 +1,53 @@
1
+ # Releasing TrustDex
2
+
3
+ TrustDex uses two small GitHub Actions workflows:
4
+
5
+ - `release.yml` creates a GitHub tag and release after tests pass.
6
+ - `publish.yml` publishes that exact tag to npm.
7
+
8
+ This split keeps GitHub release creation separate from registry credentials.
9
+
10
+ ## First npm publication
11
+
12
+ npm trusted publishing can only be configured after a package already exists in the npm registry.
13
+
14
+ For the first publication:
15
+
16
+ 1. Make sure the `trustdex` package name is still available on npm.
17
+ 2. Create an npm granular access token that is allowed to publish the package and store it as the repository secret `NPM_TOKEN`.
18
+ 3. Run **Create GitHub Release** for the current package version, such as `v0.4.0`.
19
+ 4. Run **Publish Package to npmjs** for the same tag only after the GitHub release succeeds.
20
+ 5. Delete the publishing token after the first successful publication.
21
+
22
+ Do not paste npm credentials into issues, pull requests, source files, logs, or chat.
23
+
24
+ ## Switch to npm trusted publishing
25
+
26
+ After the package exists, configure npm trusted publishing for:
27
+
28
+ - GitHub user/organization: `grigent`
29
+ - repository: `trustdex`
30
+ - workflow filename: `publish.yml`
31
+ - allowed action: direct `npm publish`
32
+
33
+ The workflow grants only `contents: read` and `id-token: write`. npm can then authenticate the GitHub-hosted job with OIDC instead of a long-lived write token.
34
+
35
+ Once trusted publishing works, remove the `NPM_TOKEN` repository secret. The workflow deliberately continues to use the standard `npm publish` command so npm can select OIDC authentication.
36
+
37
+ ## Provenance
38
+
39
+ The repository is public and the package is public, so GitHub-hosted publishing can emit npm provenance. The workflow also passes `--provenance` explicitly.
40
+
41
+ Provenance links the published artifact to its source/build environment. It does not prove that the package contains no malicious behavior.
42
+
43
+ ## Version checklist
44
+
45
+ Before each release:
46
+
47
+ 1. update `package.json` version
48
+ 2. move the corresponding changelog section out of `Unreleased`
49
+ 3. make sure CI is green
50
+ 4. run the release workflow with exactly `v<package version>`
51
+ 5. run the publish workflow for that exact release tag
52
+
53
+ The publish workflow checks out the tag SHA rather than the latest branch head so the npm artifact cannot silently include later commits.
@@ -0,0 +1,33 @@
1
+ # Roadmap
2
+
3
+ TrustDex is early-stage. This roadmap communicates direction and is not a release commitment.
4
+
5
+ ## Now: harden the boundary
6
+
7
+ - expand adversarial fixtures for MCP JSON, Codex TOML, skills, and plugin manifests
8
+ - verify that all human-readable output remains free of secret values
9
+ - improve cross-platform source and path normalization
10
+ - document stable policy and snapshot schema rules
11
+ - keep CI, CodeQL, and release automation reproducible and reviewable
12
+
13
+ ## Next: prove integrity
14
+
15
+ - verify immutable package artifacts when registries expose cryptographic hashes
16
+ - connect registry observations to exact package versions and repository identities
17
+ - publish checksums or stronger provenance for TrustDex releases
18
+ - add fixtures for ownership transfer, deleted releases, yanked packages, and registry drift
19
+
20
+ ## Later: expand interoperability
21
+
22
+ - add adapters for additional agent runtimes and registries
23
+ - publish a compatibility matrix backed by tests
24
+ - define machine-readable decision output for CI and policy tooling
25
+ - explore organization-managed trust stores and review-key rotation
26
+
27
+ ## Adoption goals
28
+
29
+ The project is seeking pilot users willing to share non-sensitive feedback about false positives, false negatives, configuration formats, and maintainer workflow. Usage claims will be documented only when they can be verified; stars, branding, and package names are never treated as security evidence.
30
+
31
+ ## How to help
32
+
33
+ Open a focused issue describing the use case and redacted configuration shape. For security-sensitive proposals, include the expected trust boundary and bypass cases.
@@ -0,0 +1,47 @@
1
+ # Threat model
2
+
3
+ TrustDex addresses one narrow question:
4
+
5
+ > **Should an agent be allowed to see and invoke this extension under the user's policy?**
6
+
7
+ ## In scope
8
+
9
+ - unknown or unapproved MCP sources
10
+ - floating package versions that can change without review
11
+ - source changes between reviewed snapshots
12
+ - provenance assertions changing or disappearing
13
+ - newly observed shell, filesystem, network, install, wildcard-tool, or secret-environment signals
14
+ - unsafe tool exposure caused by policy regressions
15
+ - substitution of package bytes that do not match reviewed registry integrity metadata
16
+ - configured package versions drifting from an integrity-verified artifact version
17
+ - tampering with a reviewed snapshot when a signed trust record is available
18
+
19
+ ## Trust boundaries
20
+
21
+ TrustDex trusts:
22
+
23
+ - the local host enough to read its own policy/configuration
24
+ - the user's explicitly configured policy
25
+ - provenance evidence only to the extent that the trust-store maintainer reviewed it
26
+ - a public key only when the user obtained that key through a trusted channel
27
+
28
+ TrustDex does **not** treat a familiar project name, star count, package name, or logo as provenance.
29
+
30
+ ## Out of scope
31
+
32
+ - proving arbitrary third-party code is non-malicious
33
+ - sandboxing tool execution
34
+ - detecting every prompt-injection technique
35
+ - vulnerability scanning of all transitive dependencies
36
+ - proving that integrity-matching package bytes are non-malicious
37
+ - independently proving publisher ownership from a local trust-store claim
38
+ - protecting a fully compromised host
39
+ - protecting a stolen signing private key
40
+
41
+ ## Important failure mode
42
+
43
+ A valid signature proves that a trust record was signed by the corresponding private key and that its bound snapshot digest has not changed. It does not prove that the original review was correct.
44
+
45
+ ## Design principle
46
+
47
+ TrustDex should never convert missing evidence into trust. Unknown provenance is a policy input, not a reason to guess.