@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
@@ -0,0 +1,575 @@
1
+ import { createHash } from "node:crypto";
2
+ import { link, lstat, open, readdir, readFile, realpath, rm, stat } from "node:fs/promises";
3
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { verifyReceiptIntegrityFromFiles } from "../persistence/integrity.js";
5
+ import { readCanonicalSwarmLiveEvidenceBundle, SwarmLiveStoreError } from "./live-store.js";
6
+ import { assertSwarmPathIdentifier } from "./workspaces.js";
7
+ export const SWARM_EVIDENCE_INDEX_SCHEMA_VERSION = "martin.swarm-evidence-index.v1";
8
+ export async function readAndSealSwarmEvidence(input) {
9
+ assertSwarmPathIdentifier(input.swarmId, "swarm ID");
10
+ const roots = await resolveExistingEvidenceRoots(input.rootDir, input.swarmId);
11
+ const bundle = await readCanonicalSwarmLiveEvidenceBundle({
12
+ directory: roots.swarmRoot,
13
+ swarmId: input.swarmId
14
+ });
15
+ const artifacts = await readEvidenceArtifacts(roots.evidenceRoot, input.swarmId);
16
+ validateArtifactContents(bundle.plan, bundle.snapshot.outcome, bundle.events, artifacts);
17
+ const childProofs = await validateReferencedArtifacts(roots.runsRoot, bundle.plan, bundle.events, artifacts);
18
+ const files = [
19
+ ...bundle.files.map((file) => ({
20
+ kind: "operational",
21
+ path: normalizeRelative(relative(roots.swarmRoot, file.path)),
22
+ sha256: sha256(file.bytes),
23
+ bytes: file.bytes.byteLength
24
+ })),
25
+ ...artifacts.map((artifact) => ({
26
+ kind: "artifact",
27
+ path: `evidence/${artifact.relativePath}`,
28
+ sha256: sha256(artifact.bytes),
29
+ bytes: artifact.bytes.byteLength
30
+ })),
31
+ ...childProofs.map((proof) => ({
32
+ kind: "artifact",
33
+ path: normalizeRelative(relative(roots.runsRoot, proof.path)),
34
+ sha256: sha256(proof.bytes),
35
+ bytes: proof.bytes.byteLength
36
+ }))
37
+ ].sort((left, right) => left.path.localeCompare(right.path));
38
+ const index = {
39
+ schemaVersion: SWARM_EVIDENCE_INDEX_SCHEMA_VERSION,
40
+ swarmId: bundle.plan.swarmId,
41
+ planHash: bundle.plan.planHash,
42
+ revision: bundle.snapshot.revision,
43
+ outcome: structuredClone(bundle.snapshot.outcome),
44
+ sealedAt: bundle.snapshot.updatedAt,
45
+ files
46
+ };
47
+ const indexPath = join(roots.evidenceRoot, "evidence-index.json");
48
+ const sources = await captureSourceState([
49
+ ...bundle.files.map((file) => ({ path: file.path, bytes: file.bytes })),
50
+ ...artifacts.map((artifact) => ({ path: artifact.path, bytes: artifact.bytes })),
51
+ ...childProofs
52
+ ]);
53
+ await input.beforePublish?.();
54
+ await assertSourcesUnchanged(sources);
55
+ const indexBytes = `${JSON.stringify(index, null, 2)}\n`;
56
+ await writeIndexOnce(indexPath, indexBytes);
57
+ return {
58
+ index,
59
+ indexPath,
60
+ indexBytes,
61
+ model: {
62
+ plan: structuredClone(bundle.plan),
63
+ events: bundle.events.map((event) => structuredClone(event)),
64
+ snapshot: structuredClone(bundle.snapshot),
65
+ artifacts: artifacts.map((artifact) => ({
66
+ path: `evidence/${artifact.relativePath}`,
67
+ value: structuredClone(artifact.value)
68
+ }))
69
+ }
70
+ };
71
+ }
72
+ async function resolveExistingEvidenceRoots(rootDir, swarmId) {
73
+ const runsRoot = await existingExactDirectory(resolve(rootDir), "MISSING_SWARM_STORE");
74
+ const swarmsRoot = await existingExactDirectory(join(runsRoot, "_swarms"), "MISSING_SWARM_STORE");
75
+ assertContained(runsRoot, swarmsRoot);
76
+ const swarmRoot = await existingExactDirectory(join(swarmsRoot, swarmId), "MISSING_SWARM_STORE");
77
+ assertContained(swarmsRoot, swarmRoot);
78
+ const evidenceRoot = await existingExactDirectory(join(swarmRoot, "evidence"), "MISSING_SWARM_EVIDENCE");
79
+ assertContained(swarmRoot, evidenceRoot);
80
+ return { runsRoot, swarmsRoot, swarmRoot, evidenceRoot };
81
+ }
82
+ async function existingExactDirectory(lexicalPath, missingCode) {
83
+ const expected = resolve(lexicalPath);
84
+ const metadata = await lstat(expected).catch((error) => {
85
+ if (error.code === "ENOENT") {
86
+ throw new SwarmLiveStoreError(missingCode, "Required swarm evidence path is missing.", { path: expected });
87
+ }
88
+ throw error;
89
+ });
90
+ const canonical = await existingRealpath(expected, missingCode);
91
+ if (metadata.isSymbolicLink() || !metadata.isDirectory() || resolve(canonical) !== expected) {
92
+ throw new SwarmLiveStoreError("EVIDENCE_PATH_ALIAS", "Swarm evidence roots must be exact canonical directories.", {
93
+ path: expected
94
+ });
95
+ }
96
+ return canonical;
97
+ }
98
+ async function readEvidenceArtifacts(evidenceRoot, swarmId) {
99
+ const artifacts = [];
100
+ await walkEvidenceDirectory(evidenceRoot, evidenceRoot, artifacts, new Set([evidenceRoot]));
101
+ artifacts.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
102
+ for (const artifact of artifacts) {
103
+ if (artifact.relativePath === "evidence-index.json")
104
+ continue;
105
+ const artifactSwarmId = artifact.value.swarmId;
106
+ if (artifactSwarmId !== undefined && artifactSwarmId !== swarmId) {
107
+ throw new SwarmLiveStoreError("EVIDENCE_SWARM_MISMATCH", "Evidence artifact belongs to a different swarm.", {
108
+ path: artifact.relativePath
109
+ });
110
+ }
111
+ }
112
+ return artifacts.filter((artifact) => artifact.relativePath !== "evidence-index.json");
113
+ }
114
+ const PHASE5_SEAL_OUTPUTS = new Set([
115
+ "evidence-index.json",
116
+ "parent-receipt.json",
117
+ "swarm-receipt-integrity.json",
118
+ "swarm-receipt-seal.json",
119
+ "swarm-receipt-seal-status.json"
120
+ ]);
121
+ async function walkEvidenceDirectory(evidenceRoot, directory, artifacts, seenCanonicalPaths) {
122
+ const entries = await readdir(directory, { withFileTypes: true });
123
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
124
+ const lexicalPath = join(directory, entry.name);
125
+ if (entry.isSymbolicLink()) {
126
+ throw new SwarmLiveStoreError("EVIDENCE_PATH_ALIAS", "Evidence cannot contain symbolic-link or junction aliases.", {
127
+ path: lexicalPath
128
+ });
129
+ }
130
+ const canonicalPath = await existingRealpath(lexicalPath, "MISSING_SWARM_EVIDENCE");
131
+ assertContained(evidenceRoot, canonicalPath);
132
+ if (seenCanonicalPaths.has(canonicalPath)) {
133
+ throw new SwarmLiveStoreError("EVIDENCE_PATH_ALIAS", "Evidence contains a duplicate canonical path.", {
134
+ path: canonicalPath
135
+ });
136
+ }
137
+ seenCanonicalPaths.add(canonicalPath);
138
+ if (entry.isDirectory()) {
139
+ await walkEvidenceDirectory(evidenceRoot, canonicalPath, artifacts, seenCanonicalPaths);
140
+ continue;
141
+ }
142
+ if (!entry.isFile()) {
143
+ throw new SwarmLiveStoreError("MALFORMED_SWARM_EVIDENCE", "Evidence contains an unsupported filesystem entry.");
144
+ }
145
+ const relativePath = normalizeRelative(relative(evidenceRoot, canonicalPath));
146
+ if (PHASE5_SEAL_OUTPUTS.has(relativePath))
147
+ continue;
148
+ if (!relativePath.endsWith(".json")) {
149
+ throw new SwarmLiveStoreError("MALFORMED_SWARM_EVIDENCE", "Evidence artifacts must be JSON files.", {
150
+ path: relativePath
151
+ });
152
+ }
153
+ const bytes = await readFile(canonicalPath);
154
+ let parsed;
155
+ try {
156
+ parsed = JSON.parse(bytes.toString("utf8"));
157
+ }
158
+ catch {
159
+ throw new SwarmLiveStoreError("MALFORMED_SWARM_EVIDENCE", "Evidence artifact contains malformed JSON.", {
160
+ path: relativePath
161
+ });
162
+ }
163
+ if (!isRecord(parsed)) {
164
+ throw new SwarmLiveStoreError("MALFORMED_SWARM_EVIDENCE", "Evidence artifact must contain a JSON object.", {
165
+ path: relativePath
166
+ });
167
+ }
168
+ artifacts.push({ path: canonicalPath, relativePath, bytes, value: parsed });
169
+ }
170
+ }
171
+ async function validateReferencedArtifacts(runsRoot, plan, events, artifacts) {
172
+ const childCompletions = artifacts
173
+ .filter((artifact) => artifact.relativePath.startsWith("child-completions/"))
174
+ .map((artifact) => artifact.value);
175
+ const cleanupArtifacts = artifacts
176
+ .filter((artifact) => artifact.relativePath.startsWith("cleanup/"))
177
+ .map((artifact) => artifact.value);
178
+ const proofFiles = [];
179
+ const startedChildRuns = events
180
+ .filter((event) => event.type === "CHILD_STARTED" && event.childRunId)
181
+ .map((event) => event.childRunId);
182
+ if (new Set(startedChildRuns).size !== startedChildRuns.length) {
183
+ throw new SwarmLiveStoreError("DUPLICATE_CHILD_START_EVIDENCE", "A child run has multiple start event claims.");
184
+ }
185
+ const seenChildRuns = new Set();
186
+ for (const event of events) {
187
+ if (!event.childRunId
188
+ || !["CHILD_VERIFIED", "CHILD_STOPPED", "CHILD_NEEDS_REVIEW"].includes(event.type))
189
+ continue;
190
+ if (!startedChildRuns.includes(event.childRunId)) {
191
+ const blockedLaunch = events.some((candidate) => (candidate.type === "ACTION_BLOCKED"
192
+ && candidate.childRunId === event.childRunId
193
+ && candidate.taskId === event.taskId
194
+ && candidate.agentId === event.agentId
195
+ && candidate.payload.action === "child_launch"));
196
+ if (!blockedLaunch) {
197
+ throw new SwarmLiveStoreError("ORPHAN_CHILD_TERMINAL_EVIDENCE", "A child terminal event without a start must be bound to an explicit blocked launch.");
198
+ }
199
+ continue;
200
+ }
201
+ if (seenChildRuns.has(event.childRunId)) {
202
+ throw new SwarmLiveStoreError("DUPLICATE_CHILD_TERMINAL_EVIDENCE", "A child run has multiple terminal event claims.", {
203
+ childRunId: event.childRunId
204
+ });
205
+ }
206
+ seenChildRuns.add(event.childRunId);
207
+ const completions = childCompletions.filter((value) => value.childRunId === event.childRunId);
208
+ const completion = completions[0];
209
+ if (!completion || completions.length !== 1) {
210
+ throw new SwarmLiveStoreError("MISSING_CHILD_EVIDENCE", "Terminal child event has no persisted completion evidence.", {
211
+ childRunId: event.childRunId
212
+ });
213
+ }
214
+ proofFiles.push(...await validateChildProof(runsRoot, plan, event, completion, artifacts));
215
+ const cleanup = completion.cleanup;
216
+ if (isRecord(cleanup) && typeof cleanup.cleanupId === "string") {
217
+ if (!cleanupArtifacts.some((value) => value.cleanupId === cleanup.cleanupId)) {
218
+ throw new SwarmLiveStoreError("MISSING_CLEANUP_EVIDENCE", "Child completion references missing cleanup evidence.", {
219
+ childRunId: event.childRunId,
220
+ cleanupId: cleanup.cleanupId
221
+ });
222
+ }
223
+ }
224
+ }
225
+ for (const childRunId of startedChildRuns) {
226
+ if (!seenChildRuns.has(childRunId)) {
227
+ throw new SwarmLiveStoreError("MISSING_CHILD_TERMINAL_EVIDENCE", "Started child run has no terminal event claim.", {
228
+ childRunId
229
+ });
230
+ }
231
+ }
232
+ return proofFiles;
233
+ }
234
+ function validateArtifactContents(plan, outcome, events, artifacts) {
235
+ const semanticIds = new Set();
236
+ let verifiedGlobalCount = 0;
237
+ for (const artifact of artifacts) {
238
+ const value = artifact.value;
239
+ const category = artifact.relativePath.split("/")[0] ?? "";
240
+ const semanticId = semanticArtifactId(category, value);
241
+ if (semanticId) {
242
+ const key = `${category}:${semanticId}`;
243
+ if (semanticIds.has(key)) {
244
+ throw new SwarmLiveStoreError("DUPLICATE_SEMANTIC_EVIDENCE", "Evidence contains duplicate semantic artifacts.", {
245
+ category,
246
+ semanticId
247
+ });
248
+ }
249
+ semanticIds.add(key);
250
+ }
251
+ if (category === "cleanup") {
252
+ requireText(value.cleanupId, artifact.relativePath);
253
+ if (value.swarmId !== plan.swarmId || !["completed", "cleanup_pending", "failed"].includes(String(value.state))) {
254
+ throw new SwarmLiveStoreError("INVALID_CLEANUP_EVIDENCE", "Cleanup evidence does not bind the canonical swarm.");
255
+ }
256
+ if (value.state === "cleanup_pending" && outcome.state !== "needs_review") {
257
+ throw new SwarmLiveStoreError("CLEANUP_OUTCOME_MISMATCH", "Cleanup-pending evidence requires a needs-review parent outcome.");
258
+ }
259
+ }
260
+ else if (category === "integration-reconstructions") {
261
+ for (const field of ["reconstructionId", "failedCandidateId", "previousWorkspaceId", "replacementWorkspaceId"]) {
262
+ requireText(value[field], artifact.relativePath);
263
+ }
264
+ if (value.swarmId !== plan.swarmId
265
+ || value.baselineCommit !== plan.baselineCommit
266
+ || !["completed", "failed"].includes(String(value.state))
267
+ || (value.state === "completed" && value.expectedTreeHash !== value.actualTreeHash)) {
268
+ throw new SwarmLiveStoreError("INVALID_INTEGRATION_EVIDENCE", "Integration evidence does not bind the canonical plan.");
269
+ }
270
+ }
271
+ else if (category === "integration-outcomes") {
272
+ const decision = value.decision;
273
+ const event = value.event;
274
+ const payload = isRecord(event) ? event.payload : undefined;
275
+ if (!isRecord(decision)
276
+ || !isRecord(event)
277
+ || !isRecord(payload)
278
+ || decision.swarmId !== plan.swarmId
279
+ || event.swarmId !== plan.swarmId
280
+ || decision.candidateId !== payload.candidateId) {
281
+ throw new SwarmLiveStoreError("INVALID_INTEGRATION_EVIDENCE", "Integration outcome identities are inconsistent.");
282
+ }
283
+ }
284
+ else if (category === "global-verification") {
285
+ requireText(value.verificationId, artifact.relativePath);
286
+ if (value.swarmId !== plan.swarmId
287
+ || value.parentPolicyVersion !== plan.parentContract.policyVersion
288
+ || value.baselineCommit !== plan.baselineCommit
289
+ || !["pending", "passed", "failed", "unknown"].includes(String(value.commandState))
290
+ || !["pending", "clean", "mutated", "unknown"].includes(String(value.mutationState))) {
291
+ throw new SwarmLiveStoreError("INVALID_GLOBAL_VERIFICATION_EVIDENCE", "Verifier evidence does not bind the canonical plan.");
292
+ }
293
+ const matchingEvents = events.filter((event) => ((event.type === "GLOBAL_VERIFIER_PASSED" || event.type === "GLOBAL_VERIFIER_FAILED")
294
+ && event.payload.verificationId === value.verificationId
295
+ && event.payload.integratedTreeHash === value.integratedTreeHash));
296
+ const expectedEventType = value.commandState === "passed"
297
+ ? "GLOBAL_VERIFIER_PASSED"
298
+ : "GLOBAL_VERIFIER_FAILED";
299
+ if (matchingEvents.length !== 1 || matchingEvents[0]?.type !== expectedEventType) {
300
+ throw new SwarmLiveStoreError("INVALID_GLOBAL_VERIFICATION_EVIDENCE", "Verifier artifact does not match one canonical verifier event.");
301
+ }
302
+ if (value.commandState === "passed" && value.mutationState === "clean")
303
+ verifiedGlobalCount += 1;
304
+ }
305
+ }
306
+ const verifierEvents = events.filter((event) => (event.type === "GLOBAL_VERIFIER_PASSED" || event.type === "GLOBAL_VERIFIER_FAILED"));
307
+ const verifierArtifacts = artifacts.filter((artifact) => artifact.relativePath.startsWith("global-verification/"));
308
+ if (verifierEvents.length !== verifierArtifacts.length) {
309
+ throw new SwarmLiveStoreError("INVALID_GLOBAL_VERIFICATION_EVIDENCE", "Canonical verifier events and persisted verifier artifacts must be one-to-one.");
310
+ }
311
+ if (outcome.state === "verified" && verifiedGlobalCount !== 1) {
312
+ throw new SwarmLiveStoreError("INVALID_GLOBAL_VERIFICATION_EVIDENCE", "A verified parent outcome requires exactly one passed, clean global verification artifact.");
313
+ }
314
+ }
315
+ async function validateChildProof(runsRoot, plan, event, completion, artifacts) {
316
+ const childRunId = event.childRunId;
317
+ assertSwarmPathIdentifier(childRunId, "child run ID");
318
+ const childRoot = await existingRealpath(join(runsRoot, childRunId), "MISSING_CHILD_RUN_PROOF");
319
+ assertStrictlyContained(runsRoot, childRoot);
320
+ const loop = await readExistingProofFile(childRoot, "loop-record.json");
321
+ const ledger = await readExistingProofFile(childRoot, "ledger.jsonl");
322
+ const integrity = await readExistingProofFile(childRoot, "receipt-integrity.json");
323
+ let loopRecord;
324
+ try {
325
+ loopRecord = JSON.parse(loop.bytes.toString("utf8"));
326
+ }
327
+ catch {
328
+ throw new SwarmLiveStoreError("MALFORMED_CHILD_RUN_PROOF", "Child LoopRecord is malformed.", { childRunId });
329
+ }
330
+ const integritySummary = await verifyReceiptIntegrityFromFiles({
331
+ runId: childRunId,
332
+ runsRoot,
333
+ loopRecordPath: loop.path,
334
+ ledgerPath: ledger.path
335
+ });
336
+ if (integritySummary.state !== "verified") {
337
+ throw new SwarmLiveStoreError("CHILD_RECEIPT_INTEGRITY_INVALID", "Child run receipt integrity is not verified.", {
338
+ childRunId,
339
+ integrityState: integritySummary.state
340
+ });
341
+ }
342
+ const receipt = completion.receipt;
343
+ const link = loopRecord.receiptScope?.swarmChild;
344
+ const completionCleanup = completion.cleanup;
345
+ const matchingCleanup = isRecord(completionCleanup)
346
+ ? artifacts.find((artifact) => (artifact.relativePath.startsWith("cleanup/")
347
+ && artifact.value.cleanupId === completionCleanup.cleanupId))?.value
348
+ : undefined;
349
+ const matchingClosure = artifacts.find((artifact) => (artifact.relativePath.startsWith("process-closures/")
350
+ && artifact.value.childRunId === childRunId))?.value;
351
+ if (!isRecord(receipt)
352
+ || !link
353
+ || completion.swarmId !== plan.swarmId
354
+ || completion.childRunId !== childRunId
355
+ || completion.agentId !== event.agentId
356
+ || receipt.swarmId !== plan.swarmId
357
+ || receipt.childRunId !== childRunId
358
+ || receipt.agentId !== event.agentId
359
+ || link.parentSwarmId !== plan.swarmId
360
+ || link.agentId !== event.agentId
361
+ || completion.attemptId !== link.attemptId
362
+ || receipt.attemptId !== link.attemptId
363
+ || !sameStringSet(completion.taskIds, link.taskIds)
364
+ || !sameStringSet(receipt.taskIds, link.taskIds)
365
+ || (event.taskId !== undefined && !link.taskIds.includes(event.taskId))) {
366
+ throw new SwarmLiveStoreError("CHILD_RECEIPT_LINK_MISMATCH", "Child proof lineage does not match terminal evidence.", {
367
+ childRunId
368
+ });
369
+ }
370
+ if (typeof completion.workspaceId !== "string"
371
+ || !completion.workspaceId.trim()
372
+ || completion.processCloseState !== "closed"
373
+ || !["settled", "overspent", "released"].includes(String(completion.leaseState))
374
+ || completion.evidencePersisted !== true
375
+ || !["completed", "not_required"].includes(String(completion.workspaceCleanupState))
376
+ || !isRecord(completionCleanup)
377
+ || typeof completionCleanup.cleanupId !== "string"
378
+ || !matchingCleanup
379
+ || matchingCleanup.swarmId !== plan.swarmId
380
+ || matchingCleanup.workspaceId !== completion.workspaceId
381
+ || matchingCleanup.state !== completionCleanup.state
382
+ || !matchingClosure
383
+ || matchingClosure.swarmId !== plan.swarmId
384
+ || matchingClosure.workspaceId !== completion.workspaceId
385
+ || matchingClosure.agentId !== event.agentId
386
+ || matchingClosure.state !== "closed") {
387
+ throw new SwarmLiveStoreError("INVALID_CHILD_COMPLETION_EVIDENCE", "Child completion is missing its exact process closure or cleanup binding.", { childRunId });
388
+ }
389
+ const receiptIntegrity = receipt.integrity;
390
+ if (!isRecord(receiptIntegrity)
391
+ || receiptIntegrity.state !== "verified"
392
+ || receiptIntegrity.keyId !== integritySummary.keyId
393
+ || receiptIntegrity.loopRecordSha256 !== integritySummary.loopRecordSha256
394
+ || receiptIntegrity.ledgerSha256 !== integritySummary.ledgerSha256
395
+ || receiptIntegrity.ledgerHeadHash !== integritySummary.ledgerHeadHash
396
+ || receipt.bindingSha256 !== receiptBindingSha256(receipt)) {
397
+ throw new SwarmLiveStoreError("CHILD_RECEIPT_INTEGRITY_INVALID", "Child completion receipt does not bind verified integrity.", {
398
+ childRunId
399
+ });
400
+ }
401
+ return [loop, ledger, integrity];
402
+ }
403
+ async function readExistingProofFile(childRoot, name) {
404
+ const lexicalPath = join(childRoot, name);
405
+ const path = await existingRealpath(lexicalPath, "MISSING_CHILD_RUN_PROOF");
406
+ assertContained(childRoot, path);
407
+ if (resolve(lexicalPath) !== resolve(path)) {
408
+ throw new SwarmLiveStoreError("EVIDENCE_PATH_ALIAS", "Child proof files cannot be path aliases.", { path: lexicalPath });
409
+ }
410
+ return { path, bytes: await readFile(path) };
411
+ }
412
+ function receiptBindingSha256(receipt) {
413
+ const integrity = receipt.integrity;
414
+ if (!isRecord(integrity))
415
+ return "";
416
+ return sha256(Buffer.from(JSON.stringify({
417
+ receiptId: receipt.receiptId,
418
+ swarmId: receipt.swarmId,
419
+ childRunId: receipt.childRunId,
420
+ agentId: receipt.agentId,
421
+ attemptId: receipt.attemptId,
422
+ taskIds: Array.isArray(receipt.taskIds) ? [...receipt.taskIds].sort() : [],
423
+ integrity: {
424
+ state: integrity.state,
425
+ keyId: integrity.keyId,
426
+ loopRecordSha256: integrity.loopRecordSha256,
427
+ ledgerSha256: integrity.ledgerSha256,
428
+ ledgerHeadHash: integrity.ledgerHeadHash
429
+ }
430
+ }), "utf8"));
431
+ }
432
+ async function captureSourceState(sources) {
433
+ const seen = new Set();
434
+ const result = [];
435
+ for (const source of sources) {
436
+ const canonical = await realpath(source.path);
437
+ if (canonical !== source.path || seen.has(canonical)) {
438
+ throw new SwarmLiveStoreError("EVIDENCE_PATH_ALIAS", "Evidence source paths must be unique and canonical.", {
439
+ path: source.path
440
+ });
441
+ }
442
+ seen.add(canonical);
443
+ const metadata = await stat(canonical);
444
+ result.push({ path: canonical, bytes: source.bytes, mtimeMs: metadata.mtimeMs, size: metadata.size });
445
+ }
446
+ return result;
447
+ }
448
+ async function assertSourcesUnchanged(sources) {
449
+ for (const source of sources) {
450
+ const canonical = await realpath(source.path).catch(() => "");
451
+ const metadata = canonical ? await stat(canonical).catch(() => undefined) : undefined;
452
+ const bytes = canonical ? await readFile(canonical).catch(() => undefined) : undefined;
453
+ if (canonical !== source.path
454
+ || !metadata
455
+ || !bytes
456
+ || metadata.mtimeMs !== source.mtimeMs
457
+ || metadata.size !== source.size
458
+ || !bytes.equals(source.bytes)) {
459
+ throw new SwarmLiveStoreError("EVIDENCE_SOURCE_CHANGED", "Evidence source changed before index publication.", {
460
+ path: source.path
461
+ });
462
+ }
463
+ }
464
+ }
465
+ function semanticArtifactId(category, value) {
466
+ const fieldsByCategory = {
467
+ "child-completions": "childRunId",
468
+ cleanup: "cleanupId",
469
+ "integration-reconstructions": "reconstructionId",
470
+ "integration-outcomes": "candidateId",
471
+ "global-verification": "verificationId",
472
+ candidates: "candidateId",
473
+ decisions: "candidateId",
474
+ "process-closures": "childRunId",
475
+ "pre-cleanup": "childRunId",
476
+ "workspace-failures": "failureId"
477
+ };
478
+ const field = fieldsByCategory[category];
479
+ if (!field)
480
+ return undefined;
481
+ const direct = value[field];
482
+ if (typeof direct === "string" && direct.trim())
483
+ return direct;
484
+ if (category === "integration-outcomes" && isRecord(value.decision)) {
485
+ const candidateId = value.decision.candidateId;
486
+ if (typeof candidateId === "string" && candidateId.trim())
487
+ return candidateId;
488
+ }
489
+ return undefined;
490
+ }
491
+ function requireText(value, path) {
492
+ if (typeof value !== "string" || !value.trim()) {
493
+ throw new SwarmLiveStoreError("MALFORMED_SWARM_EVIDENCE", "Evidence artifact is missing a required identity.", {
494
+ path
495
+ });
496
+ }
497
+ }
498
+ function sameStringSet(left, right) {
499
+ return Array.isArray(left)
500
+ && left.length === right.length
501
+ && [...left].every((value) => typeof value === "string")
502
+ && [...left].sort().every((value, index) => value === [...right].sort()[index]);
503
+ }
504
+ async function writeIndexOnce(path, contents) {
505
+ const desired = Buffer.from(contents, "utf8");
506
+ try {
507
+ const existing = await readFile(path);
508
+ if (existing.equals(desired))
509
+ return;
510
+ throw new SwarmLiveStoreError("EVIDENCE_INDEX_CONFLICT", "A different sealed evidence index already exists.");
511
+ }
512
+ catch (error) {
513
+ if (!isNodeError(error, "ENOENT"))
514
+ throw error;
515
+ }
516
+ const temporary = `${path}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
517
+ let handle;
518
+ try {
519
+ handle = await open(temporary, "wx");
520
+ await handle.writeFile(desired);
521
+ await handle.sync();
522
+ await handle.close();
523
+ handle = undefined;
524
+ try {
525
+ await link(temporary, path);
526
+ }
527
+ catch (error) {
528
+ if (!isNodeError(error, "EEXIST"))
529
+ throw error;
530
+ const existing = await readFile(path);
531
+ if (!existing.equals(desired)) {
532
+ throw new SwarmLiveStoreError("EVIDENCE_INDEX_CONFLICT", "A different sealed evidence index already exists.");
533
+ }
534
+ }
535
+ }
536
+ finally {
537
+ await handle?.close();
538
+ await rm(temporary, { force: true });
539
+ }
540
+ }
541
+ async function existingRealpath(path, code) {
542
+ try {
543
+ return await realpath(path);
544
+ }
545
+ catch (error) {
546
+ if (isNodeError(error, "ENOENT")) {
547
+ throw new SwarmLiveStoreError(code, "Required swarm evidence path does not exist.", { path });
548
+ }
549
+ throw error;
550
+ }
551
+ }
552
+ function assertContained(parent, child) {
553
+ const rel = relative(parent, child);
554
+ if (!rel || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel)))
555
+ return;
556
+ throw new SwarmLiveStoreError("STORE_PATH_ESCAPE", "Swarm evidence path escapes its canonical parent.");
557
+ }
558
+ function assertStrictlyContained(parent, child) {
559
+ const rel = relative(parent, child);
560
+ if (rel && !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel))
561
+ return;
562
+ throw new SwarmLiveStoreError("STORE_PATH_ESCAPE", "Child run proof path must be strictly contained by runs root.");
563
+ }
564
+ function normalizeRelative(path) {
565
+ return path.split(sep).join("/");
566
+ }
567
+ function sha256(bytes) {
568
+ return createHash("sha256").update(bytes).digest("hex");
569
+ }
570
+ function isRecord(value) {
571
+ return typeof value === "object" && value !== null && !Array.isArray(value);
572
+ }
573
+ function isNodeError(error, code) {
574
+ return error instanceof Error && "code" in error && error.code === code;
575
+ }
@@ -0,0 +1,48 @@
1
+ import { type SwarmHostedEnvelope } from "../../contracts/swarm-hosted.js";
2
+ import { type SwarmReceiptProjection } from "./receipt-projection.js";
3
+ import { type SwarmOperationalState } from "./operations.js";
4
+ export declare const SWARM_HOSTED_TRANSPORT_DOMAIN: "martin.swarm-hosted.v1";
5
+ export interface BuildSwarmHostedEnvelopeInput {
6
+ runsRoot: string;
7
+ swarmId: string;
8
+ runtimeVersion: string;
9
+ }
10
+ export interface SwarmHostedTransportKeyLocator {
11
+ keyId: string;
12
+ keyLocatorHash: string;
13
+ domain: typeof SWARM_HOSTED_TRANSPORT_DOMAIN;
14
+ }
15
+ export interface BuiltSwarmHostedEnvelope {
16
+ envelope: SwarmHostedEnvelope;
17
+ canonicalBody: string;
18
+ payloadSha256: string;
19
+ envelopeIdentity: string;
20
+ transportKey: SwarmHostedTransportKeyLocator;
21
+ }
22
+ export interface BuildSwarmHostedEnvelopeDependencies {
23
+ readOperationalState(input: {
24
+ runsRoot: string;
25
+ swarmId: string;
26
+ }): Promise<SwarmOperationalState>;
27
+ readReceiptProjection(input: {
28
+ runsRoot: string;
29
+ swarmId: string;
30
+ }): Promise<SwarmReceiptProjection>;
31
+ ensureHostedTransportKey(input: {
32
+ runsRoot: string;
33
+ swarmId: string;
34
+ }): Promise<{
35
+ secret: string;
36
+ keyId: string;
37
+ }>;
38
+ }
39
+ /** @internal Direct-module seam for proving derivation never creates another secret file. */
40
+ export declare function deriveHostedTransportKeyMaterial(input: {
41
+ runsRoot: string;
42
+ swarmId: string;
43
+ }): Promise<{
44
+ secret: string;
45
+ keyId: string;
46
+ }>;
47
+ export declare function buildSwarmHostedEnvelope(input: BuildSwarmHostedEnvelopeInput): Promise<BuiltSwarmHostedEnvelope>;
48
+ export declare function buildSwarmHostedEnvelopeWithDependencies(input: BuildSwarmHostedEnvelopeInput, dependencies: BuildSwarmHostedEnvelopeDependencies): Promise<BuiltSwarmHostedEnvelope>;