@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,489 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { SWARM_PARENT_RECEIPT_SCHEMA_VERSION, SWARM_SCHEMA_VERSION } from "../../contracts/index.js";
|
|
3
|
+
export class SwarmParentReceiptError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "SwarmParentReceiptError";
|
|
8
|
+
this.code = code;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function buildParentSwarmReceipt(sealed) {
|
|
12
|
+
const { plan, events, artifacts } = sealed.model;
|
|
13
|
+
const outcome = sealed.index.outcome;
|
|
14
|
+
if (outcome.state === "running") {
|
|
15
|
+
throw new SwarmParentReceiptError("NON_TERMINAL_SWARM", "A running swarm cannot produce a parent receipt.");
|
|
16
|
+
}
|
|
17
|
+
const terminalOutcome = structuredClone(outcome);
|
|
18
|
+
if (sealed.index.swarmId !== plan.swarmId || sealed.index.planHash !== plan.planHash) {
|
|
19
|
+
throw new SwarmParentReceiptError("SEALED_MODEL_MISMATCH", "The sealed evidence model does not match its index.");
|
|
20
|
+
}
|
|
21
|
+
const blockedActions = projectBlockedActions(events);
|
|
22
|
+
const reassignments = projectReassignments(events);
|
|
23
|
+
const childReceipts = projectChildReceipts(sealed);
|
|
24
|
+
const globalEvent = [...events].reverse().find((event) => (event.type === "GLOBAL_VERIFIER_PASSED" || event.type === "GLOBAL_VERIFIER_FAILED"));
|
|
25
|
+
const integratedTreeHash = optionalText(globalEvent?.payload.integratedTreeHash);
|
|
26
|
+
const globalVerificationId = optionalText(globalEvent?.payload.verificationId);
|
|
27
|
+
const evidenceFiles = sealed.index.files.map((file) => structuredClone(file));
|
|
28
|
+
const evidenceBindings = projectEvidenceBindings(plan, events, sealed.model.artifacts);
|
|
29
|
+
const base = {
|
|
30
|
+
schemaVersion: SWARM_PARENT_RECEIPT_SCHEMA_VERSION,
|
|
31
|
+
swarmId: plan.swarmId,
|
|
32
|
+
planHash: plan.planHash,
|
|
33
|
+
objective: plan.parentContract.objective,
|
|
34
|
+
engine: structuredClone(plan.engine),
|
|
35
|
+
baselineCommit: plan.baselineCommit,
|
|
36
|
+
tasks: projectTerminalTasks(plan.tasks, events),
|
|
37
|
+
agents: projectTerminalAgents(plan.agents, events),
|
|
38
|
+
budget: structuredClone(plan.parentContract.budget),
|
|
39
|
+
budgetLedger: projectBudgetLedger(plan.parentContract.budget, events),
|
|
40
|
+
childReceipts,
|
|
41
|
+
blockedActions,
|
|
42
|
+
reassignments,
|
|
43
|
+
events: events.map((event) => structuredClone(event)),
|
|
44
|
+
evidenceIndexSha256: sha256(sealed.indexBytes),
|
|
45
|
+
evidenceFiles,
|
|
46
|
+
evidenceBindings,
|
|
47
|
+
...(integratedTreeHash ? { integratedTreeHash } : {}),
|
|
48
|
+
...(globalVerificationId ? { globalVerificationId } : {}),
|
|
49
|
+
parentOutcome: terminalOutcome,
|
|
50
|
+
taskVerificationState: globalEvent?.type === "GLOBAL_VERIFIER_PASSED"
|
|
51
|
+
? "passed"
|
|
52
|
+
: globalEvent?.type === "GLOBAL_VERIFIER_FAILED" ? "failed" : "unknown",
|
|
53
|
+
sealedAt: sealed.index.sealedAt
|
|
54
|
+
};
|
|
55
|
+
const receiptSha256 = sha256(stableJson(base));
|
|
56
|
+
return {
|
|
57
|
+
...base,
|
|
58
|
+
receiptId: `swarm-receipt-${receiptSha256.slice(0, 16)}`,
|
|
59
|
+
receiptSha256
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function projectTerminalTasks(tasks, events) {
|
|
63
|
+
const projected = new Map(tasks.map((task) => [task.taskId, structuredClone(task)]));
|
|
64
|
+
for (const event of [...events].sort(bySequence)) {
|
|
65
|
+
if (!event.taskId)
|
|
66
|
+
continue;
|
|
67
|
+
const task = projected.get(event.taskId);
|
|
68
|
+
if (!task)
|
|
69
|
+
continue;
|
|
70
|
+
if (event.type === "TASK_REASSIGNED") {
|
|
71
|
+
const toAgentId = requiredText(event.payload.toAgentId, "INVALID_REASSIGNMENT_EVENT");
|
|
72
|
+
projected.set(event.taskId, { ...task, assignedAgentId: toAgentId });
|
|
73
|
+
}
|
|
74
|
+
else if (event.type === "CHILD_STARTED") {
|
|
75
|
+
projected.set(event.taskId, { ...task, status: "running" });
|
|
76
|
+
}
|
|
77
|
+
else if (event.type === "CHILD_VERIFIED") {
|
|
78
|
+
projected.set(event.taskId, { ...task, status: "accepted" });
|
|
79
|
+
}
|
|
80
|
+
else if (event.type === "CHILD_STOPPED") {
|
|
81
|
+
projected.set(event.taskId, { ...task, status: "stopped" });
|
|
82
|
+
}
|
|
83
|
+
else if (event.type === "CHILD_NEEDS_REVIEW") {
|
|
84
|
+
projected.set(event.taskId, { ...task, status: "needs_review" });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return tasks.map((task) => projected.get(task.taskId));
|
|
88
|
+
}
|
|
89
|
+
function projectTerminalAgents(agents, events) {
|
|
90
|
+
const projected = new Map(agents.map((agent) => [agent.agentId, structuredClone(agent)]));
|
|
91
|
+
for (const event of [...events].sort(bySequence)) {
|
|
92
|
+
if (!event.agentId)
|
|
93
|
+
continue;
|
|
94
|
+
const agent = projected.get(event.agentId);
|
|
95
|
+
if (!agent)
|
|
96
|
+
continue;
|
|
97
|
+
if (event.type === "CHILD_STARTED") {
|
|
98
|
+
projected.set(event.agentId, { ...agent, status: "running", ...(event.childRunId ? { childRunId: event.childRunId } : {}) });
|
|
99
|
+
}
|
|
100
|
+
else if (event.type === "CHILD_VERIFIED") {
|
|
101
|
+
projected.set(event.agentId, { ...agent, status: "verified", ...(event.childRunId ? { childRunId: event.childRunId } : {}) });
|
|
102
|
+
}
|
|
103
|
+
else if (event.type === "CHILD_STOPPED") {
|
|
104
|
+
projected.set(event.agentId, { ...agent, status: "stopped", ...(event.childRunId ? { childRunId: event.childRunId } : {}) });
|
|
105
|
+
}
|
|
106
|
+
else if (event.type === "CHILD_NEEDS_REVIEW") {
|
|
107
|
+
projected.set(event.agentId, { ...agent, status: "needs_review", ...(event.childRunId ? { childRunId: event.childRunId } : {}) });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return agents.map((agent) => projected.get(agent.agentId));
|
|
111
|
+
}
|
|
112
|
+
function projectBudgetLedger(budget, events) {
|
|
113
|
+
const projected = new Map();
|
|
114
|
+
const childLeaseIds = new Map();
|
|
115
|
+
for (const event of [...events].sort(bySequence)) {
|
|
116
|
+
const explicitLeaseId = optionalText(event.payload.leaseId);
|
|
117
|
+
if (explicitLeaseId && event.childRunId)
|
|
118
|
+
childLeaseIds.set(event.childRunId, explicitLeaseId);
|
|
119
|
+
const leaseId = explicitLeaseId ?? (event.childRunId ? childLeaseIds.get(event.childRunId) : undefined);
|
|
120
|
+
if (!leaseId)
|
|
121
|
+
continue;
|
|
122
|
+
const previous = projected.get(leaseId);
|
|
123
|
+
const reservedUsd = event.payload.reservedUsd === undefined
|
|
124
|
+
? previous?.reservedUsd
|
|
125
|
+
: requiredNumber(event.payload.reservedUsd, "INVALID_CHILD_BUDGET_EVENT");
|
|
126
|
+
const reservedTokens = event.payload.reservedTokens === undefined
|
|
127
|
+
? previous?.reservedTokens
|
|
128
|
+
: requiredNumber(event.payload.reservedTokens, "INVALID_CHILD_BUDGET_EVENT");
|
|
129
|
+
if (reservedUsd === undefined || reservedTokens === undefined) {
|
|
130
|
+
throw new SwarmParentReceiptError("INVALID_CHILD_BUDGET_EVENT", "Persisted budget evidence omits the reserved lease amount.");
|
|
131
|
+
}
|
|
132
|
+
const statusValue = event.payload.leaseState;
|
|
133
|
+
const status = statusValue === "settled" || statusValue === "overspent" || statusValue === "released" || statusValue === "reserved"
|
|
134
|
+
? statusValue
|
|
135
|
+
: previous?.status ?? "reserved";
|
|
136
|
+
const actualUsd = optionalNumber(event.payload.actualUsd);
|
|
137
|
+
const actualTokens = optionalNumber(event.payload.actualTokens);
|
|
138
|
+
projected.set(leaseId, {
|
|
139
|
+
leaseId,
|
|
140
|
+
agentId: previous?.agentId ?? requiredText(event.agentId, "INVALID_CHILD_BUDGET_EVENT"),
|
|
141
|
+
taskId: previous?.taskId ?? requiredText(event.taskId, "INVALID_CHILD_BUDGET_EVENT"),
|
|
142
|
+
reservedUsd,
|
|
143
|
+
reservedTokens,
|
|
144
|
+
status,
|
|
145
|
+
...(actualUsd !== undefined && actualTokens !== undefined
|
|
146
|
+
? { actualUsage: { usd: actualUsd, tokens: actualTokens } }
|
|
147
|
+
: previous?.actualUsage ? { actualUsage: structuredClone(previous.actualUsage) } : {})
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
const leases = [...projected.values()];
|
|
151
|
+
return {
|
|
152
|
+
capUsd: budget.maxUsd,
|
|
153
|
+
...(budget.maxTokens === undefined ? {} : { capTokens: budget.maxTokens }),
|
|
154
|
+
settledUsd: leases.reduce((sum, lease) => sum + (lease.actualUsage?.usd ?? 0), 0),
|
|
155
|
+
settledTokens: leases.reduce((sum, lease) => sum + (lease.actualUsage?.tokens ?? 0), 0),
|
|
156
|
+
leases
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function projectEvidenceBindings(plan, events, artifacts) {
|
|
160
|
+
const admissions = [];
|
|
161
|
+
const rejections = [];
|
|
162
|
+
const conflicts = [];
|
|
163
|
+
const integration = [];
|
|
164
|
+
const globalVerification = [];
|
|
165
|
+
const cleanup = [];
|
|
166
|
+
for (const artifact of artifacts) {
|
|
167
|
+
const value = artifact.value;
|
|
168
|
+
const nestedDecision = isRecord(value.decision) ? value.decision : undefined;
|
|
169
|
+
const decision = nestedDecision ?? (typeof value.candidateId === "string" ? value : undefined);
|
|
170
|
+
const validDecision = decision ? isCanonicalDecision(decision, plan, events) : false;
|
|
171
|
+
if (validDecision && decision?.state === "admitted")
|
|
172
|
+
admissions.push(artifact.path);
|
|
173
|
+
if (validDecision && decision?.state === "rejected")
|
|
174
|
+
rejections.push(artifact.path);
|
|
175
|
+
if (validDecision && (isRecord(value.conflict) || decision?.reasonCode === "integration_conflict"))
|
|
176
|
+
conflicts.push(artifact.path);
|
|
177
|
+
if ((nestedDecision && decision && validDecision && isCanonicalIntegrationEvent(value.event, decision, plan, events))
|
|
178
|
+
|| isCanonicalReconstruction(value, plan))
|
|
179
|
+
integration.push(artifact.path);
|
|
180
|
+
if (isCanonicalGlobalVerification(value, plan, events))
|
|
181
|
+
globalVerification.push(artifact.path);
|
|
182
|
+
if (isCanonicalCleanup(value, plan, artifacts))
|
|
183
|
+
cleanup.push(artifact.path);
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
admissions: uniqueSorted(admissions),
|
|
187
|
+
rejections: uniqueSorted(rejections),
|
|
188
|
+
conflicts: uniqueSorted(conflicts),
|
|
189
|
+
integration: uniqueSorted(integration),
|
|
190
|
+
globalVerification: uniqueSorted(globalVerification),
|
|
191
|
+
cleanup: uniqueSorted(cleanup)
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function isCanonicalDecision(value, plan, events) {
|
|
195
|
+
if (value.schemaVersion !== SWARM_SCHEMA_VERSION
|
|
196
|
+
|| value.swarmId !== plan.swarmId
|
|
197
|
+
|| value.baselineCommit !== plan.baselineCommit
|
|
198
|
+
|| !isNonEmptyText(value.admissionId)
|
|
199
|
+
|| typeof value.candidateId !== "string"
|
|
200
|
+
|| !isNonEmptyText(value.workspaceId)
|
|
201
|
+
|| !isNonEmptyText(value.childRunId)
|
|
202
|
+
|| !isNonEmptyText(value.agentId)
|
|
203
|
+
|| !isStringArray(value.taskIds, true)
|
|
204
|
+
|| !isStringArray(value.changedPaths)
|
|
205
|
+
|| !["admitted", "rejected"].includes(String(value.state))
|
|
206
|
+
|| !SWARM_ADMISSION_REASON_CODES.has(String(value.reasonCode))
|
|
207
|
+
|| (value.state === "admitted" && value.reasonCode !== "admitted")
|
|
208
|
+
|| (value.state === "rejected" && value.reasonCode === "admitted")
|
|
209
|
+
|| !RECEIPT_INTEGRITY_STATES.has(String(value.receiptIntegrity))
|
|
210
|
+
|| (value.state === "admitted" && value.receiptIntegrity !== "verified")
|
|
211
|
+
|| !isNonEmptyText(value.decidedAt)
|
|
212
|
+
|| !isOptionalText(value.preIntegrationTreeHash)
|
|
213
|
+
|| !isOptionalText(value.postIntegrationTreeHash)
|
|
214
|
+
|| !isOptionalText(value.diagnostic))
|
|
215
|
+
return false;
|
|
216
|
+
const expectedTypes = value.state === "admitted"
|
|
217
|
+
? ["CHILD_PATCH_ADMITTED"]
|
|
218
|
+
: value.reasonCode === "integration_conflict" ? ["INTEGRATION_CONFLICT"] : ["CHILD_PATCH_REJECTED"];
|
|
219
|
+
return events.some((event) => (expectedTypes.includes(event.type)
|
|
220
|
+
&& event.payload.candidateId === value.candidateId
|
|
221
|
+
&& event.swarmId === plan.swarmId
|
|
222
|
+
&& event.planHash === plan.planHash
|
|
223
|
+
&& event.parentPolicyVersion === plan.parentContract.policyVersion
|
|
224
|
+
&& event.taskId !== undefined
|
|
225
|
+
&& value.taskIds.includes(event.taskId)
|
|
226
|
+
&& event.agentId === value.agentId
|
|
227
|
+
&& event.childRunId === value.childRunId
|
|
228
|
+
&& Number.isInteger(event.sequence)
|
|
229
|
+
&& event.sequence > 0
|
|
230
|
+
&& isNonEmptyText(event.idempotencyKey)
|
|
231
|
+
&& isNonEmptyText(event.timestamp)));
|
|
232
|
+
}
|
|
233
|
+
function isCanonicalIntegrationEvent(candidateEvent, decision, plan, events) {
|
|
234
|
+
if (!isRecord(candidateEvent) || !isRecord(candidateEvent.payload))
|
|
235
|
+
return false;
|
|
236
|
+
const expectedType = decision.state === "admitted"
|
|
237
|
+
? "CHILD_PATCH_ADMITTED"
|
|
238
|
+
: decision.reasonCode === "integration_conflict" ? "INTEGRATION_CONFLICT" : "CHILD_PATCH_REJECTED";
|
|
239
|
+
if (candidateEvent.type !== expectedType
|
|
240
|
+
|| candidateEvent.swarmId !== plan.swarmId
|
|
241
|
+
|| candidateEvent.parentPolicyVersion !== plan.parentContract.policyVersion
|
|
242
|
+
|| candidateEvent.payload.candidateId !== decision.candidateId)
|
|
243
|
+
return false;
|
|
244
|
+
return events.some((event) => event.planHash === plan.planHash
|
|
245
|
+
&& Number.isInteger(event.sequence)
|
|
246
|
+
&& event.sequence > 0
|
|
247
|
+
&& isNonEmptyText(event.idempotencyKey)
|
|
248
|
+
&& stableJson(projectLiveEvent(event)) === stableJson(candidateEvent));
|
|
249
|
+
}
|
|
250
|
+
function isCanonicalReconstruction(value, plan) {
|
|
251
|
+
return value.schemaVersion === SWARM_SCHEMA_VERSION
|
|
252
|
+
&& value.swarmId === plan.swarmId
|
|
253
|
+
&& value.baselineCommit === plan.baselineCommit
|
|
254
|
+
&& typeof value.reconstructionId === "string"
|
|
255
|
+
&& typeof value.failedCandidateId === "string"
|
|
256
|
+
&& typeof value.previousWorkspaceId === "string"
|
|
257
|
+
&& typeof value.replacementWorkspaceId === "string"
|
|
258
|
+
&& ["completed", "failed"].includes(String(value.state))
|
|
259
|
+
&& (value.state !== "completed" || value.expectedTreeHash === value.actualTreeHash);
|
|
260
|
+
}
|
|
261
|
+
function isCanonicalGlobalVerification(value, plan, events) {
|
|
262
|
+
if (value.schemaVersion !== SWARM_SCHEMA_VERSION
|
|
263
|
+
|| value.swarmId !== plan.swarmId
|
|
264
|
+
|| value.parentPolicyVersion !== plan.parentContract.policyVersion
|
|
265
|
+
|| value.baselineCommit !== plan.baselineCommit
|
|
266
|
+
|| typeof value.verificationId !== "string"
|
|
267
|
+
|| !isNonEmptyText(value.workspaceId)
|
|
268
|
+
|| typeof value.integratedTreeHash !== "string"
|
|
269
|
+
|| !isStringArray(value.commands)
|
|
270
|
+
|| stableJson(value.commands) !== stableJson(plan.parentContract.globalVerifierStack.map((step) => step.command))
|
|
271
|
+
|| !["pending", "passed", "failed", "unknown"].includes(String(value.commandState))
|
|
272
|
+
|| !["pending", "clean", "mutated", "unknown"].includes(String(value.mutationState))
|
|
273
|
+
|| !isNonEmptyText(value.startedAt)
|
|
274
|
+
|| !isOptionalText(value.completedAt)
|
|
275
|
+
|| (["passed", "failed"].includes(String(value.commandState)) && !isNonEmptyText(value.completedAt))
|
|
276
|
+
|| !Array.isArray(value.subprocessResults)
|
|
277
|
+
|| !value.subprocessResults.every(isCanonicalVerifierSubprocess))
|
|
278
|
+
return false;
|
|
279
|
+
const expectedType = value.commandState === "passed" && value.mutationState === "clean"
|
|
280
|
+
? "GLOBAL_VERIFIER_PASSED"
|
|
281
|
+
: "GLOBAL_VERIFIER_FAILED";
|
|
282
|
+
const matching = events.filter((event) => ((event.type === "GLOBAL_VERIFIER_PASSED" || event.type === "GLOBAL_VERIFIER_FAILED")
|
|
283
|
+
&& event.payload.verificationId === value.verificationId
|
|
284
|
+
&& event.payload.integratedTreeHash === value.integratedTreeHash
|
|
285
|
+
&& event.swarmId === plan.swarmId
|
|
286
|
+
&& event.planHash === plan.planHash
|
|
287
|
+
&& event.parentPolicyVersion === plan.parentContract.policyVersion
|
|
288
|
+
&& Number.isInteger(event.sequence)
|
|
289
|
+
&& event.sequence > 0
|
|
290
|
+
&& isNonEmptyText(event.idempotencyKey)
|
|
291
|
+
&& isNonEmptyText(event.timestamp)));
|
|
292
|
+
return matching.length === 1 && matching[0]?.type === expectedType;
|
|
293
|
+
}
|
|
294
|
+
function isCanonicalCleanup(value, plan, artifacts) {
|
|
295
|
+
if (value.schemaVersion !== SWARM_SCHEMA_VERSION
|
|
296
|
+
|| value.swarmId !== plan.swarmId
|
|
297
|
+
|| typeof value.cleanupId !== "string"
|
|
298
|
+
|| typeof value.workspaceId !== "string"
|
|
299
|
+
|| !["pending", "completed", "failed", "cleanup_pending"].includes(String(value.state))
|
|
300
|
+
|| !["child", "integration", "verifier"].includes(String(value.workspaceKind))
|
|
301
|
+
|| !["not_required", "pending", "closed", "failed"].includes(String(value.processCloseState))
|
|
302
|
+
|| !["pending", "removed", "failed"].includes(String(value.removalState))
|
|
303
|
+
|| typeof value.evidencePersisted !== "boolean"
|
|
304
|
+
|| !isNonEmptyText(value.attemptedAt)
|
|
305
|
+
|| !isOptionalText(value.completedAt)
|
|
306
|
+
|| !isOptionalText(value.errorCode)
|
|
307
|
+
|| (value.state === "completed" && (!isNonEmptyText(value.completedAt) || value.removalState !== "removed"))
|
|
308
|
+
|| (value.state !== "completed" && !isNonEmptyText(value.errorCode)))
|
|
309
|
+
return false;
|
|
310
|
+
return artifacts.some((artifact) => {
|
|
311
|
+
const completionCleanup = artifact.value.cleanup;
|
|
312
|
+
return isRecord(completionCleanup)
|
|
313
|
+
&& stableJson(completionCleanup) === stableJson(value);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
function projectLiveEvent(event) {
|
|
317
|
+
return {
|
|
318
|
+
type: event.type,
|
|
319
|
+
swarmId: event.swarmId,
|
|
320
|
+
timestamp: event.timestamp,
|
|
321
|
+
parentPolicyVersion: event.parentPolicyVersion,
|
|
322
|
+
...(event.taskId ? { taskId: event.taskId } : {}),
|
|
323
|
+
...(event.agentId ? { agentId: event.agentId } : {}),
|
|
324
|
+
...(event.childRunId ? { childRunId: event.childRunId } : {}),
|
|
325
|
+
...(event.failureClass ? { failureClass: event.failureClass } : {}),
|
|
326
|
+
payload: structuredClone(event.payload)
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function isCanonicalVerifierSubprocess(value) {
|
|
330
|
+
return isRecord(value)
|
|
331
|
+
&& isNonEmptyText(value.command)
|
|
332
|
+
&& typeof value.launched === "boolean"
|
|
333
|
+
&& typeof value.completed === "boolean"
|
|
334
|
+
&& typeof value.timedOut === "boolean"
|
|
335
|
+
&& (value.exitCode === null || (typeof value.exitCode === "number" && Number.isInteger(value.exitCode)))
|
|
336
|
+
&& isOptionalText(value.signal)
|
|
337
|
+
&& isNonEmptyText(value.startedAt)
|
|
338
|
+
&& isOptionalText(value.completedAt);
|
|
339
|
+
}
|
|
340
|
+
function isStringArray(value, requireNonEmpty = false) {
|
|
341
|
+
return Array.isArray(value)
|
|
342
|
+
&& (!requireNonEmpty || value.length > 0)
|
|
343
|
+
&& value.every((item) => isNonEmptyText(item));
|
|
344
|
+
}
|
|
345
|
+
function isNonEmptyText(value) {
|
|
346
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
347
|
+
}
|
|
348
|
+
function isOptionalText(value) {
|
|
349
|
+
return value === undefined || isNonEmptyText(value);
|
|
350
|
+
}
|
|
351
|
+
const RECEIPT_INTEGRITY_STATES = new Set([
|
|
352
|
+
"verified", "unsigned", "tamper_detected", "relocated", "material_missing", "selector_noncanonical"
|
|
353
|
+
]);
|
|
354
|
+
const SWARM_ADMISSION_REASON_CODES = new Set([
|
|
355
|
+
"admitted", "child_not_terminal", "process_active", "identity_mismatch", "receipt_not_verified",
|
|
356
|
+
"receipt_scope_mismatch", "stale_baseline", "git_inventory_failed", "invalid_concrete_path", "unsafe_path",
|
|
357
|
+
"undeclared_path", "path_not_allowed", "path_denied", "task_scope_violation", "candidate_changed_during_capture",
|
|
358
|
+
"empty_candidate", "patch_hash_mismatch", "integration_precondition_failed", "integration_conflict",
|
|
359
|
+
"integration_apply_failed", "artifact_persistence_failed"
|
|
360
|
+
]);
|
|
361
|
+
function requiredNumber(value, code) {
|
|
362
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
363
|
+
throw new SwarmParentReceiptError(code, "Persisted budget evidence is malformed.");
|
|
364
|
+
}
|
|
365
|
+
return value;
|
|
366
|
+
}
|
|
367
|
+
function optionalNumber(value) {
|
|
368
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
369
|
+
}
|
|
370
|
+
function uniqueSorted(values) {
|
|
371
|
+
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
372
|
+
}
|
|
373
|
+
export function verifyParentSwarmReceipt(receipt) {
|
|
374
|
+
if (receipt.schemaVersion !== SWARM_PARENT_RECEIPT_SCHEMA_VERSION) {
|
|
375
|
+
return { ok: false, reason: "UNKNOWN_RECEIPT_SCHEMA" };
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
if (stableJson(receipt.blockedActions) !== stableJson(projectBlockedActions(receipt.events))) {
|
|
379
|
+
return { ok: false, reason: "BLOCKED_ACTION_PROJECTION_MISMATCH" };
|
|
380
|
+
}
|
|
381
|
+
if (stableJson(receipt.reassignments) !== stableJson(projectReassignments(receipt.events))) {
|
|
382
|
+
return { ok: false, reason: "REASSIGNMENT_PROJECTION_MISMATCH" };
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
return { ok: false, reason: error instanceof SwarmParentReceiptError ? error.code : "INVALID_EVENT_PROJECTION" };
|
|
387
|
+
}
|
|
388
|
+
const { receiptId, receiptSha256, ...base } = receipt;
|
|
389
|
+
const expectedHash = sha256(stableJson(base));
|
|
390
|
+
if (receiptSha256 !== expectedHash || receiptId !== `swarm-receipt-${expectedHash.slice(0, 16)}`) {
|
|
391
|
+
return { ok: false, reason: "RECEIPT_HASH_MISMATCH" };
|
|
392
|
+
}
|
|
393
|
+
return { ok: true };
|
|
394
|
+
}
|
|
395
|
+
function projectBlockedActions(events) {
|
|
396
|
+
return events
|
|
397
|
+
.filter((event) => event.type === "ACTION_BLOCKED")
|
|
398
|
+
.map((event) => ({
|
|
399
|
+
eventId: requiredText(event.idempotencyKey, "INVALID_BLOCKED_ACTION_EVENT"),
|
|
400
|
+
sequence: event.sequence,
|
|
401
|
+
agentId: requiredText(event.agentId, "INVALID_BLOCKED_ACTION_EVENT"),
|
|
402
|
+
taskId: requiredText(event.taskId, "INVALID_BLOCKED_ACTION_EVENT"),
|
|
403
|
+
attemptId: requiredText(event.payload.attemptId, "INVALID_BLOCKED_ACTION_EVENT"),
|
|
404
|
+
action: requiredText(event.payload.action, "INVALID_BLOCKED_ACTION_EVENT"),
|
|
405
|
+
reason: requiredText(event.payload.reason, "INVALID_BLOCKED_ACTION_EVENT"),
|
|
406
|
+
timestamp: requiredText(event.timestamp, "INVALID_BLOCKED_ACTION_EVENT")
|
|
407
|
+
}))
|
|
408
|
+
.sort(bySequence);
|
|
409
|
+
}
|
|
410
|
+
function projectReassignments(events) {
|
|
411
|
+
return events
|
|
412
|
+
.filter((event) => event.type === "TASK_REASSIGNED")
|
|
413
|
+
.map((event) => ({
|
|
414
|
+
eventId: requiredText(event.idempotencyKey, "INVALID_REASSIGNMENT_EVENT"),
|
|
415
|
+
sequence: event.sequence,
|
|
416
|
+
taskId: requiredText(event.taskId, "INVALID_REASSIGNMENT_EVENT"),
|
|
417
|
+
fromAgentId: requiredText(event.payload.fromAgentId, "INVALID_REASSIGNMENT_EVENT"),
|
|
418
|
+
toAgentId: requiredText(event.payload.toAgentId, "INVALID_REASSIGNMENT_EVENT"),
|
|
419
|
+
fromAttemptId: requiredText(event.payload.fromAttemptId, "INVALID_REASSIGNMENT_EVENT"),
|
|
420
|
+
toAttemptId: requiredText(event.payload.toAttemptId, "INVALID_REASSIGNMENT_EVENT"),
|
|
421
|
+
reason: requiredText(event.payload.reason, "INVALID_REASSIGNMENT_EVENT"),
|
|
422
|
+
timestamp: requiredText(event.timestamp, "INVALID_REASSIGNMENT_EVENT")
|
|
423
|
+
}))
|
|
424
|
+
.sort(bySequence);
|
|
425
|
+
}
|
|
426
|
+
function projectChildReceipts(sealed) {
|
|
427
|
+
const startedChildRuns = new Set(sealed.model.events
|
|
428
|
+
.filter((event) => event.type === "CHILD_STARTED" && event.childRunId)
|
|
429
|
+
.map((event) => event.childRunId));
|
|
430
|
+
const terminalChildren = sealed.model.events.filter((event) => (event.childRunId
|
|
431
|
+
&& startedChildRuns.has(event.childRunId)
|
|
432
|
+
&& ["CHILD_VERIFIED", "CHILD_STOPPED", "CHILD_NEEDS_REVIEW"].includes(event.type)));
|
|
433
|
+
return terminalChildren.map((event) => {
|
|
434
|
+
const matches = sealed.model.artifacts.filter((artifact) => (artifact.path.startsWith("evidence/child-completions/") && artifact.value.childRunId === event.childRunId));
|
|
435
|
+
if (matches.length !== 1) {
|
|
436
|
+
throw new SwarmParentReceiptError("MISSING_CHILD_EVIDENCE", "Terminal child evidence must have one completion artifact.");
|
|
437
|
+
}
|
|
438
|
+
const value = matches[0].value;
|
|
439
|
+
const receipt = isRecord(value.receipt) ? value.receipt : undefined;
|
|
440
|
+
const integrity = receipt && isRecord(receipt.integrity) ? receipt.integrity : undefined;
|
|
441
|
+
if (integrity?.state !== "verified") {
|
|
442
|
+
throw new SwarmParentReceiptError("UNVERIFIED_CHILD_EVIDENCE", "Child receipt integrity must be verified.");
|
|
443
|
+
}
|
|
444
|
+
const childRunId = requiredText(value.childRunId, "INVALID_CHILD_EVIDENCE");
|
|
445
|
+
const integrityFile = sealed.index.files.find((file) => file.path === `${childRunId}/receipt-integrity.json`);
|
|
446
|
+
if (!integrityFile) {
|
|
447
|
+
throw new SwarmParentReceiptError("MISSING_CHILD_INTEGRITY_FILE", "Child receipt integrity material is absent from the sealed index.");
|
|
448
|
+
}
|
|
449
|
+
const taskIds = value.taskIds;
|
|
450
|
+
if (!Array.isArray(taskIds) || taskIds.some((taskId) => typeof taskId !== "string" || taskId.length === 0)) {
|
|
451
|
+
throw new SwarmParentReceiptError("INVALID_CHILD_EVIDENCE", "Child completion task IDs are malformed.");
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
childRunId,
|
|
455
|
+
agentId: requiredText(value.agentId, "INVALID_CHILD_EVIDENCE"),
|
|
456
|
+
attemptId: requiredText(value.attemptId, "INVALID_CHILD_EVIDENCE"),
|
|
457
|
+
taskIds: [...taskIds],
|
|
458
|
+
receiptIntegritySha256: integrityFile.sha256
|
|
459
|
+
};
|
|
460
|
+
}).sort((left, right) => left.childRunId.localeCompare(right.childRunId));
|
|
461
|
+
}
|
|
462
|
+
function requiredText(value, code) {
|
|
463
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
464
|
+
throw new SwarmParentReceiptError(code, "Persisted receipt evidence is missing required identity.");
|
|
465
|
+
}
|
|
466
|
+
return value;
|
|
467
|
+
}
|
|
468
|
+
function optionalText(value) {
|
|
469
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
470
|
+
}
|
|
471
|
+
function bySequence(left, right) {
|
|
472
|
+
return left.sequence - right.sequence;
|
|
473
|
+
}
|
|
474
|
+
function sha256(value) {
|
|
475
|
+
return createHash("sha256").update(value).digest("hex");
|
|
476
|
+
}
|
|
477
|
+
function stableJson(value) {
|
|
478
|
+
return JSON.stringify(canonicalize(value));
|
|
479
|
+
}
|
|
480
|
+
function canonicalize(value) {
|
|
481
|
+
if (Array.isArray(value))
|
|
482
|
+
return value.map(canonicalize);
|
|
483
|
+
if (!isRecord(value))
|
|
484
|
+
return value;
|
|
485
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalize(value[key])]));
|
|
486
|
+
}
|
|
487
|
+
function isRecord(value) {
|
|
488
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
489
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SwarmParentReceipt } from "../../contracts/index.js";
|
|
2
|
+
import { type SwarmReceiptIntegritySummary } from "../persistence/swarm-integrity.js";
|
|
3
|
+
interface SwarmReceiptSealCommit {
|
|
4
|
+
schemaVersion: "martin.swarm-receipt-seal.v1";
|
|
5
|
+
swarmId: string;
|
|
6
|
+
planHash: string;
|
|
7
|
+
receiptSha256: string;
|
|
8
|
+
evidenceIndexSha256: string;
|
|
9
|
+
integrityMaterialSha256: string;
|
|
10
|
+
committedAt: string;
|
|
11
|
+
commitHmacSha256: string;
|
|
12
|
+
}
|
|
13
|
+
export interface SwarmReceiptProjection {
|
|
14
|
+
receipt: SwarmParentReceipt;
|
|
15
|
+
integrity: SwarmReceiptIntegritySummary;
|
|
16
|
+
seal: SwarmReceiptSealCommit;
|
|
17
|
+
}
|
|
18
|
+
export declare function readSwarmReceiptProjection(input: {
|
|
19
|
+
runsRoot: string;
|
|
20
|
+
swarmId: string;
|
|
21
|
+
}): Promise<SwarmReceiptProjection>;
|
|
22
|
+
export declare function verifySwarmReceiptProjection(input: {
|
|
23
|
+
runsRoot: string;
|
|
24
|
+
swarmId: string;
|
|
25
|
+
}): Promise<SwarmReceiptIntegritySummary>;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, readFile, realpath } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { resolveSwarmReceiptIntegrityPath, verifySwarmReceiptSealCommitAuthentication, verifySwarmReceiptIntegrityFromFiles, } from "../persistence/swarm-integrity.js";
|
|
5
|
+
import { readAndSealSwarmEvidence } from "./evidence.js";
|
|
6
|
+
import { buildParentSwarmReceipt } from "./parent-receipt.js";
|
|
7
|
+
import { assertSwarmPathIdentifier } from "./workspaces.js";
|
|
8
|
+
export async function readSwarmReceiptProjection(input) {
|
|
9
|
+
assertSwarmPathIdentifier(input.swarmId, "swarm ID");
|
|
10
|
+
const runsRoot = await realpath(resolve(input.runsRoot));
|
|
11
|
+
const swarmsRoot = await realpath(join(runsRoot, "_swarms"));
|
|
12
|
+
assertContainedOrSame(runsRoot, swarmsRoot, "Swarm projection escaped the runs root.");
|
|
13
|
+
const swarmRoot = await realpath(join(swarmsRoot, input.swarmId));
|
|
14
|
+
assertStrictlyContained(swarmsRoot, swarmRoot, "Swarm projection escaped the swarm root.");
|
|
15
|
+
const evidenceRootExpected = join(swarmRoot, "evidence");
|
|
16
|
+
const evidenceRoot = await resolveExactDirectory(swarmRoot, evidenceRootExpected);
|
|
17
|
+
assertStrictlyContained(swarmRoot, evidenceRoot, "Swarm projection escaped the evidence root.");
|
|
18
|
+
const receiptPath = await resolveExactRegularFile(evidenceRoot, join(evidenceRoot, "parent-receipt.json"));
|
|
19
|
+
const indexPath = await resolveExactRegularFile(evidenceRoot, join(evidenceRoot, "evidence-index.json"));
|
|
20
|
+
const integrityPath = await resolveExactRegularFile(evidenceRoot, resolveSwarmReceiptIntegrityPath(runsRoot, input.swarmId));
|
|
21
|
+
const sealPath = await resolveExactRegularFile(evidenceRoot, join(evidenceRoot, "swarm-receipt-seal.json"));
|
|
22
|
+
for (const path of [receiptPath, indexPath, integrityPath, sealPath]) {
|
|
23
|
+
assertStrictlyContained(evidenceRoot, path, "Swarm projection artifact escaped the evidence root.");
|
|
24
|
+
}
|
|
25
|
+
const [receiptRaw, indexRaw, integrityRaw, sealRaw] = await Promise.all([
|
|
26
|
+
readFile(receiptPath, "utf8"),
|
|
27
|
+
readFile(indexPath, "utf8"),
|
|
28
|
+
readFile(integrityPath, "utf8"),
|
|
29
|
+
readFile(sealPath, "utf8")
|
|
30
|
+
]);
|
|
31
|
+
const receipt = parseCommittedProjectionJson(receiptRaw, "parent receipt");
|
|
32
|
+
const seal = parseCommittedProjectionJson(sealRaw, "receipt seal");
|
|
33
|
+
if (seal.schemaVersion !== "martin.swarm-receipt-seal.v1"
|
|
34
|
+
|| seal.swarmId !== input.swarmId
|
|
35
|
+
|| seal.planHash !== receipt.planHash
|
|
36
|
+
|| seal.receiptSha256 !== sha256Text(receiptRaw)
|
|
37
|
+
|| seal.evidenceIndexSha256 !== sha256Text(indexRaw)
|
|
38
|
+
|| seal.integrityMaterialSha256 !== sha256Text(integrityRaw)
|
|
39
|
+
|| !await verifySwarmReceiptSealCommitAuthentication({
|
|
40
|
+
runsRoot,
|
|
41
|
+
swarmId: input.swarmId,
|
|
42
|
+
commit: seal
|
|
43
|
+
})) {
|
|
44
|
+
throw codedRuntimeError("SWARM_SEAL_COMMIT_MISMATCH", "Committed swarm receipt seal is missing or tampered.");
|
|
45
|
+
}
|
|
46
|
+
const integrity = await verifySwarmReceiptIntegrityFromFiles({ runsRoot, swarmId: input.swarmId });
|
|
47
|
+
if (integrity.state !== "verified") {
|
|
48
|
+
throw codedRuntimeError("SWARM_SEAL_INTEGRITY_FAILED", "Committed swarm receipt integrity did not verify.");
|
|
49
|
+
}
|
|
50
|
+
const canonical = await readAndSealSwarmEvidence({ rootDir: runsRoot, swarmId: input.swarmId });
|
|
51
|
+
const canonicalReceipt = buildParentSwarmReceipt(canonical);
|
|
52
|
+
if (receiptRaw !== `${JSON.stringify(canonicalReceipt, null, 2)}\n`) {
|
|
53
|
+
throw codedRuntimeError("SWARM_SEAL_COMMIT_MISMATCH", "Committed swarm receipt disagrees with canonical evidence.");
|
|
54
|
+
}
|
|
55
|
+
return { receipt, integrity, seal };
|
|
56
|
+
}
|
|
57
|
+
async function resolveExactDirectory(parent, expected) {
|
|
58
|
+
const metadata = await lstat(expected);
|
|
59
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
|
|
60
|
+
throw codedRuntimeError("SWARM_SEAL_PATH_ALIAS", "Swarm seal directory is not an exact canonical directory.");
|
|
61
|
+
}
|
|
62
|
+
const canonical = await realpath(expected);
|
|
63
|
+
assertStrictlyContained(parent, canonical, "Swarm seal directory escaped its parent.");
|
|
64
|
+
if (!sameFilesystemPath(canonical, resolve(expected))) {
|
|
65
|
+
throw codedRuntimeError("SWARM_SEAL_PATH_ALIAS", "Swarm seal directory is aliased.");
|
|
66
|
+
}
|
|
67
|
+
return canonical;
|
|
68
|
+
}
|
|
69
|
+
async function resolveExactRegularFile(parent, expected) {
|
|
70
|
+
const metadata = await lstat(expected);
|
|
71
|
+
if (metadata.isSymbolicLink() || !metadata.isFile()) {
|
|
72
|
+
throw codedRuntimeError("SWARM_SEAL_PATH_ALIAS", "Swarm seal artifact is not an exact regular file.");
|
|
73
|
+
}
|
|
74
|
+
const canonical = await realpath(expected);
|
|
75
|
+
assertStrictlyContained(parent, canonical, "Swarm seal artifact escaped its parent.");
|
|
76
|
+
if (!sameFilesystemPath(canonical, resolve(expected))) {
|
|
77
|
+
throw codedRuntimeError("SWARM_SEAL_PATH_ALIAS", "Swarm seal artifact is aliased.");
|
|
78
|
+
}
|
|
79
|
+
return canonical;
|
|
80
|
+
}
|
|
81
|
+
function sameFilesystemPath(left, right) {
|
|
82
|
+
return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
83
|
+
}
|
|
84
|
+
export async function verifySwarmReceiptProjection(input) {
|
|
85
|
+
try {
|
|
86
|
+
return (await readSwarmReceiptProjection(input)).integrity;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
return {
|
|
90
|
+
state: "tamper_detected",
|
|
91
|
+
reason: error instanceof Error && "code" in error ? String(error.code) : "swarm_seal_unavailable"
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function sha256Text(value) {
|
|
96
|
+
return createHash("sha256").update(value).digest("hex");
|
|
97
|
+
}
|
|
98
|
+
function assertContainedOrSame(parent, candidate, message) {
|
|
99
|
+
const relation = relative(parent, candidate);
|
|
100
|
+
if (relation === "")
|
|
101
|
+
return;
|
|
102
|
+
assertStrictlyContained(parent, candidate, message);
|
|
103
|
+
}
|
|
104
|
+
function assertStrictlyContained(parent, candidate, message) {
|
|
105
|
+
const relation = relative(parent, candidate);
|
|
106
|
+
if (!relation || relation === ".." || relation.startsWith(`..${sep}`) || isAbsolute(relation)) {
|
|
107
|
+
throw codedRuntimeError("LIVE_SWARM_PATH_ESCAPE", message);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function codedRuntimeError(code, message) {
|
|
111
|
+
return Object.assign(new Error(message), { code });
|
|
112
|
+
}
|
|
113
|
+
function parseCommittedProjectionJson(source, label) {
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(source);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
throw codedRuntimeError("MALFORMED_JSON", `Committed swarm ${label} is malformed.`);
|
|
119
|
+
}
|
|
120
|
+
}
|