@martinloop/mcp 0.2.7 → 0.3.1

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 (64) hide show
  1. package/README.md +49 -104
  2. package/dist/package-version.d.ts +1 -1
  3. package/dist/package-version.js +1 -1
  4. package/dist/prompts.d.ts +1 -1
  5. package/dist/resources.d.ts +1 -1
  6. package/dist/resources.js +2 -2
  7. package/dist/server-validation.d.ts +1 -0
  8. package/dist/server-validation.js +8 -0
  9. package/dist/server.js +87 -9
  10. package/dist/tools/doctor.d.ts +39 -1
  11. package/dist/tools/doctor.js +68 -9
  12. package/dist/tools/eval.js +3 -2
  13. package/dist/tools/get-run.d.ts +3 -0
  14. package/dist/tools/get-run.js +3 -1
  15. package/dist/tools/get-verification-results.d.ts +3 -0
  16. package/dist/tools/get-verification-results.js +3 -1
  17. package/dist/tools/plan.js +4 -2
  18. package/dist/tools/pr-tools.js +2 -1
  19. package/dist/tools/preflight.d.ts +41 -1
  20. package/dist/tools/preflight.js +74 -19
  21. package/dist/tools/run-dossier.d.ts +3 -0
  22. package/dist/tools/run-dossier.js +5 -2
  23. package/dist/tools/run-loop.d.ts +7 -2
  24. package/dist/tools/run-loop.js +67 -35
  25. package/dist/tools/run-store.js +67 -15
  26. package/dist/tools/tool-errors.js +1 -1
  27. package/dist/tools/tool-support.d.ts +8 -3
  28. package/dist/tools/tool-support.js +61 -18
  29. package/dist/tools/workflow-governance.d.ts +19 -3
  30. package/dist/tools/workflow-governance.js +107 -55
  31. package/dist/vendor/adapters/claude-cli.d.ts +45 -3
  32. package/dist/vendor/adapters/claude-cli.js +465 -45
  33. package/dist/vendor/adapters/cli-bridge.d.ts +46 -0
  34. package/dist/vendor/adapters/cli-bridge.js +147 -38
  35. package/dist/vendor/adapters/codex-launcher.d.ts +76 -0
  36. package/dist/vendor/adapters/codex-launcher.js +538 -0
  37. package/dist/vendor/adapters/index.d.ts +3 -2
  38. package/dist/vendor/adapters/index.js +3 -2
  39. package/dist/vendor/adapters/openai-compatible.d.ts +19 -4
  40. package/dist/vendor/adapters/openai-compatible.js +50 -19
  41. package/dist/vendor/adapters/runtime-support.d.ts +3 -0
  42. package/dist/vendor/adapters/runtime-support.js +9 -1
  43. package/dist/vendor/adapters/stub-direct-provider.js +3 -0
  44. package/dist/vendor/adapters/verifier-only.d.ts +2 -0
  45. package/dist/vendor/adapters/verifier-only.js +11 -4
  46. package/dist/vendor/contracts/index.d.ts +39 -0
  47. package/dist/vendor/contracts/index.js +2 -0
  48. package/dist/vendor/core/context-integrity.js +28 -3
  49. package/dist/vendor/core/grounding.d.ts +1 -0
  50. package/dist/vendor/core/grounding.js +6 -2
  51. package/dist/vendor/core/index.d.ts +24 -3
  52. package/dist/vendor/core/index.js +113 -21
  53. package/dist/vendor/core/leash.js +85 -8
  54. package/dist/vendor/core/persistence/index.d.ts +2 -0
  55. package/dist/vendor/core/persistence/index.js +1 -0
  56. package/dist/vendor/core/persistence/integrity.d.ts +38 -0
  57. package/dist/vendor/core/persistence/integrity.js +248 -0
  58. package/dist/vendor/core/persistence/store.d.ts +7 -0
  59. package/dist/vendor/core/persistence/store.js +25 -1
  60. package/dist/vendor/core/policy.d.ts +9 -0
  61. package/dist/workflow-state.d.ts +9 -0
  62. package/dist/workflow-state.js +46 -3
  63. package/package.json +2 -2
  64. package/server.json +2 -2
@@ -13,7 +13,7 @@ export { runContextIntegrityPrecheck } from "./context-integrity.js";
13
13
  // ─── Prompt packet compiler ──────────────────────────────────────────────────
14
14
  export { compilePromptPacket } from "./compiler.js";
15
15
  // ─── Persistence (RunStore, LedgerEvent, FileRunStore) ──────────────────────
16
- export { createFileRunStore, makeLedgerEvent, readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot } from "./persistence/index.js";
16
+ export { createFileRunStore, makeLedgerEvent, readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot, resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./persistence/index.js";
17
17
  export { compileAndPersistContext } from "./persistence/index.js";
18
18
  /**
19
19
  * Admission gate — must pass before any attempt is executed.
@@ -101,6 +101,7 @@ export async function runMartin(input) {
101
101
  projectId: input.projectId,
102
102
  task: input.task,
103
103
  budget: input.budget,
104
+ ...(input.receiptScope ? { receiptScope: input.receiptScope } : {}),
104
105
  ...(input.teamId ? { teamId: input.teamId } : {}),
105
106
  ...(input.metadata ? { metadata: input.metadata } : {})
106
107
  }, { now: now(), idFactory });
@@ -270,8 +271,20 @@ export async function runMartin(input) {
270
271
  // GATHER → ADMIT: run admission control before executing
271
272
  currentPhase = "ADMIT";
272
273
  // T05: Context Integrity Pre-gate — blocks authority inversion / injection before reasoning
273
- const contextPrecheck = await runContextIntegrityPrecheck(loop.loopId, loop.attempts.length + 1, runDir(resolveRunsRoot(), loop.loopId), {
274
+ //
275
+ // Untrusted "tool output" re-entering the loop is verifier command output from prior
276
+ // attempts (e.g. test runners echoing attacker-controlled strings). Pull that text from
277
+ // already-persisted verification.completed events so the gate scans what actually
278
+ // re-enters subsequent prompts, matching the documented "tool output / test output" scope.
279
+ const priorVerifierOutput = loop.events
280
+ .filter((event) => event.type === "verification.completed")
281
+ .flatMap((event) => event.payload.steps ?? [])
282
+ .map((step) => step.detail)
283
+ .filter((detail) => Boolean(detail))
284
+ .join("\n---\n");
285
+ const contextPrecheck = await runContextIntegrityPrecheck(loop.loopId, loop.attempts.length + 1, runDir(resolveActiveRunsRoot(input.store), loop.loopId), {
274
286
  userPrompt: distilled.focus,
287
+ toolOutput: priorVerifierOutput || undefined,
275
288
  history: loop.attempts.map(a => a.summary).join("\n")
276
289
  });
277
290
  if (contextPrecheck.verdict === "context_poisoning_block") {
@@ -411,13 +424,18 @@ export async function runMartin(input) {
411
424
  },
412
425
  previousAttempts: loop.attempts
413
426
  };
414
- const rollbackBoundary = await captureRollbackBoundary({
415
- repoRoot: request.context.repoRoot,
416
- capturedAt: attemptStartedAt
417
- });
427
+ const tracksWorkspaceMutations = request.context.repoRoot !== undefined &&
428
+ executingAdapter.metadata.capabilities?.workspaceMutations !== false;
429
+ const rollbackBoundary = tracksWorkspaceMutations
430
+ ? await captureRollbackBoundary({
431
+ repoRoot: request.context.repoRoot,
432
+ capturedAt: attemptStartedAt
433
+ })
434
+ : undefined;
418
435
  const result = await executingAdapter.execute(request);
419
436
  const attemptCompletedAt = now();
420
437
  const compiledContext = compilePromptPacket(request);
438
+ const verification = normalizeVerificationOutcome(result);
421
439
  // PATCH → VERIFY
422
440
  currentPhase = "VERIFY";
423
441
  let failure = result.status === "failed"
@@ -444,7 +462,16 @@ export async function runMartin(input) {
444
462
  actualUsd: roundUsd(loop.cost.actualUsd + getUsageUsd(result.usage)),
445
463
  avoidedUsd: loop.cost.avoidedUsd,
446
464
  tokensIn: loop.cost.tokensIn + result.usage.tokensIn,
447
- tokensOut: loop.cost.tokensOut + result.usage.tokensOut
465
+ tokensOut: loop.cost.tokensOut + result.usage.tokensOut,
466
+ ...(result.usage.estimatedUsd !== undefined
467
+ ? {
468
+ estimatedUsd: roundUsd((loop.cost.estimatedUsd ?? loop.cost.actualUsd) + result.usage.estimatedUsd)
469
+ }
470
+ : {}),
471
+ provenance: getUsageProvenance(result.usage),
472
+ ...(result.usage.providerSettlement
473
+ ? { providerSettlement: result.usage.providerSettlement }
474
+ : {})
448
475
  },
449
476
  updatedAt: attemptCompletedAt
450
477
  };
@@ -503,11 +530,14 @@ export async function runMartin(input) {
503
530
  }
504
531
  loop = appendLoopEvent(loop, {
505
532
  type: "verification.completed",
506
- lifecycleState: result.verification.passed ? "completed" : "verifying",
533
+ lifecycleState: verification.passed ? "completed" : "verifying",
507
534
  payload: {
508
535
  attemptId,
509
- passed: result.verification.passed,
510
- summary: result.verification.summary
536
+ attemptIndex: currentAttemptIndex,
537
+ passed: verification.passed,
538
+ summary: verification.summary,
539
+ ...(verification.steps?.length ? { steps: verification.steps } : {}),
540
+ ...(verification.warnings?.length ? { warnings: verification.warnings } : {})
511
541
  }
512
542
  }, { now: attemptCompletedAt, idFactory });
513
543
  const costState = evaluateCostGovernor({
@@ -521,7 +551,8 @@ export async function runMartin(input) {
521
551
  payload: {
522
552
  actualUsd: loop.cost.actualUsd,
523
553
  remainingBudgetUsd: costState.remainingBudgetUsd,
524
- pressure: costState.pressure
554
+ pressure: costState.pressure,
555
+ provenance: getUsageProvenance(result.usage)
525
556
  }
526
557
  }, { now: now(), idFactory });
527
558
  if (input.store) {
@@ -534,6 +565,7 @@ export async function runMartin(input) {
534
565
  });
535
566
  await input.store.writeAttemptArtifacts(loop.loopId, currentAttemptIndex, {
536
567
  compiledContext,
568
+ verification,
537
569
  ...(rollbackBoundary ? { rollbackBoundary } : {})
538
570
  });
539
571
  await input.store.appendLedger(loop.loopId, makeLedgerEvent({
@@ -546,7 +578,13 @@ export async function runMartin(input) {
546
578
  kind: "verification.completed",
547
579
  runId: loop.loopId,
548
580
  attemptIndex: currentAttemptIndex,
549
- payload: { passed: result.verification.passed, summary: result.verification.summary }
581
+ payload: {
582
+ attemptId,
583
+ passed: verification.passed,
584
+ summary: verification.summary,
585
+ ...(verification.steps?.length ? { steps: verification.steps } : {}),
586
+ ...(verification.warnings?.length ? { warnings: verification.warnings } : {})
587
+ }
550
588
  }));
551
589
  await input.store.appendLedger(loop.loopId, makeLedgerEvent({
552
590
  kind: "budget.settled",
@@ -557,10 +595,13 @@ export async function runMartin(input) {
557
595
  estimatedUsd: result.usage.estimatedUsd,
558
596
  tokensIn: result.usage.tokensIn,
559
597
  tokensOut: result.usage.tokensOut,
598
+ cachedInputTokens: result.usage.cachedInputTokens,
599
+ reasoningTokensOut: result.usage.reasoningTokensOut,
560
600
  provenance: getUsageProvenance(result.usage),
561
601
  transport: getAdapterTransport(executingAdapter),
562
602
  providerId: executingAdapter.metadata.providerId,
563
603
  model: executingAdapter.metadata.model,
604
+ providerSettlement: result.usage.providerSettlement,
564
605
  patchCost: settlement.patchCost,
565
606
  verificationCost: settlement.verificationCost,
566
607
  varianceUsd: settlement.varianceUsd,
@@ -568,16 +609,20 @@ export async function runMartin(input) {
568
609
  }
569
610
  }));
570
611
  }
571
- const changedFiles = resolveChangedFiles(result, request.context.repoRoot);
612
+ const changedFiles = tracksWorkspaceMutations
613
+ ? resolveChangedFiles(result, request.context.repoRoot)
614
+ : [];
572
615
  // Evidence is only reliable when the adapter explicitly reported files OR git actually
573
616
  // returned a non-empty list. A repoRoot alone is insufficient — git may fail (e.g. not
574
617
  // a git repo) and silently return [], which would falsely trigger no_code_change.
575
618
  const changedFileEvidenceAvailable = result.execution?.changedFiles !== undefined || changedFiles.length > 0;
619
+ const isVerifierOnlyAdapter = executingAdapter.adapterId === "direct:verifier:verify-only";
620
+ const patchTruthCountsEdits = !isVerifyOnly && !isVerifierOnlyAdapter && changedFileEvidenceAvailable;
576
621
  if (isVerifyOnly && changedFiles.length > 0) {
577
622
  const patchDecision = evaluatePatchDecision({
578
- verificationPassed: result.verification.passed,
623
+ verificationPassed: verification.passed,
579
624
  previousVerifierScore,
580
- verifierScore: result.verification.passed ? 1 : 0,
625
+ verifierScore: verification.passed ? 1 : 0,
581
626
  scopeViolationCount: changedFiles.length,
582
627
  changedFileCount: changedFiles.length,
583
628
  diffNovelty: 1,
@@ -842,12 +887,12 @@ export async function runMartin(input) {
842
887
  let patchDecision;
843
888
  if (result.status === "completed") {
844
889
  patchDecision = evaluatePatchDecision({
845
- verificationPassed: result.verification.passed,
890
+ verificationPassed: verification.passed,
846
891
  previousVerifierScore,
847
- verifierScore: result.verification.passed ? 1 : 0,
892
+ verifierScore: verification.passed ? 1 : 0,
848
893
  groundingViolationCount: groundingScanResult?.violations.length ?? 0,
849
- changedFileCount: !isVerifyOnly && changedFileEvidenceAvailable ? changedFiles.length : undefined,
850
- diffNovelty: !isVerifyOnly && changedFileEvidenceAvailable ? (changedFiles.length > 0 ? 1 : 0) : undefined,
894
+ changedFileCount: patchTruthCountsEdits ? changedFiles.length : undefined,
895
+ diffNovelty: patchTruthCountsEdits ? (changedFiles.length > 0 ? 1 : 0) : undefined,
851
896
  diffStats: result.execution?.diffStats,
852
897
  costUsd: getUsageUsd(result.usage),
853
898
  summary: result.summary
@@ -897,10 +942,10 @@ export async function runMartin(input) {
897
942
  }
898
943
  else {
899
944
  await input.store.appendLedger(loop.loopId, makeLedgerEvent({
900
- kind: result.verification.passed ? "attempt.kept" : "attempt.discarded",
945
+ kind: verification.passed ? "attempt.kept" : "attempt.discarded",
901
946
  runId: loop.loopId,
902
947
  attemptIndex: currentAttemptIndex,
903
- payload: { reason: result.verification.summary }
948
+ payload: { reason: verification.summary }
904
949
  }));
905
950
  }
906
951
  }
@@ -1145,6 +1190,9 @@ function createBudgetSettlement(input) {
1145
1190
  usd: 0,
1146
1191
  provenance: "unavailable"
1147
1192
  },
1193
+ ...(input.usage.providerSettlement
1194
+ ? { providerSettlement: input.usage.providerSettlement }
1195
+ : {}),
1148
1196
  totalActualUsd,
1149
1197
  preflightEstimateUsd: input.estimate.estimatedAttemptCostUsd,
1150
1198
  varianceUsd: roundUsd(totalActualUsd - input.estimate.estimatedAttemptCostUsd),
@@ -1185,6 +1233,50 @@ function getLastVerifierScore(loop) {
1185
1233
  }
1186
1234
  return 0;
1187
1235
  }
1236
+ function truncateVerificationDetail(text, maxLength) {
1237
+ return text.length <= maxLength ? text : `${text.slice(0, maxLength - 1)}…`;
1238
+ }
1239
+ function normalizeVerificationOutcome(result) {
1240
+ const warnings = [...(result.verification.warnings ?? [])];
1241
+ const contradiction = detectVerificationContradiction(result);
1242
+ if (contradiction && !warnings.includes(contradiction)) {
1243
+ warnings.push(contradiction);
1244
+ }
1245
+ return {
1246
+ passed: result.verification.passed,
1247
+ summary: result.verification.summary,
1248
+ ...(result.verification.steps?.length ? { steps: result.verification.steps } : {}),
1249
+ ...(warnings.length ? { warnings } : {})
1250
+ };
1251
+ }
1252
+ function detectVerificationContradiction(result) {
1253
+ if (!result.verification.passed) {
1254
+ return undefined;
1255
+ }
1256
+ const sources = [result.summary, result.failure?.message]
1257
+ .filter((value) => typeof value === "string" && value.trim().length > 0);
1258
+ const contradictionPatterns = [
1259
+ /createprocessasuserw failed:\s*\d+/iu,
1260
+ /\bverifier not run\b/iu,
1261
+ /\bfailed before verifier\b/iu,
1262
+ /\bnever launched\b/iu,
1263
+ /\bfailed to launch\b/iu,
1264
+ /\bcould not launch\b/iu
1265
+ ];
1266
+ for (const source of sources) {
1267
+ const match = contradictionPatterns.find((pattern) => pattern.test(source));
1268
+ if (!match) {
1269
+ continue;
1270
+ }
1271
+ const excerpt = truncateVerificationDetail(source.trim().replace(/\s+/gu, " "), 220);
1272
+ return `Adapter output reported a tool-launch problem before MartinLoop ran its own verifier: ${excerpt}`;
1273
+ }
1274
+ return undefined;
1275
+ }
1276
+ function resolveActiveRunsRoot(store) {
1277
+ const configuredRunsRoot = store?.runsRoot?.trim();
1278
+ return configuredRunsRoot && configuredRunsRoot.length > 0 ? configuredRunsRoot : resolveRunsRoot();
1279
+ }
1188
1280
  function toPatchDecisionArtifact(decision) {
1189
1281
  return {
1190
1282
  decision: decision.decision,
@@ -1,20 +1,51 @@
1
1
  import { isAbsolute, relative, resolve } from "node:path";
2
2
  const BLOCKED_PATTERNS = [
3
- /(^|\s)rm\s+-rf(\s|$)/u,
3
+ /(^|[\s;&|`(])rm\s+-rf(\s|$)/iu,
4
4
  /git\s+reset\s+--hard/iu,
5
5
  /git\s+clean\s+-fd/iu,
6
6
  /curl\b[^\n|]*\|\s*(sh|bash)/iu,
7
7
  /wget\b[^\n|]*\|\s*(sh|bash)/iu,
8
- /(^|\s)sudo(\s|$)/u,
9
- /(^|\s)mkfs(\.|\s|$)/u,
10
- /(^|\s)dd\s+if=/u,
8
+ /(^|[\s;&|`(])sudo(\s|$)/iu,
9
+ /(^|[\s;&|`(])mkfs(\.|\s|$)/iu,
10
+ /(^|[\s;&|`(])dd\s+if=/iu,
11
11
  /(shutdown|reboot)(\s|$)/iu,
12
- /:\(\)\s*\{\s*:\|:&\s*\}\s*;\s*:/u,
12
+ /:\(\)\s*\{\s*:\|:&\s*\}\s*;\s*:/iu,
13
13
  /chmod\s+-R\s+777\s+\//iu,
14
14
  /(kubectl|docker)\s+.*\b(delete|prune|rm)\b/iu,
15
15
  /ssh\s+/iu,
16
- /scp\s+/iu
16
+ /scp\s+/iu,
17
+ // bash/sh -c "<destructive command>" wrappers — flags the wrapper shape directly,
18
+ // since a naive regex on the outer command misses the destructive inner command.
19
+ /\b(?:bash|sh|zsh)\s+-c\s+["'`]/iu,
20
+ // recursive directory deletion via `find ... -delete` / `find ... -exec rm`
21
+ /\bfind\s+\S+(?:\s+-[a-z-]+(?:\s+\S+)?)*\s+-delete\b/iu,
22
+ /\bfind\s+\S+(?:\s+-[a-z-]+(?:\s+\S+)?)*\s+-exec\s+rm\b/iu,
23
+ // scripting-language recursive deletion one-liners
24
+ /\bshutil\.rmtree\s*\(/iu,
25
+ /\bos\.(?:remove|rmdir|removedirs|unlink)\s*\(/iu,
26
+ /\.(?:rm|rmdir|unlink)(?:Sync)?\s*\(/iu,
27
+ /\brimraf\s*\(/iu
17
28
  ];
29
+ /**
30
+ * Detects `rm` invocations that combine recursive + force flags regardless of
31
+ * flag ordering/grouping (`-rf`, `-fr`, `-r -f`, `--recursive --force`),
32
+ * absolute-path invocation (`/bin/rm`, `/usr/bin/rm`), case, or `${IFS}`-style
33
+ * shell obfuscation — all of which slip past the literal `rm\s+-rf` pattern.
34
+ */
35
+ function commandContainsDestructiveRemoval(command) {
36
+ const normalized = command.replace(/\$\{?IFS\}?/giu, " ").toLowerCase();
37
+ const rmInvocation = /(?:^|[\s;&|`(])(?:\/(?:usr\/(?:local\/)?)?s?bin\/)?rm\s+([^\n;|`]+)/giu;
38
+ let match;
39
+ while ((match = rmInvocation.exec(normalized)) !== null) {
40
+ const args = match[1] ?? "";
41
+ const hasRecursive = /(?:^|\s)-[a-z]*r[a-z]*(?:\s|$)|--recursive\b/u.test(args);
42
+ const hasForce = /(?:^|\s)-[a-z]*f[a-z]*(?:\s|$)|--force\b/u.test(args);
43
+ if (hasRecursive && hasForce) {
44
+ return true;
45
+ }
46
+ }
47
+ return false;
48
+ }
18
49
  const SECRET_PATTERNS = [
19
50
  {
20
51
  kind: "secret_value",
@@ -28,7 +59,52 @@ const SECRET_PATTERNS = [
28
59
  },
29
60
  {
30
61
  kind: "secret_value",
31
- pattern: /\bghp_[A-Za-z0-9_]{8,}\b/gu,
62
+ pattern: /\bghp_[A-Za-z0-9_]{16,}\b/gu,
63
+ replacement: "[REDACTED_SECRET]"
64
+ },
65
+ {
66
+ kind: "secret_value",
67
+ pattern: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/gu,
68
+ replacement: "[REDACTED_SECRET]"
69
+ },
70
+ {
71
+ kind: "secret_value",
72
+ pattern: /\b(?:gho|ghu|ghs|ghr)_[A-Za-z0-9_]{16,}\b/gu,
73
+ replacement: "[REDACTED_SECRET]"
74
+ },
75
+ {
76
+ kind: "secret_value",
77
+ pattern: /\bAKIA[0-9A-Z]{16}\b/gu,
78
+ replacement: "[REDACTED_SECRET]"
79
+ },
80
+ {
81
+ kind: "secret_value",
82
+ pattern: /\b(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY)\s*[:=]\s*[^\s"'`]+/giu,
83
+ replacement: "AWS_SECRET_ACCESS_KEY=[REDACTED_SECRET]"
84
+ },
85
+ {
86
+ kind: "secret_value",
87
+ pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/giu,
88
+ replacement: "[REDACTED_SECRET]"
89
+ },
90
+ {
91
+ kind: "secret_value",
92
+ pattern: /\bAIza[0-9A-Za-z_-]{30,}\b/gu,
93
+ replacement: "[REDACTED_SECRET]"
94
+ },
95
+ {
96
+ kind: "secret_value",
97
+ pattern: /-----BEGIN(?:\s+[A-Z0-9]+)*\s+PRIVATE KEY-----[\s\S]*?-----END(?:\s+[A-Z0-9]+)*\s+PRIVATE KEY-----/gu,
98
+ replacement: "[REDACTED_SECRET]"
99
+ },
100
+ {
101
+ kind: "secret_value",
102
+ pattern: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/gu,
103
+ replacement: "[REDACTED_SECRET]"
104
+ },
105
+ {
106
+ kind: "secret_value",
107
+ pattern: /\b(?:api[_-]?key|secret|password|passwd|token)\s*[:=]\s*["']?[A-Za-z0-9_\-/+=]{12,}["']?/giu,
32
108
  replacement: "[REDACTED_SECRET]"
33
109
  }
34
110
  ];
@@ -55,7 +131,8 @@ export function evaluateVerificationLeash(task) {
55
131
  ...((task.verificationStack ?? []).map((step) => step.command))
56
132
  ].filter(Boolean);
57
133
  const profile = resolveExecutionProfile(task);
58
- const blockedCommands = commands.filter((command) => BLOCKED_PATTERNS.some((pattern) => pattern.test(command)));
134
+ const blockedCommands = commands.filter((command) => BLOCKED_PATTERNS.some((pattern) => pattern.test(command)) ||
135
+ commandContainsDestructiveRemoval(command));
59
136
  const violations = blockedCommands.map((command) => ({
60
137
  kind: "command_blocked",
61
138
  command,
@@ -1,5 +1,7 @@
1
1
  export { makeLedgerEvent } from "./ledger.js";
2
2
  export type { LedgerEvent, LedgerEventDraft, LedgerEventKind } from "./ledger.js";
3
+ export { resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./integrity.js";
4
+ export type { ReceiptIntegrityChainEntry, StoredReceiptIntegrityMaterial } from "./integrity.js";
3
5
  export { artifactDir, createFileRunStore, resolveRunsRoot, runDir } from "./store.js";
4
6
  export type { AttemptArtifacts, RunContract, RunStore } from "./store.js";
5
7
  export { readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile } from "./runs-reader.js";
@@ -1,4 +1,5 @@
1
1
  export { makeLedgerEvent } from "./ledger.js";
2
+ export { resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./integrity.js";
2
3
  export { artifactDir, createFileRunStore, resolveRunsRoot, runDir } from "./store.js";
3
4
  export { readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile } from "./runs-reader.js";
4
5
  export { compileAndPersistContext } from "./compiler.js";
@@ -0,0 +1,38 @@
1
+ import type { LoopRecord, ReceiptIntegritySummary, ReceiptScope } from "../../contracts/index.js";
2
+ export interface ReceiptIntegrityChainEntry {
3
+ index: number;
4
+ eventId?: string;
5
+ type?: string;
6
+ kind?: string;
7
+ timestamp?: string;
8
+ prevHash: string;
9
+ entryHash: string;
10
+ }
11
+ export interface StoredReceiptIntegrityMaterial {
12
+ schemaVersion: "martin.receipt-integrity.v1";
13
+ runId: string;
14
+ keyId: string;
15
+ signedAt: string;
16
+ scope?: ReceiptScope;
17
+ loopRecordSha256: string;
18
+ ledgerSha256: string;
19
+ ledgerHeadHash: string;
20
+ entryCount: number;
21
+ chain: ReceiptIntegrityChainEntry[];
22
+ signatureHmacSha256: string;
23
+ }
24
+ export declare function writeReceiptIntegrityMaterial(input: {
25
+ runId: string;
26
+ runsRoot: string;
27
+ loopRecord: LoopRecord;
28
+ ledgerEntries: unknown[];
29
+ scope?: ReceiptScope;
30
+ signedAt?: string;
31
+ }): Promise<StoredReceiptIntegrityMaterial | undefined>;
32
+ export declare function verifyReceiptIntegrityFromFiles(input: {
33
+ runId: string;
34
+ runsRoot: string;
35
+ loopRecordPath: string;
36
+ ledgerPath: string;
37
+ }): Promise<ReceiptIntegritySummary>;
38
+ export declare function resolveReceiptIntegrityPath(runsRoot: string, runId: string): string;
@@ -0,0 +1,248 @@
1
+ import { createHash, createHmac, randomBytes } from "node:crypto";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ const RECEIPT_INTEGRITY_SCHEMA_VERSION = "martin.receipt-integrity.v1";
6
+ const RECEIPT_INTEGRITY_KEY_DIR_MODE = 0o700;
7
+ const RECEIPT_INTEGRITY_KEY_FILE_MODE = 0o600;
8
+ export async function writeReceiptIntegrityMaterial(input) {
9
+ const signedAt = input.signedAt ?? new Date().toISOString();
10
+ const keyMaterial = await ensureReceiptIntegrityKey(input.runsRoot, input.runId);
11
+ if (!keyMaterial) {
12
+ return undefined;
13
+ }
14
+ const [key, keyId] = keyMaterial;
15
+ const chain = buildReceiptIntegrityChain(input.ledgerEntries);
16
+ const loopRecordRaw = serializeStoredJson(input.loopRecord);
17
+ const ledgerRaw = serializeStoredJsonl(input.ledgerEntries);
18
+ const materialBase = {
19
+ schemaVersion: RECEIPT_INTEGRITY_SCHEMA_VERSION,
20
+ runId: input.runId,
21
+ keyId,
22
+ signedAt,
23
+ ...(input.scope ? { scope: input.scope } : {}),
24
+ loopRecordSha256: sha256(loopRecordRaw),
25
+ ledgerSha256: sha256(ledgerRaw),
26
+ ledgerHeadHash: chain.at(-1)?.entryHash ?? "root",
27
+ entryCount: chain.length,
28
+ chain
29
+ };
30
+ const material = {
31
+ ...materialBase,
32
+ signatureHmacSha256: createReceiptIntegritySignature(key, materialBase)
33
+ };
34
+ await mkdir(join(input.runsRoot, input.runId), { recursive: true });
35
+ await writeFile(resolveReceiptIntegrityPath(input.runsRoot, input.runId), serializeStoredJson(material), "utf8");
36
+ return material;
37
+ }
38
+ export async function verifyReceiptIntegrityFromFiles(input) {
39
+ const integrityPath = resolveReceiptIntegrityPath(input.runsRoot, input.runId);
40
+ const [rawMaterial, rawLoopRecord, rawLedger, key] = await Promise.all([
41
+ readFile(integrityPath, "utf8").catch(() => null),
42
+ readFile(input.loopRecordPath, "utf8").catch(() => null),
43
+ readFile(input.ledgerPath, "utf8").catch(() => null),
44
+ readReceiptIntegrityKey(input.runsRoot, input.runId).catch(() => null)
45
+ ]);
46
+ if (!rawMaterial || !rawLoopRecord || rawLedger === null || !key) {
47
+ return {
48
+ state: "unsigned",
49
+ reason: "Receipt integrity material is missing for this run.",
50
+ warnings: ["Receipts are local-only and unsigned; trust claims are unavailable."]
51
+ };
52
+ }
53
+ let material;
54
+ try {
55
+ material = JSON.parse(rawMaterial);
56
+ }
57
+ catch {
58
+ return {
59
+ state: "tamper_detected",
60
+ reason: "Receipt integrity metadata is unreadable."
61
+ };
62
+ }
63
+ const actualLoopRecordSha256 = sha256(rawLoopRecord);
64
+ if (actualLoopRecordSha256 !== material.loopRecordSha256) {
65
+ return {
66
+ state: "tamper_detected",
67
+ keyId: material.keyId,
68
+ signedAt: material.signedAt,
69
+ loopRecordSha256: material.loopRecordSha256,
70
+ ledgerSha256: material.ledgerSha256,
71
+ ledgerHeadHash: material.ledgerHeadHash,
72
+ entryCount: material.entryCount,
73
+ reason: "loop_record_hash_mismatch"
74
+ };
75
+ }
76
+ const actualLedgerSha256 = sha256(rawLedger);
77
+ if (actualLedgerSha256 !== material.ledgerSha256) {
78
+ return {
79
+ state: "tamper_detected",
80
+ keyId: material.keyId,
81
+ signedAt: material.signedAt,
82
+ loopRecordSha256: material.loopRecordSha256,
83
+ ledgerSha256: material.ledgerSha256,
84
+ ledgerHeadHash: material.ledgerHeadHash,
85
+ entryCount: material.entryCount,
86
+ reason: "ledger_hash_mismatch"
87
+ };
88
+ }
89
+ let parsedLedgerEntries;
90
+ try {
91
+ parsedLedgerEntries = rawLedger
92
+ .split(/\r?\n/u)
93
+ .map((line) => line.trim())
94
+ .filter(Boolean)
95
+ .map((line) => JSON.parse(line));
96
+ }
97
+ catch {
98
+ return {
99
+ state: "tamper_detected",
100
+ keyId: material.keyId,
101
+ signedAt: material.signedAt,
102
+ loopRecordSha256: material.loopRecordSha256,
103
+ ledgerSha256: material.ledgerSha256,
104
+ ledgerHeadHash: material.ledgerHeadHash,
105
+ entryCount: material.entryCount,
106
+ reason: "ledger_entry_parse_error"
107
+ };
108
+ }
109
+ const actualChain = buildReceiptIntegrityChain(parsedLedgerEntries);
110
+ if (actualChain.length !== material.chain.length) {
111
+ return {
112
+ state: "tamper_detected",
113
+ keyId: material.keyId,
114
+ signedAt: material.signedAt,
115
+ loopRecordSha256: material.loopRecordSha256,
116
+ ledgerSha256: material.ledgerSha256,
117
+ ledgerHeadHash: material.ledgerHeadHash,
118
+ entryCount: material.entryCount,
119
+ reason: "ledger_entry_count_mismatch"
120
+ };
121
+ }
122
+ for (let index = 0; index < actualChain.length; index += 1) {
123
+ const expected = material.chain[index];
124
+ const actual = actualChain[index];
125
+ if (!expected ||
126
+ !actual ||
127
+ expected.entryHash !== actual.entryHash ||
128
+ expected.prevHash !== actual.prevHash) {
129
+ return {
130
+ state: "tamper_detected",
131
+ keyId: material.keyId,
132
+ signedAt: material.signedAt,
133
+ loopRecordSha256: material.loopRecordSha256,
134
+ ledgerSha256: material.ledgerSha256,
135
+ ledgerHeadHash: material.ledgerHeadHash,
136
+ entryCount: material.entryCount,
137
+ reason: `ledger_chain_mismatch:${index}`
138
+ };
139
+ }
140
+ }
141
+ const signatureBase = {
142
+ schemaVersion: material.schemaVersion,
143
+ runId: material.runId,
144
+ keyId: material.keyId,
145
+ signedAt: material.signedAt,
146
+ ...(material.scope ? { scope: material.scope } : {}),
147
+ loopRecordSha256: material.loopRecordSha256,
148
+ ledgerSha256: material.ledgerSha256,
149
+ ledgerHeadHash: material.ledgerHeadHash,
150
+ entryCount: material.entryCount,
151
+ chain: material.chain
152
+ };
153
+ const expectedSignature = createReceiptIntegritySignature(key, signatureBase);
154
+ if (expectedSignature !== material.signatureHmacSha256) {
155
+ return {
156
+ state: "tamper_detected",
157
+ keyId: material.keyId,
158
+ signedAt: material.signedAt,
159
+ loopRecordSha256: material.loopRecordSha256,
160
+ ledgerSha256: material.ledgerSha256,
161
+ ledgerHeadHash: material.ledgerHeadHash,
162
+ entryCount: material.entryCount,
163
+ reason: "signature_mismatch"
164
+ };
165
+ }
166
+ return {
167
+ state: "verified",
168
+ keyId: material.keyId,
169
+ signedAt: material.signedAt,
170
+ loopRecordSha256: material.loopRecordSha256,
171
+ ledgerSha256: material.ledgerSha256,
172
+ ledgerHeadHash: material.ledgerHeadHash,
173
+ entryCount: material.entryCount
174
+ };
175
+ }
176
+ export function resolveReceiptIntegrityPath(runsRoot, runId) {
177
+ return join(runsRoot, runId, "receipt-integrity.json");
178
+ }
179
+ function buildReceiptIntegrityChain(entries) {
180
+ let previousHash = "root";
181
+ return entries.map((entry, index) => {
182
+ const normalizedEntry = JSON.stringify(entry);
183
+ const entryHash = sha256(`${previousHash}\n${normalizedEntry}`);
184
+ const candidate = entry;
185
+ const chainEntry = {
186
+ index,
187
+ prevHash: previousHash,
188
+ entryHash
189
+ };
190
+ if (typeof candidate.eventId === "string") {
191
+ chainEntry.eventId = candidate.eventId;
192
+ }
193
+ if (typeof candidate.type === "string") {
194
+ chainEntry.type = candidate.type;
195
+ }
196
+ if (typeof candidate.kind === "string") {
197
+ chainEntry.kind = candidate.kind;
198
+ }
199
+ if (typeof candidate.timestamp === "string") {
200
+ chainEntry.timestamp = candidate.timestamp;
201
+ }
202
+ previousHash = entryHash;
203
+ return chainEntry;
204
+ });
205
+ }
206
+ function createReceiptIntegritySignature(key, material) {
207
+ return createHmac("sha256", key).update(JSON.stringify(material)).digest("hex");
208
+ }
209
+ async function ensureReceiptIntegrityKey(runsRoot, runId) {
210
+ const keyPath = resolveReceiptIntegrityKeyPath(runsRoot, runId);
211
+ const existing = await readFile(keyPath, "utf8").catch(() => null);
212
+ if (existing) {
213
+ const trimmed = existing.trim();
214
+ return [trimmed, sha256(trimmed).slice(0, 16)];
215
+ }
216
+ const generated = randomBytes(32).toString("hex");
217
+ try {
218
+ await mkdir(dirname(keyPath), { recursive: true, mode: RECEIPT_INTEGRITY_KEY_DIR_MODE });
219
+ await writeFile(keyPath, `${generated}\n`, {
220
+ encoding: "utf8",
221
+ mode: RECEIPT_INTEGRITY_KEY_FILE_MODE
222
+ });
223
+ }
224
+ catch {
225
+ return undefined;
226
+ }
227
+ return [generated, sha256(generated).slice(0, 16)];
228
+ }
229
+ async function readReceiptIntegrityKey(runsRoot, runId) {
230
+ const raw = await readFile(resolveReceiptIntegrityKeyPath(runsRoot, runId), "utf8");
231
+ return raw.trim();
232
+ }
233
+ function resolveReceiptIntegrityKeyPath(runsRoot, runId) {
234
+ const rootHash = sha256(runsRoot).slice(0, 16);
235
+ return join(homedir(), ".martin", "receipt-integrity", rootHash, `${runId}.key`);
236
+ }
237
+ function serializeStoredJson(value) {
238
+ return `${JSON.stringify(value, null, 2)}\n`;
239
+ }
240
+ function serializeStoredJsonl(entries) {
241
+ if (entries.length === 0) {
242
+ return "";
243
+ }
244
+ return `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`;
245
+ }
246
+ function sha256(value) {
247
+ return createHash("sha256").update(value).digest("hex");
248
+ }