@martinloop/mcp 0.7.1 → 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.
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/vendor/adapters/claude-cli.d.ts +11 -1
- package/dist/vendor/adapters/claude-cli.js +285 -71
- package/dist/vendor/adapters/cli-bridge.d.ts +12 -3
- package/dist/vendor/adapters/cli-bridge.js +129 -149
- package/dist/vendor/adapters/codex-cli.d.ts +3 -3
- package/dist/vendor/adapters/codex-cli.js +10 -24
- package/dist/vendor/adapters/codex-host.js +127 -132
- package/dist/vendor/adapters/index.d.ts +2 -1
- package/dist/vendor/adapters/index.js +2 -1
- package/dist/vendor/adapters/openai-compatible.js +88 -6
- package/dist/vendor/adapters/process-supervisor.d.ts +60 -0
- package/dist/vendor/adapters/process-supervisor.js +541 -0
- package/dist/vendor/adapters/verifier-only.d.ts +10 -1
- package/dist/vendor/adapters/verifier-only.js +43 -1
- package/dist/vendor/contracts/external-outcome.d.ts +102 -0
- package/dist/vendor/contracts/external-outcome.js +226 -0
- package/dist/vendor/contracts/index.d.ts +8 -1
- package/dist/vendor/contracts/index.js +2 -0
- package/dist/vendor/contracts/swarm-hosted.d.ts +204 -0
- package/dist/vendor/contracts/swarm-hosted.js +629 -0
- package/dist/vendor/contracts/swarm.d.ts +449 -0
- package/dist/vendor/contracts/swarm.js +550 -0
- package/dist/vendor/core/external-outcome.d.ts +34 -0
- package/dist/vendor/core/external-outcome.js +515 -0
- package/dist/vendor/core/grounding.js +1 -1
- package/dist/vendor/core/index.d.ts +47 -1
- package/dist/vendor/core/index.js +53 -3
- package/dist/vendor/core/leash.d.ts +1 -0
- package/dist/vendor/core/leash.js +13 -0
- package/dist/vendor/core/persistence/integrity.d.ts +4 -0
- package/dist/vendor/core/persistence/integrity.js +19 -6
- package/dist/vendor/core/persistence/swarm-integrity.d.ts +63 -0
- package/dist/vendor/core/persistence/swarm-integrity.js +309 -0
- package/dist/vendor/core/swarm/candidates.d.ts +78 -0
- package/dist/vendor/core/swarm/candidates.js +810 -0
- package/dist/vendor/core/swarm/evidence-projection.d.ts +87 -0
- package/dist/vendor/core/swarm/evidence-projection.js +128 -0
- package/dist/vendor/core/swarm/evidence.d.ts +40 -0
- package/dist/vendor/core/swarm/evidence.js +575 -0
- package/dist/vendor/core/swarm/hosted-export.d.ts +48 -0
- package/dist/vendor/core/swarm/hosted-export.js +362 -0
- package/dist/vendor/core/swarm/hosted-projection.d.ts +5 -0
- package/dist/vendor/core/swarm/hosted-projection.js +258 -0
- package/dist/vendor/core/swarm/index.d.ts +110 -0
- package/dist/vendor/core/swarm/index.js +1060 -0
- package/dist/vendor/core/swarm/integration.d.ts +76 -0
- package/dist/vendor/core/swarm/integration.js +615 -0
- package/dist/vendor/core/swarm/lifecycle.d.ts +72 -0
- package/dist/vendor/core/swarm/lifecycle.js +252 -0
- package/dist/vendor/core/swarm/live-runtime.d.ts +78 -0
- package/dist/vendor/core/swarm/live-runtime.js +1482 -0
- package/dist/vendor/core/swarm/live-store.d.ts +99 -0
- package/dist/vendor/core/swarm/live-store.js +860 -0
- package/dist/vendor/core/swarm/operational-reader.d.ts +17 -0
- package/dist/vendor/core/swarm/operational-reader.js +96 -0
- package/dist/vendor/core/swarm/operations.d.ts +29 -0
- package/dist/vendor/core/swarm/operations.js +128 -0
- package/dist/vendor/core/swarm/parent-receipt.d.ts +13 -0
- package/dist/vendor/core/swarm/parent-receipt.js +489 -0
- package/dist/vendor/core/swarm/receipt-projection.d.ts +26 -0
- package/dist/vendor/core/swarm/receipt-projection.js +120 -0
- package/dist/vendor/core/swarm/scheduler.d.ts +92 -0
- package/dist/vendor/core/swarm/scheduler.js +555 -0
- package/dist/vendor/core/swarm/verification.d.ts +70 -0
- package/dist/vendor/core/swarm/verification.js +295 -0
- package/dist/vendor/core/swarm/workspaces.d.ts +77 -0
- package/dist/vendor/core/swarm/workspaces.js +488 -0
- package/dist/vendor/core/verified-handoff.d.ts +4 -0
- package/dist/vendor/core/verified-handoff.js +4 -0
- package/package.json +1 -1
- 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>;
|