@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,810 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { lstat, readFile, realpath } from "node:fs/promises";
4
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
5
+ import { SWARM_SCHEMA_VERSION, swarmConcretePathMatchesPattern, validateSwarmChildContract, validateSwarmConcretePath, validateSwarmRunRecord } from "../../contracts/index.js";
6
+ import { verifyReceiptIntegrityFromFiles } from "../persistence/integrity.js";
7
+ const MAX_GIT_OUTPUT_BYTES = 64 * 1024 * 1024;
8
+ const MAX_DIAGNOSTIC_LENGTH = 512;
9
+ /**
10
+ * Internal live-runtime bridge to the candidate module's single NUL-safe Git
11
+ * inventory parser. Policy enforcement remains in captureAndAdmitSwarmCandidate.
12
+ */
13
+ export async function inventorySwarmCandidatePaths(workspacePath, gitRunner = runGit) {
14
+ const workspaceRoot = await realpath(resolve(workspacePath));
15
+ const inventory = await inventoryChangedDelta(gitRunner, workspaceRoot);
16
+ const concrete = validateConcretePathSet(inventory.paths);
17
+ if (!concrete.ok)
18
+ throw new Error(concrete.error);
19
+ return [...concrete.paths];
20
+ }
21
+ export async function captureAndAdmitSwarmCandidate(input) {
22
+ const now = input.now ?? (() => new Date().toISOString());
23
+ const gitRunner = input.gitRunner ?? runGit;
24
+ let receiptIntegrity = "material_missing";
25
+ let changedPaths = [];
26
+ const reject = async (reasonCode, diagnostic) => persistRejection({
27
+ input,
28
+ reasonCode,
29
+ receiptIntegrity,
30
+ changedPaths,
31
+ ...(diagnostic ? { diagnostic: sanitizeDiagnostic(diagnostic, input.workspace.path) } : {}),
32
+ now
33
+ });
34
+ if (!hasIdentityFields(input) || !hasValidWorkspaceBinding(input) || !hasValidParentAuthority(input)) {
35
+ return reject("identity_mismatch", "Candidate identity does not match the manager-issued child workspace.");
36
+ }
37
+ if (input.workspace.record.baselineCommit !== input.expectedBaselineCommit) {
38
+ return reject("stale_baseline", "Candidate workspace record does not match the immutable parent baseline.");
39
+ }
40
+ if (input.childState !== "verified") {
41
+ return reject("child_not_terminal", "Child must be in the verified terminal state before candidate capture.");
42
+ }
43
+ const processClosure = await input.artifactWriter.loadProcessClosure(input.childRunId);
44
+ if (!processClosureMatches(input, processClosure)) {
45
+ return reject("process_active", "Child process tree must be closed before candidate capture.");
46
+ }
47
+ const receiptSummary = await verifyReceiptIntegrityFromFiles({
48
+ runId: input.childRunId,
49
+ runsRoot: input.runsRoot,
50
+ loopRecordPath: input.loopRecordPath,
51
+ ledgerPath: input.ledgerPath
52
+ });
53
+ receiptIntegrity = receiptSummary.state;
54
+ if (receiptSummary.state !== "verified") {
55
+ return reject("receipt_not_verified", receiptSummary.reason ?? "Child receipt integrity is not verified.");
56
+ }
57
+ const verifiedMaterial = await readVerifiedReceiptMaterial(input, receiptSummary);
58
+ if (!verifiedMaterial || !receiptBindingMatches(input, verifiedMaterial)) {
59
+ return reject("receipt_scope_mismatch", "Verified receipt is not bound to this swarm candidate identity.");
60
+ }
61
+ let workspaceRoot;
62
+ try {
63
+ workspaceRoot = await realpath(resolve(input.workspace.path));
64
+ if (!await signedReceiptScopeMatches(verifiedMaterial.receiptScope, workspaceRoot)) {
65
+ return reject("receipt_scope_mismatch", "Signed receipt scope does not match the manager-issued child workspace.");
66
+ }
67
+ await assertCanonicalStateUnchanged(gitRunner, input.canonicalRoot, workspaceRoot, input.expectedBaselineCommit, input.workspace.isolationMode ?? "worktree");
68
+ const workspaceHead = await gitScalar(gitRunner, workspaceRoot, [
69
+ "rev-parse",
70
+ "--verify",
71
+ "HEAD^{commit}"
72
+ ]);
73
+ if (input.workspace.record.baselineCommit !== input.expectedBaselineCommit
74
+ || workspaceHead !== input.expectedBaselineCommit) {
75
+ return reject("stale_baseline", "Candidate workspace does not match the immutable parent baseline.");
76
+ }
77
+ }
78
+ catch (error) {
79
+ return reject("stale_baseline", diagnosticFromError(error));
80
+ }
81
+ let observedInventory;
82
+ try {
83
+ observedInventory = await inventoryChangedDelta(gitRunner, workspaceRoot);
84
+ changedPaths = observedInventory.paths;
85
+ }
86
+ catch (error) {
87
+ return reject("git_inventory_failed", diagnosticFromError(error));
88
+ }
89
+ if (changedPaths.length === 0) {
90
+ return reject("empty_candidate", "Git reported no candidate mutation.");
91
+ }
92
+ const declared = validateConcretePathSet(input.declaredPaths);
93
+ if (!declared.ok) {
94
+ return reject("invalid_concrete_path", declared.error);
95
+ }
96
+ const observed = validateConcretePathSet(changedPaths);
97
+ if (!observed.ok) {
98
+ return reject("invalid_concrete_path", observed.error);
99
+ }
100
+ if (!sameStringSet(observed.paths, declared.paths)) {
101
+ return reject("undeclared_path", "Declared paths do not exactly equal the Git-observed delta.");
102
+ }
103
+ for (const candidatePath of observed.paths) {
104
+ if (input.inheritedDeniedPaths.some((pattern) => swarmConcretePathMatchesPattern(candidatePath, pattern))) {
105
+ return reject("path_denied", `Denied path: ${candidatePath}`);
106
+ }
107
+ if (!input.childAllowedPaths.some((pattern) => swarmConcretePathMatchesPattern(candidatePath, pattern))) {
108
+ return reject("path_not_allowed", `Path is outside the child allowance: ${candidatePath}`);
109
+ }
110
+ if (!input.taskWriteScope.some((pattern) => swarmConcretePathMatchesPattern(candidatePath, pattern))) {
111
+ return reject("task_scope_violation", `Path is outside the task write scope: ${candidatePath}`);
112
+ }
113
+ try {
114
+ await assertContainedConcretePath(workspaceRoot, candidatePath);
115
+ }
116
+ catch (error) {
117
+ return reject("unsafe_path", diagnosticFromError(error));
118
+ }
119
+ }
120
+ try {
121
+ await readTrackedModesAndAssertSafe(gitRunner, workspaceRoot, observed.paths);
122
+ }
123
+ catch (error) {
124
+ return reject("unsafe_path", diagnosticFromError(error));
125
+ }
126
+ const preStageFingerprint = await fingerprintCandidatePaths(workspaceRoot, observed.paths);
127
+ await input.beforeStage?.();
128
+ const postHookFingerprint = await fingerprintCandidatePaths(workspaceRoot, observed.paths);
129
+ if (preStageFingerprint !== postHookFingerprint) {
130
+ return reject("candidate_changed_during_capture", "Candidate bytes changed after validation and before immutable capture.");
131
+ }
132
+ try {
133
+ await gitRunner({ cwd: workspaceRoot, args: ["add", "-A", "--", "."] });
134
+ const secondInventory = await inventoryChangedDelta(gitRunner, workspaceRoot);
135
+ const stagedPaths = await inventoryStagedPaths(gitRunner, workspaceRoot, input.expectedBaselineCommit);
136
+ if (!sameStringSet(observed.paths, secondInventory.paths) || !sameStringSet(observed.paths, stagedPaths)) {
137
+ return reject("candidate_changed_during_capture", "Candidate delta changed after policy validation and before immutable capture.");
138
+ }
139
+ const unstaged = await gitRunner({ cwd: workspaceRoot, args: ["diff", "--quiet", "--no-ext-diff", "--"] });
140
+ if (unstaged.stdout.length !== 0 || unstaged.stderr.length !== 0) {
141
+ return reject("candidate_changed_during_capture", "Candidate retained unstaged mutations after capture.");
142
+ }
143
+ }
144
+ catch (error) {
145
+ return reject("candidate_changed_during_capture", diagnosticFromError(error));
146
+ }
147
+ try {
148
+ await readTrackedModesAndAssertSafe(gitRunner, workspaceRoot, observed.paths);
149
+ }
150
+ catch (error) {
151
+ return reject("unsafe_path", diagnosticFromError(error));
152
+ }
153
+ let patch;
154
+ let manifest;
155
+ let candidateTreeHash;
156
+ try {
157
+ const captured = await captureImmutableGitArtifact(gitRunner, workspaceRoot, input.expectedBaselineCommit, observed.paths);
158
+ patch = captured.patch;
159
+ manifest = captured.manifest;
160
+ candidateTreeHash = captured.candidateTreeHash;
161
+ }
162
+ catch (error) {
163
+ return reject("git_inventory_failed", diagnosticFromError(error));
164
+ }
165
+ if (patch.length === 0) {
166
+ return reject("empty_candidate", "Captured candidate patch is empty.");
167
+ }
168
+ const patchSha256 = sha256(patch);
169
+ const manifestSha256 = sha256(manifest);
170
+ const candidate = Object.freeze({
171
+ schemaVersion: SWARM_SCHEMA_VERSION,
172
+ candidateId: input.candidateId,
173
+ swarmId: input.swarmId,
174
+ workspaceId: input.workspace.record.workspaceId,
175
+ childRunId: input.childRunId,
176
+ agentId: input.agentId,
177
+ taskIds: Object.freeze([...input.taskIds]),
178
+ baselineCommit: input.expectedBaselineCommit,
179
+ patchSha256,
180
+ changedPaths: Object.freeze([...observed.paths]),
181
+ createdAt: now()
182
+ });
183
+ const artifact = Object.freeze({
184
+ candidate,
185
+ attemptId: input.attemptId,
186
+ proposalId: input.proposalId,
187
+ identitySha256: hashCandidateArtifactIdentity({
188
+ candidate,
189
+ attemptId: input.attemptId,
190
+ proposalId: input.proposalId,
191
+ patchSha256,
192
+ manifestSha256,
193
+ candidateTreeHash,
194
+ receipt: receiptSummary
195
+ }),
196
+ candidateTreeHash,
197
+ patch,
198
+ manifest,
199
+ manifestSha256,
200
+ receiptIntegrity: Object.freeze({ ...receiptSummary })
201
+ });
202
+ const admission = createAdmission(input, {
203
+ state: "admitted",
204
+ reasonCode: "admitted",
205
+ receiptIntegrity: receiptSummary.state,
206
+ changedPaths: observed.paths,
207
+ decidedAt: now()
208
+ });
209
+ try {
210
+ await input.artifactWriter.persistCandidate(artifact);
211
+ await input.artifactWriter.persistDecision(admission);
212
+ }
213
+ catch (error) {
214
+ const persistenceFailure = createAdmission(input, {
215
+ state: "rejected",
216
+ reasonCode: "artifact_persistence_failed",
217
+ receiptIntegrity: receiptSummary.state,
218
+ changedPaths: observed.paths,
219
+ decidedAt: now(),
220
+ diagnostic: sanitizeDiagnostic(diagnosticFromError(error), workspaceRoot)
221
+ });
222
+ try {
223
+ await input.artifactWriter.persistDecision(persistenceFailure);
224
+ }
225
+ catch {
226
+ // The caller receives no cleanup authority when durable evidence is unavailable.
227
+ }
228
+ return { admission: persistenceFailure, artifact, cleanupAuthorized: false };
229
+ }
230
+ return { admission, artifact, cleanupAuthorized: true };
231
+ }
232
+ async function persistRejection(context) {
233
+ const decision = createAdmission(context.input, {
234
+ state: "rejected",
235
+ reasonCode: context.reasonCode,
236
+ receiptIntegrity: context.receiptIntegrity,
237
+ changedPaths: context.changedPaths,
238
+ decidedAt: context.now(),
239
+ ...(context.diagnostic ? { diagnostic: context.diagnostic } : {})
240
+ });
241
+ try {
242
+ await context.input.artifactWriter.persistDecision(decision);
243
+ return { admission: decision, cleanupAuthorized: true };
244
+ }
245
+ catch (error) {
246
+ return {
247
+ admission: createAdmission(context.input, {
248
+ state: "rejected",
249
+ reasonCode: "artifact_persistence_failed",
250
+ receiptIntegrity: context.receiptIntegrity,
251
+ changedPaths: context.changedPaths,
252
+ decidedAt: context.now(),
253
+ diagnostic: sanitizeDiagnostic(diagnosticFromError(error), context.input.workspace.path)
254
+ }),
255
+ cleanupAuthorized: false
256
+ };
257
+ }
258
+ }
259
+ function createAdmission(input, decision) {
260
+ return Object.freeze({
261
+ schemaVersion: SWARM_SCHEMA_VERSION,
262
+ admissionId: input.admissionId,
263
+ candidateId: input.candidateId,
264
+ swarmId: input.swarmId,
265
+ workspaceId: input.workspace.record.workspaceId,
266
+ childRunId: input.childRunId,
267
+ agentId: input.agentId,
268
+ taskIds: Object.freeze([...input.taskIds]),
269
+ baselineCommit: input.expectedBaselineCommit,
270
+ changedPaths: Object.freeze([...decision.changedPaths]),
271
+ state: decision.state,
272
+ reasonCode: decision.reasonCode,
273
+ receiptIntegrity: decision.receiptIntegrity,
274
+ decidedAt: decision.decidedAt,
275
+ ...(decision.diagnostic ? { diagnostic: decision.diagnostic } : {})
276
+ });
277
+ }
278
+ function hasIdentityFields(input) {
279
+ return [
280
+ input.swarmId,
281
+ input.candidateId,
282
+ input.admissionId,
283
+ input.attemptId,
284
+ input.proposalId,
285
+ input.childRunId,
286
+ input.agentId,
287
+ input.expectedBaselineCommit
288
+ ].every((value) => value.trim().length > 0)
289
+ && input.taskIds.length > 0
290
+ && new Set(input.taskIds).size === input.taskIds.length;
291
+ }
292
+ function hasValidWorkspaceBinding(input) {
293
+ const record = input.workspace.record;
294
+ return record.kind === "child"
295
+ && record.swarmId === input.swarmId
296
+ && record.childRunId === input.childRunId
297
+ && record.agentId === input.agentId
298
+ && sameStringSet(record.taskIds, input.taskIds);
299
+ }
300
+ function processClosureMatches(input, closure) {
301
+ return closure?.state === "closed"
302
+ && closure.swarmId === input.swarmId
303
+ && closure.workspaceId === input.workspace.record.workspaceId
304
+ && closure.childRunId === input.childRunId
305
+ && closure.agentId === input.agentId
306
+ && Number.isFinite(Date.parse(closure.completedAt));
307
+ }
308
+ async function readVerifiedReceiptMaterial(input, firstSummary) {
309
+ try {
310
+ const rawLoopRecord = await readFile(input.loopRecordPath, "utf8");
311
+ if (!firstSummary.loopRecordSha256
312
+ || sha256(Buffer.from(rawLoopRecord, "utf8")) !== firstSummary.loopRecordSha256) {
313
+ return undefined;
314
+ }
315
+ const secondSummary = await verifyReceiptIntegrityFromFiles({
316
+ runId: input.childRunId,
317
+ runsRoot: input.runsRoot,
318
+ loopRecordPath: input.loopRecordPath,
319
+ ledgerPath: input.ledgerPath
320
+ });
321
+ if (secondSummary.state !== "verified"
322
+ || secondSummary.loopRecordSha256 !== firstSummary.loopRecordSha256
323
+ || secondSummary.ledgerSha256 !== firstSummary.ledgerSha256
324
+ || secondSummary.ledgerHeadHash !== firstSummary.ledgerHeadHash
325
+ || secondSummary.keyId !== firstSummary.keyId) {
326
+ return undefined;
327
+ }
328
+ return JSON.parse(rawLoopRecord);
329
+ }
330
+ catch {
331
+ return undefined;
332
+ }
333
+ }
334
+ async function signedReceiptScopeMatches(scope, workspaceRoot) {
335
+ const boundPaths = [scope?.repoRoot, scope?.workingDirectory]
336
+ .filter((value) => typeof value === "string" && value.trim().length > 0);
337
+ if (boundPaths.length === 0)
338
+ return false;
339
+ for (const boundPath of boundPaths) {
340
+ try {
341
+ if (!pathsEqual(await realpath(resolve(boundPath)), workspaceRoot))
342
+ return false;
343
+ }
344
+ catch {
345
+ return false;
346
+ }
347
+ }
348
+ return true;
349
+ }
350
+ function receiptBindingMatches(input, loop) {
351
+ let boundTaskIds;
352
+ try {
353
+ boundTaskIds = JSON.parse(loop.metadata["swarm.taskIds"] ?? "null");
354
+ }
355
+ catch {
356
+ return false;
357
+ }
358
+ return loop.loopId === input.childRunId
359
+ && loop.workspaceId === input.workspace.record.workspaceId
360
+ && loop.status === "completed"
361
+ && loop.lifecycleState === "completed"
362
+ && loop.metadata["swarm.parentId"] === input.swarmId
363
+ && loop.metadata["swarm.agentId"] === input.agentId
364
+ && loop.metadata["swarm.attemptId"] === input.attemptId
365
+ && loop.metadata["swarm.proposalId"] === input.proposalId
366
+ && loop.metadata["swarm.baselineCommit"] === input.expectedBaselineCommit
367
+ && Array.isArray(boundTaskIds)
368
+ && boundTaskIds.every((value) => typeof value === "string")
369
+ && sameStringSet(boundTaskIds, input.taskIds);
370
+ }
371
+ function hasValidParentAuthority(input) {
372
+ if (!validateSwarmRunRecord(input.swarmRecord).ok)
373
+ return false;
374
+ if (input.swarmRecord.swarmId !== input.swarmId || input.swarmRecord.outcome.state !== "running")
375
+ return false;
376
+ const agent = input.swarmRecord.agents.find((candidate) => candidate.agentId === input.agentId);
377
+ if (!agent
378
+ || agent.childRunId !== input.childRunId
379
+ || !validateSwarmChildContract(input.swarmRecord.parentContract, agent.contract).ok
380
+ || !sameStringSet(agent.contract.taskIds, input.taskIds)
381
+ || !sameStringSet(agent.contract.scope.allowedPaths, input.childAllowedPaths)) {
382
+ return false;
383
+ }
384
+ const requiredDenials = [
385
+ ...new Set([
386
+ ...input.swarmRecord.parentContract.scope.deniedPaths,
387
+ ...agent.contract.scope.deniedPaths
388
+ ])
389
+ ];
390
+ if (!sameStringSet(requiredDenials, input.inheritedDeniedPaths))
391
+ return false;
392
+ const tasks = input.taskIds.map((taskId) => input.swarmRecord.tasks.find((task) => task.taskId === taskId));
393
+ if (tasks.some((task) => !task) || tasks.some((task) => task?.assignedAgentId !== input.agentId))
394
+ return false;
395
+ const acceptedState = tasks.every((task) => task?.status === "accepted")
396
+ && agent.status === input.childState;
397
+ const livePreAdmissionState = tasks.every((task) => task?.status === "running")
398
+ && agent.status === "running";
399
+ if (!acceptedState && !livePreAdmissionState)
400
+ return false;
401
+ const taskScope = [...new Set(tasks.flatMap((task) => task?.writeScope ?? []))];
402
+ return sameStringSet(taskScope, input.taskWriteScope);
403
+ }
404
+ async function assertCanonicalStateUnchanged(gitRunner, canonicalRootInput, workspaceRoot, expectedBaselineCommit, isolationMode) {
405
+ const canonicalRoot = await realpath(resolve(canonicalRootInput));
406
+ const [canonicalHead, canonicalStatus, canonicalCommon, workspaceCommon] = await Promise.all([
407
+ gitScalar(gitRunner, canonicalRoot, ["rev-parse", "--verify", "HEAD^{commit}"]),
408
+ gitRunner({
409
+ cwd: canonicalRoot,
410
+ args: ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--no-renames"]
411
+ }),
412
+ resolveGitCommonDirectory(gitRunner, canonicalRoot),
413
+ resolveGitCommonDirectory(gitRunner, workspaceRoot)
414
+ ]);
415
+ if (canonicalHead !== expectedBaselineCommit || canonicalStatus.stdout.length !== 0) {
416
+ throw new Error("Canonical repository changed during candidate admission.");
417
+ }
418
+ if (isolationMode === "worktree") {
419
+ if (!pathsEqual(canonicalCommon, workspaceCommon)) {
420
+ throw new Error("Canonical repository does not own the candidate worktree.");
421
+ }
422
+ return;
423
+ }
424
+ if (!isContained(workspaceRoot, workspaceCommon) || pathsEqual(canonicalCommon, workspaceCommon)) {
425
+ throw new Error("Independent clone Git metadata is not self-contained.");
426
+ }
427
+ const origin = await gitScalar(gitRunner, workspaceRoot, ["config", "--get", "remote.origin.url"]);
428
+ if (!isAbsolute(origin)) {
429
+ throw new Error("Independent clone origin is not an absolute canonical repository path.");
430
+ }
431
+ const originReal = await realpath(resolve(origin));
432
+ if (!pathsEqual(originReal, canonicalRoot)) {
433
+ throw new Error("Independent clone origin does not match the canonical repository.");
434
+ }
435
+ }
436
+ async function resolveGitCommonDirectory(gitRunner, cwd) {
437
+ const value = await gitScalar(gitRunner, cwd, ["rev-parse", "--git-common-dir"]);
438
+ return realpath(resolve(cwd, value));
439
+ }
440
+ async function inventoryChangedDelta(gitRunner, cwd) {
441
+ const result = await gitRunner({
442
+ cwd,
443
+ args: ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--no-renames"]
444
+ });
445
+ return parsePorcelainStatus(result.stdout);
446
+ }
447
+ async function inventoryStagedPaths(gitRunner, cwd, baselineCommit) {
448
+ const result = await gitRunner({
449
+ cwd,
450
+ args: ["diff", "--cached", "--name-only", "-z", "--no-renames", baselineCommit, "--"]
451
+ });
452
+ return parseNulPaths(result.stdout);
453
+ }
454
+ function parsePorcelainStatus(output) {
455
+ if (output.length === 0)
456
+ return { paths: [], statuses: new Map() };
457
+ const statuses = new Map();
458
+ const paths = splitNul(output).map((record) => {
459
+ if (record.length < 4 || record[2] !== 0x20) {
460
+ throw new Error("Malformed NUL-delimited Git status record.");
461
+ }
462
+ const path = decodeGitPath(record.subarray(3));
463
+ statuses.set(path, record.subarray(0, 2).toString("ascii"));
464
+ return path;
465
+ }).sort(comparePaths);
466
+ return { paths, statuses };
467
+ }
468
+ function parseNulPaths(output) {
469
+ if (output.length === 0)
470
+ return [];
471
+ return splitNul(output).map(decodeGitPath).sort(comparePaths);
472
+ }
473
+ function splitNul(output) {
474
+ const records = [];
475
+ let start = 0;
476
+ for (let index = 0; index < output.length; index += 1) {
477
+ if (output[index] !== 0)
478
+ continue;
479
+ if (index > start)
480
+ records.push(output.subarray(start, index));
481
+ start = index + 1;
482
+ }
483
+ if (start !== output.length) {
484
+ throw new Error("Git NUL-delimited output was truncated.");
485
+ }
486
+ return records;
487
+ }
488
+ function decodeGitPath(value) {
489
+ return new TextDecoder("utf-8", { fatal: true }).decode(value);
490
+ }
491
+ function validateConcretePathSet(paths) {
492
+ const validated = [];
493
+ for (const value of paths) {
494
+ const result = validateSwarmConcretePath(value);
495
+ if (!result.ok)
496
+ return { ok: false, error: result.error };
497
+ validated.push(result.value);
498
+ }
499
+ const unique = [...new Set(validated)].sort(comparePaths);
500
+ if (unique.length !== validated.length) {
501
+ return { ok: false, error: "Concrete path set contains duplicates." };
502
+ }
503
+ return { ok: true, paths: unique };
504
+ }
505
+ async function assertContainedConcretePath(workspaceRoot, concretePath) {
506
+ let current = workspaceRoot;
507
+ for (const segment of concretePath.split("/")) {
508
+ current = resolve(current, segment);
509
+ try {
510
+ const stat = await lstat(current);
511
+ if (stat.isSymbolicLink()) {
512
+ throw new Error(`Candidate path traverses a symlink or junction: ${concretePath}`);
513
+ }
514
+ const resolvedSegment = await realpath(current);
515
+ if (!isContained(workspaceRoot, resolvedSegment)) {
516
+ throw new Error(`Candidate path escapes its workspace: ${concretePath}`);
517
+ }
518
+ }
519
+ catch (error) {
520
+ if (isNodeError(error) && error.code === "ENOENT") {
521
+ const ancestor = await nearestExistingRealAncestor(dirname(current));
522
+ if (!isContained(workspaceRoot, ancestor)) {
523
+ throw new Error(`Candidate path ancestor escapes its workspace: ${concretePath}`);
524
+ }
525
+ return;
526
+ }
527
+ throw error;
528
+ }
529
+ }
530
+ }
531
+ async function nearestExistingRealAncestor(start) {
532
+ let candidate = start;
533
+ while (true) {
534
+ try {
535
+ const stat = await lstat(candidate);
536
+ if (stat.isSymbolicLink()) {
537
+ throw new Error("Candidate path ancestor is a symlink or junction.");
538
+ }
539
+ return realpath(candidate);
540
+ }
541
+ catch (error) {
542
+ if (!isNodeError(error) || error.code !== "ENOENT")
543
+ throw error;
544
+ const parent = dirname(candidate);
545
+ if (parent === candidate)
546
+ throw error;
547
+ candidate = parent;
548
+ }
549
+ }
550
+ }
551
+ async function readTrackedModesAndAssertSafe(gitRunner, cwd, paths) {
552
+ const result = await gitRunner({
553
+ cwd,
554
+ args: ["ls-files", "--stage", "-z", "--", ...paths]
555
+ });
556
+ const modes = new Map();
557
+ for (const record of splitNul(result.stdout)) {
558
+ const tab = record.indexOf(0x09);
559
+ if (tab < 0)
560
+ throw new Error("Malformed Git index mode record.");
561
+ const header = record.subarray(0, tab).toString("ascii");
562
+ const mode = header.split(" ", 1)[0];
563
+ const path = decodeGitPath(record.subarray(tab + 1));
564
+ if (mode)
565
+ modes.set(path, mode);
566
+ if (mode === "120000")
567
+ throw new Error("Changed symbolic links are forbidden in swarm candidates.");
568
+ if (mode === "160000")
569
+ throw new Error("Changed gitlinks are forbidden in swarm candidates.");
570
+ }
571
+ return modes;
572
+ }
573
+ export function verifyPersistedSwarmCandidateArtifact(artifact) {
574
+ const patchSha256 = sha256(artifact.patch);
575
+ const manifestSha256 = sha256(artifact.manifest);
576
+ return artifact.candidate.schemaVersion === SWARM_SCHEMA_VERSION
577
+ && artifact.candidate.candidateId.trim().length > 0
578
+ && artifact.candidate.swarmId.trim().length > 0
579
+ && artifact.candidate.childRunId.trim().length > 0
580
+ && artifact.candidate.agentId.trim().length > 0
581
+ && artifact.candidate.baselineCommit.trim().length > 0
582
+ && artifact.attemptId.trim().length > 0
583
+ && artifact.proposalId.trim().length > 0
584
+ && /^[0-9a-f]{40,64}$/u.test(artifact.candidateTreeHash)
585
+ && artifact.receiptIntegrity.state === "verified"
586
+ && manifestMatchesCandidate(artifact.manifest, artifact.candidate.changedPaths)
587
+ && patchSha256 === artifact.candidate.patchSha256
588
+ && manifestSha256 === artifact.manifestSha256
589
+ && artifact.identitySha256 === hashCandidateArtifactIdentity({
590
+ candidate: artifact.candidate,
591
+ attemptId: artifact.attemptId,
592
+ proposalId: artifact.proposalId,
593
+ patchSha256,
594
+ manifestSha256,
595
+ candidateTreeHash: artifact.candidateTreeHash,
596
+ receipt: artifact.receiptIntegrity
597
+ });
598
+ }
599
+ function manifestMatchesCandidate(manifest, changedPaths) {
600
+ let decoded;
601
+ try {
602
+ decoded = new TextDecoder("utf-8", { fatal: true }).decode(manifest);
603
+ }
604
+ catch {
605
+ return false;
606
+ }
607
+ const fields = decoded.split("\0");
608
+ if (fields.at(-1) !== "")
609
+ return false;
610
+ fields.pop();
611
+ if (fields.length !== changedPaths.length * 3)
612
+ return false;
613
+ const manifestPaths = [];
614
+ for (let index = 0; index < fields.length; index += 3) {
615
+ const status = fields[index];
616
+ const mode = fields[index + 1];
617
+ const path = fields[index + 2];
618
+ if (!status || status.length !== 2 || !mode || !/^[0-7]{6}$/u.test(mode) || !path)
619
+ return false;
620
+ if (!validateSwarmConcretePath(path).ok)
621
+ return false;
622
+ manifestPaths.push(path);
623
+ }
624
+ return sameStringSet(manifestPaths, changedPaths);
625
+ }
626
+ function hashCandidateArtifactIdentity(input) {
627
+ return sha256(Buffer.from(JSON.stringify({
628
+ swarmId: input.candidate.swarmId,
629
+ candidateId: input.candidate.candidateId,
630
+ attemptId: input.attemptId,
631
+ proposalId: input.proposalId,
632
+ workspaceId: input.candidate.workspaceId,
633
+ childRunId: input.candidate.childRunId,
634
+ agentId: input.candidate.agentId,
635
+ taskIds: [...input.candidate.taskIds].sort(comparePaths),
636
+ baselineCommit: input.candidate.baselineCommit,
637
+ patchSha256: input.patchSha256,
638
+ manifestSha256: input.manifestSha256,
639
+ candidateTreeHash: input.candidateTreeHash,
640
+ receipt: {
641
+ keyId: input.receipt.keyId,
642
+ loopRecordSha256: input.receipt.loopRecordSha256,
643
+ ledgerSha256: input.receipt.ledgerSha256,
644
+ ledgerHeadHash: input.receipt.ledgerHeadHash
645
+ }
646
+ }), "utf8"));
647
+ }
648
+ async function captureImmutableGitArtifact(gitRunner, cwd, baselineCommit, expectedPaths) {
649
+ const candidateTreeHash = await gitScalar(gitRunner, cwd, ["write-tree"]);
650
+ if (!/^[0-9a-f]{40,64}$/u.test(candidateTreeHash)) {
651
+ throw new Error("Git returned an invalid immutable candidate tree identity.");
652
+ }
653
+ const raw = await gitRunner({
654
+ cwd,
655
+ args: ["diff", "--raw", "-z", "--no-abbrev", "--no-renames", baselineCommit, candidateTreeHash, "--"]
656
+ });
657
+ const parsed = parseRawDiffManifest(raw.stdout);
658
+ if (!sameStringSet(parsed.paths, expectedPaths)) {
659
+ throw new Error("Captured patch paths do not match the validated candidate delta.");
660
+ }
661
+ const patch = await gitRunner({
662
+ cwd,
663
+ args: [
664
+ "diff",
665
+ "--binary",
666
+ "--full-index",
667
+ "--no-ext-diff",
668
+ "--no-renames",
669
+ baselineCommit,
670
+ candidateTreeHash,
671
+ "--"
672
+ ]
673
+ });
674
+ const stableTreeHash = await gitScalar(gitRunner, cwd, ["write-tree"]);
675
+ if (stableTreeHash !== candidateTreeHash) {
676
+ throw new Error("Candidate index changed while its immutable tree evidence was captured.");
677
+ }
678
+ return { patch: Buffer.from(patch.stdout), manifest: parsed.manifest, candidateTreeHash };
679
+ }
680
+ function parseRawDiffManifest(output) {
681
+ const records = splitNul(output);
682
+ if (records.length % 2 !== 0)
683
+ throw new Error("Malformed Git raw diff output.");
684
+ const paths = [];
685
+ const fields = [];
686
+ for (let index = 0; index < records.length; index += 2) {
687
+ const header = records[index]?.toString("ascii") ?? "";
688
+ const pathRecord = records[index + 1];
689
+ const match = /^:([0-7]{6}) ([0-7]{6}) [0-9a-f]+ [0-9a-f]+ ([A-Z])$/u.exec(header);
690
+ if (!match?.[1] || !match[2] || !match[3] || !pathRecord) {
691
+ throw new Error("Malformed Git raw diff record.");
692
+ }
693
+ const path = decodeGitPath(pathRecord);
694
+ const concrete = validateSwarmConcretePath(path);
695
+ if (!concrete.ok)
696
+ throw new Error(concrete.error);
697
+ const mode = match[2] === "000000" ? match[1] : match[2];
698
+ if (mode === "120000")
699
+ throw new Error("Changed symbolic links are forbidden in swarm candidates.");
700
+ if (mode === "160000")
701
+ throw new Error("Changed gitlinks are forbidden in swarm candidates.");
702
+ paths.push(concrete.value);
703
+ fields.push(`${match[3]} `, mode, concrete.value, "");
704
+ }
705
+ const unique = [...new Set(paths)];
706
+ if (unique.length !== paths.length)
707
+ throw new Error("Git raw diff contains duplicate paths.");
708
+ return {
709
+ paths: [...paths].sort(comparePaths),
710
+ manifest: Buffer.from(fields.join("\0"), "utf8")
711
+ };
712
+ }
713
+ async function fingerprintCandidatePaths(root, paths) {
714
+ const hash = createHash("sha256");
715
+ for (const path of paths) {
716
+ hash.update(path, "utf8");
717
+ hash.update("\0", "utf8");
718
+ const absolute = resolve(root, ...path.split("/"));
719
+ try {
720
+ const stat = await lstat(absolute);
721
+ if (stat.isSymbolicLink()) {
722
+ hash.update("symlink", "utf8");
723
+ }
724
+ else if (stat.isFile()) {
725
+ hash.update("file\0", "utf8");
726
+ hash.update(await readFile(absolute));
727
+ }
728
+ else if (stat.isDirectory()) {
729
+ hash.update("directory", "utf8");
730
+ }
731
+ else {
732
+ hash.update("other", "utf8");
733
+ }
734
+ }
735
+ catch (error) {
736
+ if (!isNodeError(error) || error.code !== "ENOENT")
737
+ throw error;
738
+ hash.update("missing", "utf8");
739
+ }
740
+ hash.update("\0", "utf8");
741
+ }
742
+ return hash.digest("hex");
743
+ }
744
+ function sameStringSet(left, right) {
745
+ if (left.length !== right.length)
746
+ return false;
747
+ const normalizedLeft = [...left].sort(comparePaths);
748
+ const normalizedRight = [...right].sort(comparePaths);
749
+ return normalizedLeft.every((value, index) => value === normalizedRight[index]);
750
+ }
751
+ function comparePaths(left, right) {
752
+ return Buffer.from(left, "utf8").compare(Buffer.from(right, "utf8"));
753
+ }
754
+ function isContained(root, candidate) {
755
+ const rel = relative(root, candidate);
756
+ return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel));
757
+ }
758
+ function pathsEqual(left, right) {
759
+ return process.platform === "win32"
760
+ ? resolve(left).toLowerCase() === resolve(right).toLowerCase()
761
+ : resolve(left) === resolve(right);
762
+ }
763
+ async function gitScalar(gitRunner, cwd, args) {
764
+ const result = await gitRunner({ cwd, args });
765
+ return result.stdout.toString("utf8").trim();
766
+ }
767
+ function runGit(command) {
768
+ return new Promise((resolvePromise, rejectPromise) => {
769
+ execFile("git", [...command.args], {
770
+ cwd: command.cwd,
771
+ encoding: null,
772
+ windowsHide: true,
773
+ maxBuffer: MAX_GIT_OUTPUT_BYTES
774
+ }, (error, stdout, stderr) => {
775
+ if (error) {
776
+ rejectPromise(Object.assign(error, { stderr }));
777
+ return;
778
+ }
779
+ resolvePromise({
780
+ stdout: Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout),
781
+ stderr: Buffer.isBuffer(stderr) ? stderr : Buffer.from(stderr)
782
+ });
783
+ });
784
+ });
785
+ }
786
+ function sha256(value) {
787
+ return createHash("sha256").update(value).digest("hex");
788
+ }
789
+ function sanitizeDiagnostic(value, workspacePath) {
790
+ const compact = value
791
+ .replaceAll(workspacePath, "<workspace>")
792
+ .replace(/[\u0000-\u001f\u007f]+/gu, " ")
793
+ .replace(/\s+/gu, " ")
794
+ .trim();
795
+ return compact.slice(0, MAX_DIAGNOSTIC_LENGTH);
796
+ }
797
+ function diagnosticFromError(error) {
798
+ if (error instanceof Error) {
799
+ const stderr = error.stderr;
800
+ if (Buffer.isBuffer(stderr) && stderr.length > 0)
801
+ return stderr.toString("utf8");
802
+ if (typeof stderr === "string" && stderr.length > 0)
803
+ return stderr;
804
+ return error.message;
805
+ }
806
+ return String(error);
807
+ }
808
+ function isNodeError(error) {
809
+ return error instanceof Error && "code" in error;
810
+ }