@martinloop/mcp 0.7.0 → 0.8.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 (73) hide show
  1. package/dist/package-version.d.ts +1 -1
  2. package/dist/package-version.js +1 -1
  3. package/dist/vendor/adapters/claude-cli.d.ts +11 -1
  4. package/dist/vendor/adapters/claude-cli.js +285 -71
  5. package/dist/vendor/adapters/cli-bridge.d.ts +12 -3
  6. package/dist/vendor/adapters/cli-bridge.js +129 -149
  7. package/dist/vendor/adapters/codex-cli.d.ts +3 -3
  8. package/dist/vendor/adapters/codex-cli.js +10 -24
  9. package/dist/vendor/adapters/codex-host.js +127 -132
  10. package/dist/vendor/adapters/index.d.ts +2 -1
  11. package/dist/vendor/adapters/index.js +2 -1
  12. package/dist/vendor/adapters/openai-compatible.js +88 -6
  13. package/dist/vendor/adapters/process-supervisor.d.ts +60 -0
  14. package/dist/vendor/adapters/process-supervisor.js +541 -0
  15. package/dist/vendor/adapters/verifier-only.d.ts +10 -1
  16. package/dist/vendor/adapters/verifier-only.js +43 -1
  17. package/dist/vendor/contracts/external-outcome.d.ts +102 -0
  18. package/dist/vendor/contracts/external-outcome.js +226 -0
  19. package/dist/vendor/contracts/index.d.ts +8 -1
  20. package/dist/vendor/contracts/index.js +2 -0
  21. package/dist/vendor/contracts/swarm-hosted.d.ts +204 -0
  22. package/dist/vendor/contracts/swarm-hosted.js +629 -0
  23. package/dist/vendor/contracts/swarm.d.ts +449 -0
  24. package/dist/vendor/contracts/swarm.js +550 -0
  25. package/dist/vendor/core/external-outcome.d.ts +34 -0
  26. package/dist/vendor/core/external-outcome.js +515 -0
  27. package/dist/vendor/core/grounding.js +1 -1
  28. package/dist/vendor/core/index.d.ts +47 -1
  29. package/dist/vendor/core/index.js +53 -3
  30. package/dist/vendor/core/leash.d.ts +1 -0
  31. package/dist/vendor/core/leash.js +13 -0
  32. package/dist/vendor/core/persistence/integrity.d.ts +4 -0
  33. package/dist/vendor/core/persistence/integrity.js +19 -6
  34. package/dist/vendor/core/persistence/swarm-integrity.d.ts +63 -0
  35. package/dist/vendor/core/persistence/swarm-integrity.js +309 -0
  36. package/dist/vendor/core/swarm/candidates.d.ts +78 -0
  37. package/dist/vendor/core/swarm/candidates.js +810 -0
  38. package/dist/vendor/core/swarm/evidence-projection.d.ts +87 -0
  39. package/dist/vendor/core/swarm/evidence-projection.js +128 -0
  40. package/dist/vendor/core/swarm/evidence.d.ts +40 -0
  41. package/dist/vendor/core/swarm/evidence.js +575 -0
  42. package/dist/vendor/core/swarm/hosted-export.d.ts +48 -0
  43. package/dist/vendor/core/swarm/hosted-export.js +362 -0
  44. package/dist/vendor/core/swarm/hosted-projection.d.ts +5 -0
  45. package/dist/vendor/core/swarm/hosted-projection.js +258 -0
  46. package/dist/vendor/core/swarm/index.d.ts +110 -0
  47. package/dist/vendor/core/swarm/index.js +1060 -0
  48. package/dist/vendor/core/swarm/integration.d.ts +76 -0
  49. package/dist/vendor/core/swarm/integration.js +615 -0
  50. package/dist/vendor/core/swarm/lifecycle.d.ts +72 -0
  51. package/dist/vendor/core/swarm/lifecycle.js +252 -0
  52. package/dist/vendor/core/swarm/live-runtime.d.ts +78 -0
  53. package/dist/vendor/core/swarm/live-runtime.js +1482 -0
  54. package/dist/vendor/core/swarm/live-store.d.ts +99 -0
  55. package/dist/vendor/core/swarm/live-store.js +860 -0
  56. package/dist/vendor/core/swarm/operational-reader.d.ts +17 -0
  57. package/dist/vendor/core/swarm/operational-reader.js +96 -0
  58. package/dist/vendor/core/swarm/operations.d.ts +29 -0
  59. package/dist/vendor/core/swarm/operations.js +128 -0
  60. package/dist/vendor/core/swarm/parent-receipt.d.ts +13 -0
  61. package/dist/vendor/core/swarm/parent-receipt.js +489 -0
  62. package/dist/vendor/core/swarm/receipt-projection.d.ts +26 -0
  63. package/dist/vendor/core/swarm/receipt-projection.js +120 -0
  64. package/dist/vendor/core/swarm/scheduler.d.ts +92 -0
  65. package/dist/vendor/core/swarm/scheduler.js +555 -0
  66. package/dist/vendor/core/swarm/verification.d.ts +70 -0
  67. package/dist/vendor/core/swarm/verification.js +295 -0
  68. package/dist/vendor/core/swarm/workspaces.d.ts +77 -0
  69. package/dist/vendor/core/swarm/workspaces.js +488 -0
  70. package/dist/vendor/core/verified-handoff.d.ts +4 -0
  71. package/dist/vendor/core/verified-handoff.js +4 -0
  72. package/package.json +1 -1
  73. package/server.json +2 -2
@@ -16,6 +16,7 @@ import { verifierActuallyPassed, } from "./verified-handoff.js";
16
16
  export { DEFAULT_AGENT_EXECUTION_INTENT, DEFAULT_PROVIDER_EXECUTION_TIMEOUT_MS, GOVERNED_AUTONOMOUS_BOUNDARY, normalizeProviderExecutionTimeoutMs } from "../contracts/index.js";
17
17
  export { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe, evaluateVerificationLeash, evaluateFilesystemLeash, evaluateChangeApprovalLeash, evaluateSecretLeash, resolveExecutionProfile, redactSecretsFromText, buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations, captureRollbackBoundary, listAttemptChangedFilesSinceBoundary, restoreRollbackBoundary };
18
18
  export { buildContextGraphSnapshot, queryContextGraph } from "./context-graph.js";
19
+ export { ExternalOutcomeContractError, ExternalOutcomePolicyError, hashExternalOutcomeContract, verifyExternalOutcomes, writeExternalOutcomeEvidence, } from "./external-outcome.js";
19
20
  export { createLocalIdentityAuthority, hasIdentityScope, issueIdentityToken, verifyIdentityToken } from "./identity.js";
20
21
  export { compileExecutionPolicy } from "./policy-compiler.js";
21
22
  export { assessTrajectory, decideCircuitBreak } from "./trajectory.js";
@@ -401,6 +402,34 @@ export async function runMartin(input) {
401
402
  }
402
403
  }
403
404
  }
405
+ let parentAbortObserved = false;
406
+ function observeParentAbort() {
407
+ if (parentAbortObserved || input.signal === undefined)
408
+ return;
409
+ parentAbortObserved = true;
410
+ const reason = input.signal.reason instanceof Error
411
+ ? input.signal.reason.message
412
+ : typeof input.signal.reason === "string"
413
+ ? input.signal.reason
414
+ : "parent cancelled governed child run";
415
+ const parentSignal = {
416
+ kind: "human_interrupt",
417
+ schemaVersion: "exit-signal/1",
418
+ runId: loop.loopId,
419
+ reason: `Parent cancellation: ${reason}`,
420
+ requestedAt: now(),
421
+ requestedBy: "parent_swarm"
422
+ };
423
+ observeSignals([parentSignal]);
424
+ activeAttemptController?.abort(input.signal.reason ?? parentSignal);
425
+ }
426
+ if (input.signal?.aborted)
427
+ observeParentAbort();
428
+ else if (input.signal) {
429
+ input.signal.addEventListener("abort", observeParentAbort, { once: true });
430
+ if (input.signal.aborted)
431
+ observeParentAbort();
432
+ }
404
433
  function queueControlDiagnostic(code, details) {
405
434
  if (!input.store)
406
435
  return;
@@ -443,6 +472,7 @@ export async function runMartin(input) {
443
472
  // snapshot. Draining alone leaves a window for new writes during signing.
444
473
  disposeSignalMonitor();
445
474
  runController.abort();
475
+ input.signal?.removeEventListener("abort", observeParentAbort);
446
476
  await Promise.all(pendingControlWrites.splice(0));
447
477
  }
448
478
  // Snapshot of the current run state for eight-exit evaluation
@@ -753,6 +783,7 @@ export async function runMartin(input) {
753
783
  ? { definitionOfDonePreSatisfied: loop.task.definitionOfDonePreSatisfied }
754
784
  : {}),
755
785
  ...(loop.task.repoRoot ? { repoRoot: loop.task.repoRoot } : {}),
786
+ ...(input.store?.runsRoot ? { runsRoot: input.store.runsRoot } : {}),
756
787
  ...(loop.task.allowedPaths ? { allowedPaths: loop.task.allowedPaths } : {}),
757
788
  ...(loop.task.deniedPaths ? { deniedPaths: loop.task.deniedPaths } : {}),
758
789
  ...(loop.task.acceptanceCriteria ? { acceptanceCriteria: loop.task.acceptanceCriteria } : {}),
@@ -766,9 +797,13 @@ export async function runMartin(input) {
766
797
  focus: distilled.focus,
767
798
  remainingBudgetUsd: distilled.constraints.remainingBudgetUsd,
768
799
  remainingIterations: distilled.constraints.remainingIterations,
769
- remainingTokens: distilled.constraints.remainingTokens
800
+ remainingTokens: distilled.constraints.remainingTokens,
801
+ ...(input.verificationExecutionOwner
802
+ ? { verificationExecutionOwner: input.verificationExecutionOwner }
803
+ : {})
770
804
  },
771
- previousAttempts: loop.attempts
805
+ previousAttempts: loop.attempts,
806
+ ...(input.observedUsageGovernor ? { observedUsageGovernor: input.observedUsageGovernor } : {})
772
807
  };
773
808
  const tracksWorkspaceMutations = request.context.repoRoot !== undefined &&
774
809
  executingAdapter.metadata.capabilities?.workspaceMutations !== false;
@@ -996,7 +1031,8 @@ export async function runMartin(input) {
996
1031
  repoRoot: request.context.repoRoot,
997
1032
  changedFiles,
998
1033
  allowedPaths: request.context.allowedPaths,
999
- deniedPaths: request.context.deniedPaths
1034
+ deniedPaths: request.context.deniedPaths,
1035
+ mutationMode: request.context.mutationMode
1000
1036
  });
1001
1037
  if (!filesystemDecision.allowed) {
1002
1038
  const patchDecision = evaluatePatchDecision({
@@ -1412,6 +1448,7 @@ export async function runMartin(input) {
1412
1448
  clearTimeout(wallTimer);
1413
1449
  disposeSignalMonitor();
1414
1450
  runController.abort();
1451
+ input.signal?.removeEventListener("abort", observeParentAbort);
1415
1452
  activeAttemptController?.abort();
1416
1453
  // Flush any diagnostic ledger writes queued from signal callbacks
1417
1454
  await Promise.all(pendingControlWrites);
@@ -1581,6 +1618,9 @@ function resolveChangedFiles(result, repoRoot, rollbackBoundary) {
1581
1618
  return listAttemptChangedFilesSinceBoundary({ repoRoot, boundary: rollbackBoundary });
1582
1619
  }
1583
1620
  function buildPatchDiff(result, changedFiles) {
1621
+ if (result.execution?.patch) {
1622
+ return result.execution.patch;
1623
+ }
1584
1624
  // Use structured diff stats to build a minimal diff header if no raw diff is available
1585
1625
  if (result.execution?.changedFiles?.length) {
1586
1626
  // Build a synthetic diff header from changed file list
@@ -1663,7 +1703,11 @@ function resolveExpectedVerifierBinding(request) {
1663
1703
  return {
1664
1704
  runId: request.loopId,
1665
1705
  workspaceId: request.workspaceId,
1706
+ attemptId: request.attemptId,
1666
1707
  cwd: request.context.repoRoot ?? process.cwd(),
1708
+ ...(request.context.runsRoot ? { runsRoot: request.context.runsRoot } : {}),
1709
+ ...(request.context.executionProfile ? { executionProfile: request.context.executionProfile } : {}),
1710
+ ...(request.context.allowedNetworkDomains?.length ? { allowedNetworkDomains: [...request.context.allowedNetworkDomains] } : {}),
1667
1711
  commands,
1668
1712
  };
1669
1713
  }
@@ -1806,3 +1850,9 @@ export { createDefaultExitPolicy, evaluateExitPolicy, hashProgressState, toLegac
1806
1850
  export { SignalDiagnosticError, createFileExitSignalSource, exitSignalPath, readAllExitSignals, readExitSignal, startExitSignalMonitor, writeExitSignal } from "./exit-signal.js";
1807
1851
  // ─── R4 Delivery ─────────────────────────────────────────────────────────────
1808
1852
  export { cacheMessage, fetchSelectedMessage, getCliInstalledVersion, getMcpInstalledVersion, isCooldownExpired, isDismissed, isNewerVersion, loadDeliveryRecord, parseMessageSelectionResponse, recordDismissed, recordShown, resolveDefaultLedgerPath, saveDeliveryRecord, } from "./delivery/index.js";
1853
+ export { completeDeterministicDemoSwarmRun, createSwarmBudgetLedger, extendSwarmBudgetLease, releaseSwarmBudgetLease, reserveSwarmBudgetLease, selectNextSwarmBatch, settleSwarmBudgetLease, tasksHaveScopeCollision, validateSwarmTaskGraph, runParentSwarmPipeline } from "./swarm/index.js";
1854
+ export { readSwarmOperationalState } from "./swarm/operational-reader.js";
1855
+ export { readSwarmReceiptProjection, verifySwarmReceiptProjection, } from "./swarm/receipt-projection.js";
1856
+ export { buildSwarmShareProjection, readSwarmDossier, verifySwarmEvidence, } from "./swarm/evidence-projection.js";
1857
+ export { buildSwarmHostedEnvelope } from "./swarm/hosted-export.js";
1858
+ export { assertSwarmPathIdentifier } from "./swarm/workspaces.js";
@@ -32,6 +32,7 @@ export declare function evaluateFilesystemLeash(input: {
32
32
  changedFiles: string[];
33
33
  allowedPaths?: string[];
34
34
  deniedPaths?: string[];
35
+ mutationMode?: "edit" | "read_only" | "verify_only";
35
36
  }): SafetyLeashDecision;
36
37
  export declare function evaluateSecretLeash(input: {
37
38
  values: string[];
@@ -52,6 +52,11 @@ function commandContainsDestructiveRemoval(command) {
52
52
  return false;
53
53
  }
54
54
  const SECRET_PATTERNS = [
55
+ {
56
+ kind: "secret_value",
57
+ pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}\b/giu,
58
+ replacement: "Bearer [REDACTED_SECRET]"
59
+ },
55
60
  {
56
61
  kind: "secret_value",
57
62
  pattern: /\bOPENAI_API_KEY\s*=\s*[^\s"'`]+/giu,
@@ -196,6 +201,14 @@ export function evaluateFilesystemLeash(input) {
196
201
  if (!file) {
197
202
  continue;
198
203
  }
204
+ if (input.mutationMode === "read_only") {
205
+ violations.push({
206
+ kind: "path_not_allowed",
207
+ file,
208
+ message: `Read-only execution changed a file: ${file}`
209
+ });
210
+ continue;
211
+ }
199
212
  if ((input.deniedPaths ?? []).some((pattern) => matchesPathPattern(file, pattern))) {
200
213
  violations.push({
201
214
  kind: "path_denied",
@@ -37,3 +37,7 @@ export declare function verifyReceiptIntegrityFromFiles(input: {
37
37
  ledgerPath: string;
38
38
  }): Promise<ReceiptIntegritySummary>;
39
39
  export declare function resolveReceiptIntegrityPath(runsRoot: string, runId: string): string;
40
+ export declare function ensurePrivateIntegrityKey(runsRoot: string, keyFileName: string): Promise<[string, string] | undefined>;
41
+ export declare function readPrivateIntegrityKey(runsRoot: string, keyFileName: string): Promise<string>;
42
+ export declare function resolvePrivateIntegrityKeyPath(runsRoot: string, keyFileName: string): string;
43
+ export declare function createPrivateIntegrityHmac(key: string, material: unknown): string;
@@ -1,5 +1,5 @@
1
1
  import { createHash, createHmac, randomBytes } from "node:crypto";
2
- import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { lstat, mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
5
  const RECEIPT_INTEGRITY_SCHEMA_VERSION = "martin.receipt-integrity.v1";
@@ -208,10 +208,17 @@ function buildReceiptIntegrityChain(entries) {
208
208
  });
209
209
  }
210
210
  function createReceiptIntegritySignature(key, material) {
211
- return createHmac("sha256", key).update(JSON.stringify(material)).digest("hex");
211
+ return createPrivateIntegrityHmac(key, material);
212
212
  }
213
213
  async function ensureReceiptIntegrityKey(runsRoot, runId) {
214
- const keyPath = resolveReceiptIntegrityKeyPath(runsRoot, runId);
214
+ return ensurePrivateIntegrityKey(runsRoot, `${runId}.key`);
215
+ }
216
+ export async function ensurePrivateIntegrityKey(runsRoot, keyFileName) {
217
+ const keyPath = resolvePrivateIntegrityKeyPath(runsRoot, keyFileName);
218
+ const existingStat = await lstat(keyPath).catch(() => undefined);
219
+ if (existingStat?.isSymbolicLink() || (existingStat && !existingStat.isFile())) {
220
+ return undefined;
221
+ }
215
222
  const existing = await readFile(keyPath, "utf8").catch(() => null);
216
223
  if (existing) {
217
224
  const trimmed = existing.trim();
@@ -231,12 +238,18 @@ async function ensureReceiptIntegrityKey(runsRoot, runId) {
231
238
  return [generated, sha256(generated).slice(0, 16)];
232
239
  }
233
240
  async function readReceiptIntegrityKey(runsRoot, runId) {
234
- const raw = await readFile(resolveReceiptIntegrityKeyPath(runsRoot, runId), "utf8");
241
+ return readPrivateIntegrityKey(runsRoot, `${runId}.key`);
242
+ }
243
+ export async function readPrivateIntegrityKey(runsRoot, keyFileName) {
244
+ const raw = await readFile(resolvePrivateIntegrityKeyPath(runsRoot, keyFileName), "utf8");
235
245
  return raw.trim();
236
246
  }
237
- function resolveReceiptIntegrityKeyPath(runsRoot, runId) {
247
+ export function resolvePrivateIntegrityKeyPath(runsRoot, keyFileName) {
238
248
  const rootHash = sha256(runsRoot).slice(0, 16);
239
- return join(resolveReceiptIntegrityRoot(), rootHash, `${runId}.key`);
249
+ return join(resolveReceiptIntegrityRoot(), rootHash, keyFileName);
250
+ }
251
+ export function createPrivateIntegrityHmac(key, material) {
252
+ return createHmac("sha256", key).update(JSON.stringify(material)).digest("hex");
240
253
  }
241
254
  function serializeStoredJson(value) {
242
255
  return `${JSON.stringify(value, null, 2)}\n`;
@@ -0,0 +1,63 @@
1
+ declare const SWARM_RECEIPT_INTEGRITY_SCHEMA_VERSION: "martin.swarm-receipt-integrity.v1";
2
+ export interface SwarmReceiptIntegrityArtifact {
3
+ path: string;
4
+ sha256: string;
5
+ bytes: number;
6
+ }
7
+ export interface StoredSwarmReceiptIntegrityMaterial {
8
+ schemaVersion: typeof SWARM_RECEIPT_INTEGRITY_SCHEMA_VERSION;
9
+ swarmId: string;
10
+ planHash: string;
11
+ receiptSchemaVersion: string;
12
+ receiptSha256: string;
13
+ evidenceIndexSha256: string;
14
+ eventChainHead: string;
15
+ rootBindingSha256: string;
16
+ keyId: string;
17
+ signedAt: string;
18
+ taskVerificationState: "passed" | "failed" | "unknown";
19
+ artifacts: SwarmReceiptIntegrityArtifact[];
20
+ signatureHmacSha256: string;
21
+ }
22
+ export interface SwarmReceiptIntegritySummary {
23
+ state: "verified" | "material_missing" | "tamper_detected";
24
+ taskVerificationState?: "passed" | "failed" | "unknown";
25
+ keyId?: string;
26
+ signedAt?: string;
27
+ receiptSha256?: string;
28
+ evidenceIndexSha256?: string;
29
+ eventChainHead?: string;
30
+ reason?: string;
31
+ }
32
+ export interface SwarmReceiptSealCommitBase {
33
+ schemaVersion: "martin.swarm-receipt-seal.v1";
34
+ swarmId: string;
35
+ planHash: string;
36
+ receiptSha256: string;
37
+ evidenceIndexSha256: string;
38
+ integrityMaterialSha256: string;
39
+ committedAt: string;
40
+ }
41
+ export declare function authenticateSwarmReceiptSealCommit(input: {
42
+ runsRoot: string;
43
+ swarmId: string;
44
+ commit: SwarmReceiptSealCommitBase;
45
+ }): Promise<string>;
46
+ export declare function verifySwarmReceiptSealCommitAuthentication(input: {
47
+ runsRoot: string;
48
+ swarmId: string;
49
+ commit: SwarmReceiptSealCommitBase & {
50
+ commitHmacSha256?: string;
51
+ };
52
+ }): Promise<boolean>;
53
+ export declare function writeSwarmReceiptIntegrityMaterial(input: {
54
+ runsRoot: string;
55
+ swarmId: string;
56
+ signedAt?: string;
57
+ }): Promise<StoredSwarmReceiptIntegrityMaterial | undefined>;
58
+ export declare function verifySwarmReceiptIntegrityFromFiles(input: {
59
+ runsRoot: string;
60
+ swarmId: string;
61
+ }): Promise<SwarmReceiptIntegritySummary>;
62
+ export declare function resolveSwarmReceiptIntegrityPath(runsRoot: string, swarmId: string): string;
63
+ export {};
@@ -0,0 +1,309 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+ import { lstat, open, readFile, realpath } from "node:fs/promises";
3
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { verifyParentSwarmReceipt } from "../swarm/parent-receipt.js";
5
+ import { assertSwarmPathIdentifier } from "../swarm/workspaces.js";
6
+ import { createPrivateIntegrityHmac, ensurePrivateIntegrityKey, readPrivateIntegrityKey } from "./integrity.js";
7
+ const SWARM_RECEIPT_INTEGRITY_SCHEMA_VERSION = "martin.swarm-receipt-integrity.v1";
8
+ export async function authenticateSwarmReceiptSealCommit(input) {
9
+ const runsRoot = await realpath(resolve(input.runsRoot));
10
+ const key = await readPrivateIntegrityKey(runsRoot, keyFileName(input.swarmId));
11
+ if (!key)
12
+ throw new Error("SWARM_SEAL_KEY_UNAVAILABLE");
13
+ return createPrivateIntegrityHmac(key, sealAuthenticationBase(runsRoot, input.commit));
14
+ }
15
+ export async function verifySwarmReceiptSealCommitAuthentication(input) {
16
+ const runsRoot = await realpath(resolve(input.runsRoot));
17
+ const key = await readPrivateIntegrityKey(runsRoot, keyFileName(input.swarmId));
18
+ if (!key || typeof input.commit.commitHmacSha256 !== "string")
19
+ return false;
20
+ const { commitHmacSha256, ...commit } = input.commit;
21
+ const expected = createPrivateIntegrityHmac(key, sealAuthenticationBase(runsRoot, commit));
22
+ return constantTimeHexEqual(commitHmacSha256, expected);
23
+ }
24
+ export async function writeSwarmReceiptIntegrityMaterial(input) {
25
+ const bundle = await resolveCanonicalSwarmIntegrityBundle(input.runsRoot, input.swarmId);
26
+ const receipt = parseParentReceipt(bundle.receiptRaw);
27
+ const evidenceIndex = parseEvidenceIndex(bundle.indexRaw);
28
+ validateArtifactIdentities(input.swarmId, receipt, evidenceIndex, bundle.indexRaw, bundle.eventsRaw);
29
+ const keyMaterial = await ensurePrivateIntegrityKey(bundle.runsRoot, keyFileName(input.swarmId));
30
+ if (!keyMaterial)
31
+ return undefined;
32
+ const [key, keyId] = keyMaterial;
33
+ const artifacts = canonicalArtifactManifest(bundle);
34
+ const base = {
35
+ schemaVersion: SWARM_RECEIPT_INTEGRITY_SCHEMA_VERSION,
36
+ swarmId: input.swarmId,
37
+ planHash: receipt.planHash,
38
+ receiptSchemaVersion: receipt.schemaVersion,
39
+ receiptSha256: receipt.receiptSha256,
40
+ evidenceIndexSha256: sha256(bundle.indexRaw),
41
+ eventChainHead: sha256(bundle.eventsRaw),
42
+ rootBindingSha256: sha256(bundle.runsRoot),
43
+ keyId,
44
+ signedAt: input.signedAt ?? new Date().toISOString(),
45
+ taskVerificationState: receipt.taskVerificationState,
46
+ artifacts
47
+ };
48
+ const material = {
49
+ ...base,
50
+ signatureHmacSha256: createPrivateIntegrityHmac(key, base)
51
+ };
52
+ await writeExactOnce(bundle.integrityPath, serializeJson(material));
53
+ return material;
54
+ }
55
+ export async function verifySwarmReceiptIntegrityFromFiles(input) {
56
+ let bundle;
57
+ try {
58
+ bundle = await resolveCanonicalSwarmIntegrityBundle(input.runsRoot, input.swarmId);
59
+ }
60
+ catch {
61
+ return { state: "material_missing", reason: "canonical_swarm_bundle_missing" };
62
+ }
63
+ const [materialRaw, key] = await Promise.all([
64
+ readFile(bundle.integrityPath, "utf8").catch(() => null),
65
+ readPrivateIntegrityKey(bundle.runsRoot, keyFileName(input.swarmId)).catch(() => null)
66
+ ]);
67
+ if (!materialRaw || !key) {
68
+ return { state: "material_missing", reason: "swarm_receipt_integrity_material_incomplete" };
69
+ }
70
+ let material;
71
+ let receipt;
72
+ let evidenceIndex;
73
+ try {
74
+ material = JSON.parse(materialRaw);
75
+ receipt = parseParentReceipt(bundle.receiptRaw);
76
+ evidenceIndex = parseEvidenceIndex(bundle.indexRaw);
77
+ validateArtifactIdentities(input.swarmId, receipt, evidenceIndex, bundle.indexRaw, bundle.eventsRaw);
78
+ }
79
+ catch {
80
+ return { state: "tamper_detected", reason: "malformed_integrity_or_artifact_material" };
81
+ }
82
+ const actualArtifacts = canonicalArtifactManifest(bundle);
83
+ if (!validMaterialShape(material) || !sameManifest(material.artifacts, actualArtifacts)) {
84
+ return tamper(material, "artifact_manifest_mismatch");
85
+ }
86
+ if (material.rootBindingSha256 !== sha256(bundle.runsRoot)) {
87
+ return tamper(material, "root_binding_mismatch");
88
+ }
89
+ if (material.swarmId !== input.swarmId
90
+ || material.planHash !== receipt.planHash
91
+ || material.receiptSchemaVersion !== receipt.schemaVersion
92
+ || material.receiptSha256 !== receipt.receiptSha256
93
+ || material.evidenceIndexSha256 !== sha256(bundle.indexRaw)
94
+ || material.eventChainHead !== sha256(bundle.eventsRaw)
95
+ || material.taskVerificationState !== receipt.taskVerificationState) {
96
+ return tamper(material, "canonical_binding_mismatch");
97
+ }
98
+ const { signatureHmacSha256, ...base } = material;
99
+ const expected = createPrivateIntegrityHmac(key, base);
100
+ if (!constantTimeHexEqual(signatureHmacSha256, expected)) {
101
+ return tamper(material, "signature_mismatch");
102
+ }
103
+ return {
104
+ state: "verified",
105
+ taskVerificationState: receipt.taskVerificationState,
106
+ keyId: material.keyId,
107
+ signedAt: material.signedAt,
108
+ receiptSha256: material.receiptSha256,
109
+ evidenceIndexSha256: material.evidenceIndexSha256,
110
+ eventChainHead: material.eventChainHead
111
+ };
112
+ }
113
+ export function resolveSwarmReceiptIntegrityPath(runsRoot, swarmId) {
114
+ assertSwarmPathIdentifier(swarmId, "swarm ID");
115
+ return join(runsRoot, "_swarms", swarmId, "evidence", "swarm-receipt-integrity.json");
116
+ }
117
+ async function resolveCanonicalSwarmIntegrityBundle(runsRootInput, swarmId) {
118
+ assertSwarmPathIdentifier(swarmId, "swarm ID");
119
+ const runsRoot = await realpath(resolve(runsRootInput));
120
+ const swarmsRoot = await resolveExactDirectory(runsRoot, join(runsRoot, "_swarms"));
121
+ assertContained(runsRoot, swarmsRoot);
122
+ const swarmRoot = await resolveExactDirectory(swarmsRoot, join(swarmsRoot, swarmId));
123
+ assertContained(swarmsRoot, swarmRoot);
124
+ const evidenceRoot = await resolveExactDirectory(swarmRoot, join(swarmRoot, "evidence"));
125
+ assertContained(swarmRoot, evidenceRoot);
126
+ const receiptPath = await resolveExactRegularFile(evidenceRoot, join(evidenceRoot, "parent-receipt.json"));
127
+ const indexPath = await resolveExactRegularFile(evidenceRoot, join(evidenceRoot, "evidence-index.json"));
128
+ const eventsPath = await resolveExactRegularFile(swarmRoot, join(swarmRoot, "events.jsonl"));
129
+ assertContained(evidenceRoot, receiptPath);
130
+ assertContained(evidenceRoot, indexPath);
131
+ assertContained(swarmRoot, eventsPath);
132
+ const [receiptRaw, indexRaw, eventsRaw] = await Promise.all([
133
+ readFile(receiptPath, "utf8"),
134
+ readFile(indexPath, "utf8"),
135
+ readFile(eventsPath, "utf8")
136
+ ]);
137
+ return {
138
+ runsRoot,
139
+ swarmRoot,
140
+ evidenceRoot,
141
+ integrityPath: join(evidenceRoot, "swarm-receipt-integrity.json"),
142
+ receiptRaw,
143
+ indexRaw,
144
+ eventsRaw
145
+ };
146
+ }
147
+ function canonicalArtifactManifest(bundle) {
148
+ return [
149
+ { path: "evidence/evidence-index.json", sha256: sha256(bundle.indexRaw), bytes: Buffer.byteLength(bundle.indexRaw) },
150
+ { path: "evidence/parent-receipt.json", sha256: sha256(bundle.receiptRaw), bytes: Buffer.byteLength(bundle.receiptRaw) }
151
+ ];
152
+ }
153
+ function parseParentReceipt(raw) {
154
+ const receipt = JSON.parse(raw);
155
+ if (verifyParentSwarmReceipt(receipt).ok !== true) {
156
+ throw new Error("INVALID_PARENT_SWARM_RECEIPT");
157
+ }
158
+ return receipt;
159
+ }
160
+ function parseEvidenceIndex(raw) {
161
+ const value = JSON.parse(raw);
162
+ if (!isRecord(value) || value.schemaVersion !== "martin.swarm-evidence-index.v1") {
163
+ throw new Error("INVALID_SWARM_EVIDENCE_INDEX");
164
+ }
165
+ return value;
166
+ }
167
+ function validateArtifactIdentities(swarmId, receipt, evidenceIndex, indexRaw, eventsRaw) {
168
+ if (receipt.swarmId !== swarmId
169
+ || evidenceIndex.swarmId !== swarmId
170
+ || evidenceIndex.planHash !== receipt.planHash) {
171
+ throw new Error("SWARM_ARTIFACT_IDENTITY_MISMATCH");
172
+ }
173
+ if (receipt.evidenceIndexSha256 !== sha256(indexRaw)
174
+ || stableJson(receipt.evidenceFiles) !== stableJson(evidenceIndex.files)
175
+ || `${receipt.events.map((event) => JSON.stringify(event)).join("\n")}\n` !== eventsRaw) {
176
+ throw new Error("SWARM_ARTIFACT_RELATION_MISMATCH");
177
+ }
178
+ }
179
+ async function resolveExactDirectory(parent, expected) {
180
+ const metadata = await lstat(expected);
181
+ if (metadata.isSymbolicLink() || !metadata.isDirectory())
182
+ throw new Error("SWARM_INTEGRITY_PATH_ALIAS");
183
+ const canonical = await realpath(expected);
184
+ assertContained(parent, canonical);
185
+ if (!sameFilesystemPath(canonical, resolve(expected)))
186
+ throw new Error("SWARM_INTEGRITY_PATH_ALIAS");
187
+ return canonical;
188
+ }
189
+ async function resolveExactRegularFile(parent, expected) {
190
+ const metadata = await lstat(expected);
191
+ if (metadata.isSymbolicLink() || !metadata.isFile())
192
+ throw new Error("SWARM_INTEGRITY_PATH_ALIAS");
193
+ const canonical = await realpath(expected);
194
+ assertContained(parent, canonical);
195
+ if (!sameFilesystemPath(canonical, resolve(expected)))
196
+ throw new Error("SWARM_INTEGRITY_PATH_ALIAS");
197
+ return canonical;
198
+ }
199
+ function sealAuthenticationBase(runsRoot, commit) {
200
+ return {
201
+ purpose: "martin.swarm-receipt-seal-commit.v1",
202
+ rootBindingSha256: sha256(runsRoot),
203
+ ...commit
204
+ };
205
+ }
206
+ function sameFilesystemPath(left, right) {
207
+ return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
208
+ }
209
+ function stableJson(value) {
210
+ return JSON.stringify(canonicalize(value));
211
+ }
212
+ function canonicalize(value) {
213
+ if (Array.isArray(value))
214
+ return value.map(canonicalize);
215
+ if (!isRecord(value))
216
+ return value;
217
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalize(value[key])]));
218
+ }
219
+ function validMaterialShape(value) {
220
+ return value.schemaVersion === SWARM_RECEIPT_INTEGRITY_SCHEMA_VERSION
221
+ && typeof value.signatureHmacSha256 === "string"
222
+ && typeof value.keyId === "string"
223
+ && typeof value.signedAt === "string"
224
+ && Array.isArray(value.artifacts);
225
+ }
226
+ function sameManifest(stored, actual) {
227
+ if (stored.length !== actual.length)
228
+ return false;
229
+ const seen = new Set();
230
+ return stored.every((entry, index) => {
231
+ if (entry.path.includes("..") || entry.path.includes("\\") || seen.has(entry.path))
232
+ return false;
233
+ seen.add(entry.path);
234
+ const expected = actual[index];
235
+ return expected !== undefined
236
+ && entry.path === expected.path
237
+ && entry.sha256 === expected.sha256
238
+ && entry.bytes === expected.bytes;
239
+ });
240
+ }
241
+ function tamper(material, reason) {
242
+ return {
243
+ state: "tamper_detected",
244
+ ...(typeof material.taskVerificationState === "string"
245
+ ? { taskVerificationState: material.taskVerificationState }
246
+ : {}),
247
+ ...(typeof material.keyId === "string" ? { keyId: material.keyId } : {}),
248
+ ...(typeof material.signedAt === "string" ? { signedAt: material.signedAt } : {}),
249
+ reason
250
+ };
251
+ }
252
+ function constantTimeHexEqual(left, right) {
253
+ if (!/^[a-f0-9]{64}$/u.test(left) || !/^[a-f0-9]{64}$/u.test(right))
254
+ return false;
255
+ const leftBytes = Buffer.from(left, "hex");
256
+ const rightBytes = Buffer.from(right, "hex");
257
+ return leftBytes.byteLength === rightBytes.byteLength && timingSafeEqual(leftBytes, rightBytes);
258
+ }
259
+ function keyFileName(swarmId) {
260
+ return `swarm-${swarmId}.key`;
261
+ }
262
+ function assertContained(parent, child) {
263
+ const value = relative(parent, child);
264
+ if (value === "" || (!value.startsWith(`..${sep}`) && value !== ".." && !isAbsolute(value)))
265
+ return;
266
+ throw new Error("SWARM_INTEGRITY_PATH_ESCAPE");
267
+ }
268
+ function serializeJson(value) {
269
+ return `${JSON.stringify(value, null, 2)}\n`;
270
+ }
271
+ async function writeExactOnce(path, contents) {
272
+ const existing = await lstat(path).catch((error) => {
273
+ if (isNodeError(error, "ENOENT"))
274
+ return undefined;
275
+ throw error;
276
+ });
277
+ if (existing?.isSymbolicLink() || (existing && !existing.isFile()))
278
+ throw new Error("UNSAFE_SWARM_INTEGRITY_PATH");
279
+ if (existing) {
280
+ if (await readFile(path, "utf8") === contents)
281
+ return;
282
+ throw new Error("SWARM_INTEGRITY_IDENTITY_CONFLICT");
283
+ }
284
+ let handle;
285
+ try {
286
+ handle = await open(path, "wx", 0o600);
287
+ await handle.writeFile(contents, "utf8");
288
+ await handle.sync();
289
+ }
290
+ catch (error) {
291
+ if (!isNodeError(error, "EEXIST"))
292
+ throw error;
293
+ if (await readFile(path, "utf8") === contents)
294
+ return;
295
+ throw new Error("SWARM_INTEGRITY_IDENTITY_CONFLICT");
296
+ }
297
+ finally {
298
+ await handle?.close();
299
+ }
300
+ }
301
+ function isNodeError(error, code) {
302
+ return error instanceof Error && "code" in error && error.code === code;
303
+ }
304
+ function sha256(value) {
305
+ return createHash("sha256").update(value).digest("hex");
306
+ }
307
+ function isRecord(value) {
308
+ return typeof value === "object" && value !== null && !Array.isArray(value);
309
+ }
@@ -0,0 +1,78 @@
1
+ import { type ReceiptIntegritySummary, type SwarmCandidate, type SwarmPatchAdmission, type SwarmRunRecord } from "../../contracts/index.js";
2
+ import type { SwarmWorkspaceRuntimeHandle } from "./workspaces.js";
3
+ export interface SwarmCandidateGitCommand {
4
+ readonly cwd: string;
5
+ readonly args: readonly string[];
6
+ }
7
+ export interface SwarmCandidateGitResult {
8
+ readonly stdout: Buffer;
9
+ readonly stderr: Buffer;
10
+ }
11
+ export type SwarmCandidateGitRunner = (command: SwarmCandidateGitCommand) => Promise<SwarmCandidateGitResult>;
12
+ export interface PersistedSwarmCandidateArtifact {
13
+ readonly candidate: SwarmCandidate;
14
+ readonly attemptId: string;
15
+ readonly proposalId: string;
16
+ readonly identitySha256: string;
17
+ readonly candidateTreeHash: string;
18
+ readonly patch: Buffer;
19
+ readonly manifest: Buffer;
20
+ readonly manifestSha256: string;
21
+ readonly receiptIntegrity: ReceiptIntegritySummary;
22
+ }
23
+ export interface SwarmProcessClosureEvidence {
24
+ readonly swarmId: string;
25
+ readonly workspaceId: string;
26
+ readonly childRunId: string;
27
+ readonly agentId: string;
28
+ readonly state: "closed";
29
+ readonly completedAt: string;
30
+ }
31
+ /**
32
+ * Implementations must durably and atomically reject duplicate candidate IDs.
33
+ * The caller does not receive cleanup authority until both operations resolve.
34
+ */
35
+ export interface SwarmCandidateArtifactWriter {
36
+ loadProcessClosure(childRunId: string): Promise<SwarmProcessClosureEvidence | undefined>;
37
+ persistCandidate(artifact: PersistedSwarmCandidateArtifact): Promise<void>;
38
+ persistDecision(decision: SwarmPatchAdmission): Promise<void>;
39
+ }
40
+ export interface CaptureSwarmCandidateInput {
41
+ readonly swarmRecord: SwarmRunRecord;
42
+ readonly canonicalRoot: string;
43
+ readonly swarmId: string;
44
+ readonly candidateId: string;
45
+ readonly admissionId: string;
46
+ readonly attemptId: string;
47
+ readonly proposalId: string;
48
+ readonly childRunId: string;
49
+ readonly agentId: string;
50
+ readonly taskIds: readonly string[];
51
+ readonly expectedBaselineCommit: string;
52
+ readonly workspace: SwarmWorkspaceRuntimeHandle;
53
+ readonly childState: "verified" | "stopped" | "needs_review" | "running" | "queued";
54
+ readonly declaredPaths: readonly string[];
55
+ readonly childAllowedPaths: readonly string[];
56
+ readonly inheritedDeniedPaths: readonly string[];
57
+ readonly taskWriteScope: readonly string[];
58
+ readonly runsRoot: string;
59
+ readonly loopRecordPath: string;
60
+ readonly ledgerPath: string;
61
+ readonly artifactWriter: SwarmCandidateArtifactWriter;
62
+ readonly gitRunner?: SwarmCandidateGitRunner;
63
+ readonly now?: () => string;
64
+ /** Test-only race seam executed after validation and before staging. */
65
+ readonly beforeStage?: () => void | Promise<void>;
66
+ }
67
+ export interface CaptureSwarmCandidateResult {
68
+ readonly admission: SwarmPatchAdmission;
69
+ readonly artifact?: PersistedSwarmCandidateArtifact;
70
+ readonly cleanupAuthorized: boolean;
71
+ }
72
+ /**
73
+ * Internal live-runtime bridge to the candidate module's single NUL-safe Git
74
+ * inventory parser. Policy enforcement remains in captureAndAdmitSwarmCandidate.
75
+ */
76
+ export declare function inventorySwarmCandidatePaths(workspacePath: string, gitRunner?: SwarmCandidateGitRunner): Promise<string[]>;
77
+ export declare function captureAndAdmitSwarmCandidate(input: CaptureSwarmCandidateInput): Promise<CaptureSwarmCandidateResult>;
78
+ export declare function verifyPersistedSwarmCandidateArtifact(artifact: PersistedSwarmCandidateArtifact): boolean;