@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,362 @@
|
|
|
1
|
+
import { createHash, createHmac } from "node:crypto";
|
|
2
|
+
import { realpath } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { SWARM_PARENT_RECEIPT_SCHEMA_VERSION, SWARM_SCHEMA_VERSION, computeSwarmLivePlanHash, } from "../../contracts/index.js";
|
|
5
|
+
import { SWARM_EVIDENCE_INDEX_SCHEMA_VERSION, SWARM_HOSTED_SCHEMA_VERSION, canonicalSwarmHostedTransportBytes, computeSwarmHostedEnvelopeIdentity, validateSwarmHostedEnvelope, } from "../../contracts/swarm-hosted.js";
|
|
6
|
+
import { readPrivateIntegrityKey } from "../persistence/integrity.js";
|
|
7
|
+
import { readSwarmReceiptProjection } from "./receipt-projection.js";
|
|
8
|
+
import { assertSwarmPathIdentifier } from "./workspaces.js";
|
|
9
|
+
import { readSwarmOperationalState } from "./operations.js";
|
|
10
|
+
export const SWARM_HOSTED_TRANSPORT_DOMAIN = "martin.swarm-hosted.v1";
|
|
11
|
+
async function deriveSwarmHostedTransportKeyMaterialFromReceipt(input) {
|
|
12
|
+
assertSwarmPathIdentifier(input.swarmId, "swarm ID");
|
|
13
|
+
const runsRoot = await realpath(resolve(input.runsRoot));
|
|
14
|
+
const receiptSecret = await readPrivateIntegrityKey(runsRoot, `swarm-${input.swarmId}.key`);
|
|
15
|
+
if (!receiptSecret)
|
|
16
|
+
throw new Error("SWARM_SEAL_KEY_UNAVAILABLE");
|
|
17
|
+
const secret = createHmac("sha256", receiptSecret)
|
|
18
|
+
.update(`${input.domain}\n${input.swarmId}\n`)
|
|
19
|
+
.digest("hex");
|
|
20
|
+
return { secret, keyId: `hosted-${sha256(secret).slice(0, 16)}` };
|
|
21
|
+
}
|
|
22
|
+
const productionDependencies = {
|
|
23
|
+
readOperationalState: (input) => readSwarmOperationalState(input),
|
|
24
|
+
readReceiptProjection: (input) => readSwarmReceiptProjection(input),
|
|
25
|
+
ensureHostedTransportKey: deriveHostedTransportKeyMaterial,
|
|
26
|
+
};
|
|
27
|
+
/** @internal Direct-module seam for proving derivation never creates another secret file. */
|
|
28
|
+
export async function deriveHostedTransportKeyMaterial(input) {
|
|
29
|
+
try {
|
|
30
|
+
return await deriveSwarmHostedTransportKeyMaterialFromReceipt({
|
|
31
|
+
...input,
|
|
32
|
+
domain: SWARM_HOSTED_TRANSPORT_DOMAIN,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw hostedError("SWARM_HOSTED_KEY_UNAVAILABLE", "Authenticated receipt key material is unavailable for hosted derivation.");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function buildSwarmHostedEnvelope(input) {
|
|
40
|
+
return buildSwarmHostedEnvelopeWithDependencies(input, productionDependencies);
|
|
41
|
+
}
|
|
42
|
+
export async function buildSwarmHostedEnvelopeWithDependencies(input, dependencies) {
|
|
43
|
+
const selector = { runsRoot: input.runsRoot, swarmId: input.swarmId };
|
|
44
|
+
const [operational, projection] = await Promise.all([
|
|
45
|
+
dependencies.readOperationalState(selector),
|
|
46
|
+
dependencies.readReceiptProjection(selector),
|
|
47
|
+
]);
|
|
48
|
+
assertAuthenticatedSource(input, operational, projection);
|
|
49
|
+
const unsigned = projectEnvelope(input.runtimeVersion, operational, projection);
|
|
50
|
+
const key = await dependencies.ensureHostedTransportKey(selector);
|
|
51
|
+
const keyLocatorHash = sha256(`${SWARM_HOSTED_TRANSPORT_DOMAIN}\n${input.swarmId}\n${key.keyId}\n`);
|
|
52
|
+
const envelopeWithLocator = {
|
|
53
|
+
...unsigned,
|
|
54
|
+
transportSignature: {
|
|
55
|
+
algorithm: "hmac-sha256",
|
|
56
|
+
keyId: key.keyId,
|
|
57
|
+
keyLocatorHash,
|
|
58
|
+
signatureHmacSha256: "0".repeat(64),
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
envelopeWithLocator.envelopeId = computeSwarmHostedEnvelopeIdentity(envelopeWithLocator);
|
|
62
|
+
const signatureHmacSha256 = createHmac("sha256", key.secret)
|
|
63
|
+
.update(`${SWARM_HOSTED_TRANSPORT_DOMAIN}\n${canonicalSwarmHostedTransportBytes(envelopeWithLocator)}`)
|
|
64
|
+
.digest("hex");
|
|
65
|
+
const envelope = {
|
|
66
|
+
...envelopeWithLocator,
|
|
67
|
+
transportSignature: { ...envelopeWithLocator.transportSignature, signatureHmacSha256 },
|
|
68
|
+
};
|
|
69
|
+
const validation = validateSwarmHostedEnvelope(envelope);
|
|
70
|
+
if (!validation.ok) {
|
|
71
|
+
throw hostedError("SWARM_HOSTED_PROJECTION_INVALID", `Hosted swarm projection failed validation: ${validation.errors.map((error) => `${error.code}:${error.path}`).join(",")}`);
|
|
72
|
+
}
|
|
73
|
+
const canonicalBody = `${JSON.stringify(envelope)}\n`;
|
|
74
|
+
return {
|
|
75
|
+
envelope,
|
|
76
|
+
canonicalBody,
|
|
77
|
+
payloadSha256: sha256(canonicalBody),
|
|
78
|
+
envelopeIdentity: envelope.envelopeId,
|
|
79
|
+
transportKey: { keyId: key.keyId, keyLocatorHash, domain: SWARM_HOSTED_TRANSPORT_DOMAIN },
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function projectEnvelope(runtimeVersion, operational, projection) {
|
|
83
|
+
const { plan } = operational;
|
|
84
|
+
const { receipt, integrity } = projection;
|
|
85
|
+
const childReceiptById = new Map(receipt.childReceipts.map((child) => [child.childRunId, child]));
|
|
86
|
+
const events = receipt.events.map((event) => projectEvent(event, childReceiptById));
|
|
87
|
+
const eventById = new Map(events.map((event) => [event.eventId, event]));
|
|
88
|
+
const plannedTaskAgent = new Map(plan.tasks.map((task) => [task.taskId, task.assignedAgentId]));
|
|
89
|
+
const effectiveTaskAgent = new Map(receipt.tasks.map((task) => [task.taskId, task.assignedAgentId]));
|
|
90
|
+
const agentTasks = new Map(receipt.agents.map((agent) => [agent.agentId, new Set(agent.contract.taskIds)]));
|
|
91
|
+
for (const task of receipt.tasks) {
|
|
92
|
+
for (const agentId of [plannedTaskAgent.get(task.taskId), effectiveTaskAgent.get(task.taskId)]) {
|
|
93
|
+
if (agentId)
|
|
94
|
+
agentTasks.get(agentId)?.add(task.taskId);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const agentById = new Map(receipt.agents.map((agent) => [agent.agentId, agent]));
|
|
98
|
+
const parentEvent = findSingleEvent(events, parentTerminalType(receipt.parentOutcome.state), undefined);
|
|
99
|
+
const globalEvent = receipt.globalVerificationId
|
|
100
|
+
? findSingleEvent(events, receipt.taskVerificationState === "passed" ? "GLOBAL_VERIFIER_PASSED" : "GLOBAL_VERIFIER_FAILED", receipt.globalVerificationId)
|
|
101
|
+
: undefined;
|
|
102
|
+
const envelope = {
|
|
103
|
+
schemaVersion: SWARM_HOSTED_SCHEMA_VERSION,
|
|
104
|
+
envelopeId: "0".repeat(64),
|
|
105
|
+
sourceSchemas: {
|
|
106
|
+
swarm: SWARM_SCHEMA_VERSION,
|
|
107
|
+
receipt: SWARM_PARENT_RECEIPT_SCHEMA_VERSION,
|
|
108
|
+
evidenceIndex: SWARM_EVIDENCE_INDEX_SCHEMA_VERSION,
|
|
109
|
+
},
|
|
110
|
+
sourceIdentities: {
|
|
111
|
+
receiptId: receipt.receiptId,
|
|
112
|
+
receiptSha256: receipt.receiptSha256,
|
|
113
|
+
evidenceIndexSha256: receipt.evidenceIndexSha256,
|
|
114
|
+
eventChainSha256: integrity.eventChainHead,
|
|
115
|
+
},
|
|
116
|
+
runtimeVersion,
|
|
117
|
+
createdAt: receipt.sealedAt,
|
|
118
|
+
swarm: {
|
|
119
|
+
swarmId: receipt.swarmId,
|
|
120
|
+
workspaceId: plan.workspaceId,
|
|
121
|
+
projectId: plan.projectId,
|
|
122
|
+
planHash: receipt.planHash,
|
|
123
|
+
baselineCommit: receipt.baselineCommit,
|
|
124
|
+
},
|
|
125
|
+
topology: {
|
|
126
|
+
tasks: receipt.tasks.map((task) => ({
|
|
127
|
+
taskId: task.taskId,
|
|
128
|
+
required: task.required,
|
|
129
|
+
dependsOn: [...task.dependsOn],
|
|
130
|
+
...(plannedTaskAgent.get(task.taskId) ? { plannedAgentId: plannedTaskAgent.get(task.taskId) } : {}),
|
|
131
|
+
...(task.assignedAgentId ? { effectiveAgentId: task.assignedAgentId } : {}),
|
|
132
|
+
status: task.status,
|
|
133
|
+
})),
|
|
134
|
+
agents: receipt.agents.map((agent) => ({
|
|
135
|
+
agentId: agent.agentId,
|
|
136
|
+
role: agent.role,
|
|
137
|
+
status: agent.status,
|
|
138
|
+
taskIds: [...(agentTasks.get(agent.agentId) ?? [])].sort(),
|
|
139
|
+
...(agent.childRunId ? { childRunId: agent.childRunId } : {}),
|
|
140
|
+
})),
|
|
141
|
+
children: receipt.childReceipts.map((child) => {
|
|
142
|
+
const agent = agentById.get(child.agentId);
|
|
143
|
+
const evidence = events
|
|
144
|
+
.filter((event) => event.childRunId === child.childRunId
|
|
145
|
+
&& event.agentId === child.agentId
|
|
146
|
+
&& event.attemptId === child.attemptId
|
|
147
|
+
&& (event.taskId === undefined || child.taskIds.includes(event.taskId)))
|
|
148
|
+
.map(eventReference);
|
|
149
|
+
return {
|
|
150
|
+
childRunId: child.childRunId,
|
|
151
|
+
agentId: child.agentId,
|
|
152
|
+
attemptId: child.attemptId,
|
|
153
|
+
taskIds: [...child.taskIds],
|
|
154
|
+
status: agent?.status ?? "needs_review",
|
|
155
|
+
receiptIntegrityState: "verified",
|
|
156
|
+
evidence,
|
|
157
|
+
};
|
|
158
|
+
}),
|
|
159
|
+
},
|
|
160
|
+
budget: {
|
|
161
|
+
capUsd: receipt.budgetLedger.capUsd,
|
|
162
|
+
...(receipt.budgetLedger.capTokens !== undefined ? { capTokens: receipt.budgetLedger.capTokens } : {}),
|
|
163
|
+
settledUsd: receipt.budgetLedger.settledUsd,
|
|
164
|
+
settledTokens: receipt.budgetLedger.settledTokens,
|
|
165
|
+
},
|
|
166
|
+
interventions: {
|
|
167
|
+
blockedActions: receipt.blockedActions.map((action) => ({
|
|
168
|
+
action: action.action,
|
|
169
|
+
reasonCode: action.reason,
|
|
170
|
+
taskId: action.taskId,
|
|
171
|
+
agentId: action.agentId,
|
|
172
|
+
attemptId: action.attemptId,
|
|
173
|
+
evidence: [{ eventId: action.eventId, sequence: action.sequence }],
|
|
174
|
+
})),
|
|
175
|
+
reassignments: receipt.reassignments.map((reassignment) => ({
|
|
176
|
+
taskId: reassignment.taskId,
|
|
177
|
+
fromAgentId: reassignment.fromAgentId,
|
|
178
|
+
toAgentId: reassignment.toAgentId,
|
|
179
|
+
fromAttemptId: reassignment.fromAttemptId,
|
|
180
|
+
toAttemptId: reassignment.toAttemptId,
|
|
181
|
+
reasonCode: reassignment.reason,
|
|
182
|
+
evidence: [{ eventId: reassignment.eventId, sequence: reassignment.sequence }],
|
|
183
|
+
})),
|
|
184
|
+
},
|
|
185
|
+
integration: {
|
|
186
|
+
admissions: integrationDecisions(events, "CHILD_PATCH_ADMITTED", "admitted"),
|
|
187
|
+
rejections: integrationDecisions(events, "CHILD_PATCH_REJECTED", "rejected"),
|
|
188
|
+
conflicts: integrationConflicts(events),
|
|
189
|
+
...(receipt.integratedTreeHash ? { integratedTreeHash: receipt.integratedTreeHash } : {}),
|
|
190
|
+
},
|
|
191
|
+
events,
|
|
192
|
+
globalVerification: globalEvent ? {
|
|
193
|
+
state: receipt.taskVerificationState === "passed" ? "passed" : "failed",
|
|
194
|
+
verificationId: receipt.globalVerificationId,
|
|
195
|
+
...(receipt.integratedTreeHash ? { integratedTreeHash: receipt.integratedTreeHash } : {}),
|
|
196
|
+
evidence: [eventReference(globalEvent)],
|
|
197
|
+
} : {
|
|
198
|
+
state: "unknown",
|
|
199
|
+
evidence: [],
|
|
200
|
+
},
|
|
201
|
+
parentOutcome: {
|
|
202
|
+
state: receipt.parentOutcome.state,
|
|
203
|
+
source: "sealed_parent_receipt",
|
|
204
|
+
evidence: [eventReference(parentEvent)],
|
|
205
|
+
},
|
|
206
|
+
taskVerificationState: receipt.taskVerificationState,
|
|
207
|
+
receiptIntegrityState: "verified",
|
|
208
|
+
transportSignature: {
|
|
209
|
+
algorithm: "hmac-sha256",
|
|
210
|
+
keyId: "pending",
|
|
211
|
+
keyLocatorHash: "0".repeat(64),
|
|
212
|
+
signatureHmacSha256: "0".repeat(64),
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
for (const reference of allEvidenceReferences(envelope)) {
|
|
216
|
+
if (!eventById.has(reference.eventId)) {
|
|
217
|
+
throw hostedError("SWARM_HOSTED_SOURCE_MISMATCH", "Hosted evidence reference does not resolve to an authenticated event.");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return envelope;
|
|
221
|
+
}
|
|
222
|
+
function assertAuthenticatedSource(input, operational, projection) {
|
|
223
|
+
const { plan, snapshot, events } = operational;
|
|
224
|
+
const { receipt, integrity, seal } = projection;
|
|
225
|
+
if (integrity.state !== "verified"
|
|
226
|
+
|| !integrity.receiptSha256
|
|
227
|
+
|| !integrity.evidenceIndexSha256
|
|
228
|
+
|| !integrity.eventChainHead) {
|
|
229
|
+
throw hostedError("SWARM_HOSTED_INTEGRITY_NOT_VERIFIED", "Committed swarm receipt integrity is not verified.");
|
|
230
|
+
}
|
|
231
|
+
if (plan.schemaVersion !== SWARM_SCHEMA_VERSION
|
|
232
|
+
|| receipt.schemaVersion !== SWARM_PARENT_RECEIPT_SCHEMA_VERSION
|
|
233
|
+
|| seal.schemaVersion !== "martin.swarm-receipt-seal.v1") {
|
|
234
|
+
throw hostedError("SWARM_HOSTED_UNSUPPORTED_SOURCE", "Swarm hosted export source schema is unsupported.");
|
|
235
|
+
}
|
|
236
|
+
if (input.swarmId !== plan.swarmId
|
|
237
|
+
|| computeSwarmLivePlanHash(plan) !== plan.planHash
|
|
238
|
+
|| receipt.swarmId !== plan.swarmId
|
|
239
|
+
|| seal.swarmId !== plan.swarmId
|
|
240
|
+
|| receipt.planHash !== plan.planHash
|
|
241
|
+
|| seal.planHash !== plan.planHash
|
|
242
|
+
|| receipt.baselineCommit !== plan.baselineCommit
|
|
243
|
+
|| receipt.evidenceIndexSha256 !== integrity.evidenceIndexSha256
|
|
244
|
+
|| receipt.receiptSha256 !== integrity.receiptSha256
|
|
245
|
+
|| integrity.taskVerificationState !== receipt.taskVerificationState
|
|
246
|
+
|| seal.evidenceIndexSha256 !== receipt.evidenceIndexSha256
|
|
247
|
+
|| seal.committedAt !== receipt.sealedAt
|
|
248
|
+
|| snapshot.outcome.state !== receipt.parentOutcome.state
|
|
249
|
+
|| stableJson(events) !== stableJson(receipt.events)) {
|
|
250
|
+
throw hostedError("SWARM_HOSTED_SOURCE_MISMATCH", "Committed swarm source identities disagree.");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function projectEvent(event, childReceiptById) {
|
|
254
|
+
const payload = event.payload;
|
|
255
|
+
const hasExplicitAttemptId = Object.prototype.hasOwnProperty.call(payload, "attemptId");
|
|
256
|
+
if (hasExplicitAttemptId
|
|
257
|
+
&& (typeof payload.attemptId !== "string" || payload.attemptId.length === 0)) {
|
|
258
|
+
throw hostedError("SWARM_HOSTED_SOURCE_MISMATCH", "Present child attempt identity must be a non-empty string; only true omission may use receipt linkage.");
|
|
259
|
+
}
|
|
260
|
+
const explicitAttemptId = text(payload.attemptId)
|
|
261
|
+
?? (event.type === "TASK_REASSIGNED" ? text(payload.fromAttemptId) : undefined);
|
|
262
|
+
let attemptId = explicitAttemptId;
|
|
263
|
+
if (event.childRunId && event.type.startsWith("CHILD_")) {
|
|
264
|
+
const child = childReceiptById.get(event.childRunId);
|
|
265
|
+
if (!child
|
|
266
|
+
|| event.agentId !== child.agentId
|
|
267
|
+
|| (event.taskId !== undefined && !child.taskIds.includes(event.taskId))
|
|
268
|
+
|| (explicitAttemptId !== undefined && explicitAttemptId !== child.attemptId)) {
|
|
269
|
+
throw hostedError("SWARM_HOSTED_SOURCE_MISMATCH", "Child lifecycle event identities contradict authenticated child receipt linkage.");
|
|
270
|
+
}
|
|
271
|
+
attemptId = child.attemptId;
|
|
272
|
+
}
|
|
273
|
+
const relatedAttemptId = text(payload.toAttemptId);
|
|
274
|
+
const toAgentId = text(payload.toAgentId);
|
|
275
|
+
const candidateId = text(payload.candidateId);
|
|
276
|
+
const verificationId = text(payload.verificationId);
|
|
277
|
+
return {
|
|
278
|
+
eventId: event.idempotencyKey,
|
|
279
|
+
sourceIdempotencyKey: event.idempotencyKey,
|
|
280
|
+
sequence: event.sequence,
|
|
281
|
+
type: event.type,
|
|
282
|
+
timestamp: event.timestamp,
|
|
283
|
+
...(event.taskId ? { taskId: event.taskId } : {}),
|
|
284
|
+
...(event.agentId ? { agentId: event.type === "TASK_REASSIGNED" ? text(payload.fromAgentId) ?? event.agentId : event.agentId } : {}),
|
|
285
|
+
...(event.childRunId ? { childRunId: event.childRunId } : {}),
|
|
286
|
+
...(attemptId ? { attemptId } : {}),
|
|
287
|
+
...(relatedAttemptId ? { relatedAttemptId } : {}),
|
|
288
|
+
...(toAgentId ? { toAgentId } : {}),
|
|
289
|
+
...(candidateId ? { candidateId } : {}),
|
|
290
|
+
...(verificationId ? { verificationId } : {}),
|
|
291
|
+
...(event.failureClass ? { failureClass: event.failureClass } : {}),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function integrationDecisions(events, type, state) {
|
|
295
|
+
return events.filter((event) => event.type === type).map((event) => {
|
|
296
|
+
if (!event.candidateId || !event.taskId || !event.agentId || !event.childRunId) {
|
|
297
|
+
throw hostedError("SWARM_HOSTED_SOURCE_MISMATCH", "Integration event lacks authenticated hosted identities.");
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
candidateId: event.candidateId,
|
|
301
|
+
taskId: event.taskId,
|
|
302
|
+
agentId: event.agentId,
|
|
303
|
+
childRunId: event.childRunId,
|
|
304
|
+
state,
|
|
305
|
+
evidence: [eventReference(event)],
|
|
306
|
+
};
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
function integrationConflicts(events) {
|
|
310
|
+
return events.filter((event) => event.type === "INTEGRATION_CONFLICT").map((event) => ({
|
|
311
|
+
conflictId: event.candidateId ?? event.eventId,
|
|
312
|
+
taskIds: event.taskId ? [event.taskId] : [],
|
|
313
|
+
state: "blocking",
|
|
314
|
+
evidence: [eventReference(event)],
|
|
315
|
+
}));
|
|
316
|
+
}
|
|
317
|
+
function findSingleEvent(events, type, identity) {
|
|
318
|
+
const matches = events.filter((event) => event.type === type
|
|
319
|
+
&& (identity === undefined || event.verificationId === identity));
|
|
320
|
+
if (matches.length !== 1) {
|
|
321
|
+
throw hostedError("SWARM_HOSTED_SOURCE_MISMATCH", `Expected one authoritative ${type} event.`);
|
|
322
|
+
}
|
|
323
|
+
return matches[0];
|
|
324
|
+
}
|
|
325
|
+
function parentTerminalType(state) {
|
|
326
|
+
return state === "verified" ? "SWARM_VERIFIED" : state === "stopped" ? "SWARM_STOPPED" : "SWARM_NEEDS_REVIEW";
|
|
327
|
+
}
|
|
328
|
+
function eventReference(event) {
|
|
329
|
+
return { eventId: event.eventId, sequence: event.sequence };
|
|
330
|
+
}
|
|
331
|
+
function allEvidenceReferences(envelope) {
|
|
332
|
+
return [
|
|
333
|
+
...envelope.topology.children.flatMap((child) => child.evidence),
|
|
334
|
+
...envelope.interventions.blockedActions.flatMap((action) => action.evidence),
|
|
335
|
+
...envelope.interventions.reassignments.flatMap((reassignment) => reassignment.evidence),
|
|
336
|
+
...envelope.integration.admissions.flatMap((decision) => decision.evidence),
|
|
337
|
+
...envelope.integration.rejections.flatMap((decision) => decision.evidence),
|
|
338
|
+
...envelope.integration.conflicts.flatMap((conflict) => conflict.evidence),
|
|
339
|
+
...envelope.globalVerification.evidence,
|
|
340
|
+
...envelope.parentOutcome.evidence,
|
|
341
|
+
];
|
|
342
|
+
}
|
|
343
|
+
function text(value) {
|
|
344
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
345
|
+
}
|
|
346
|
+
function stableJson(value) {
|
|
347
|
+
if (Array.isArray(value))
|
|
348
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
349
|
+
if (value !== null && typeof value === "object") {
|
|
350
|
+
return `{${Object.entries(value)
|
|
351
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
352
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`)
|
|
353
|
+
.join(",")}}`;
|
|
354
|
+
}
|
|
355
|
+
return JSON.stringify(value);
|
|
356
|
+
}
|
|
357
|
+
function sha256(value) {
|
|
358
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
359
|
+
}
|
|
360
|
+
function hostedError(code, message) {
|
|
361
|
+
return Object.assign(new Error(`${code}: ${message}`), { code });
|
|
362
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type SwarmAtlasView, type SwarmDashboardView, type SwarmSansaView } from "../../contracts/swarm-hosted.js";
|
|
2
|
+
export type { SwarmAtlasView, SwarmDashboardView, SwarmSansaView, SwarmTraceFact } from "../../contracts/swarm-hosted.js";
|
|
3
|
+
export declare function projectSwarmAtlasView(value: unknown): SwarmAtlasView;
|
|
4
|
+
export declare function projectSwarmTraceFacts(value: unknown): SwarmSansaView;
|
|
5
|
+
export declare function projectSwarmDashboardView(value: unknown): SwarmDashboardView;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { validateSwarmHostedEnvelope, } from "../../contracts/swarm-hosted.js";
|
|
2
|
+
const FAILURE_EVENT_TYPES = new Set([
|
|
3
|
+
"CHILD_STOPPED",
|
|
4
|
+
"CHILD_NEEDS_REVIEW",
|
|
5
|
+
"CHILD_PATCH_REJECTED",
|
|
6
|
+
]);
|
|
7
|
+
const TERMINAL_EVENT_TYPES = {
|
|
8
|
+
stopped: "CHILD_STOPPED",
|
|
9
|
+
needs_review: "CHILD_NEEDS_REVIEW",
|
|
10
|
+
rejected: "CHILD_PATCH_REJECTED",
|
|
11
|
+
};
|
|
12
|
+
export function projectSwarmAtlasView(value) {
|
|
13
|
+
const envelope = validatedEnvelope(value);
|
|
14
|
+
return {
|
|
15
|
+
schemaVersion: "martin.swarm-atlas-view.v1",
|
|
16
|
+
envelopeId: envelope.envelopeId,
|
|
17
|
+
sourceIdentities: { ...envelope.sourceIdentities },
|
|
18
|
+
swarm: { ...envelope.swarm },
|
|
19
|
+
tasks: envelope.topology.tasks.map(cloneTask),
|
|
20
|
+
dependencyEdges: envelope.topology.tasks
|
|
21
|
+
.flatMap((task) => task.dependsOn.map((dependencyTaskId) => ({
|
|
22
|
+
fromTaskId: task.taskId,
|
|
23
|
+
toTaskId: dependencyTaskId,
|
|
24
|
+
})))
|
|
25
|
+
.sort(compareDependencyEdges),
|
|
26
|
+
agents: envelope.topology.agents.map((agent) => ({ ...agent, taskIds: [...agent.taskIds] })),
|
|
27
|
+
children: envelope.topology.children.map((child) => ({
|
|
28
|
+
...child,
|
|
29
|
+
taskIds: [...child.taskIds],
|
|
30
|
+
evidence: cloneEvidence(child.evidence),
|
|
31
|
+
})),
|
|
32
|
+
budget: { ...envelope.budget },
|
|
33
|
+
interventions: {
|
|
34
|
+
blockedActions: envelope.interventions.blockedActions.map((action) => ({ ...action, evidence: cloneEvidence(action.evidence) })),
|
|
35
|
+
reassignments: envelope.interventions.reassignments.map((reassignment) => ({ ...reassignment, evidence: cloneEvidence(reassignment.evidence) })),
|
|
36
|
+
},
|
|
37
|
+
integration: {
|
|
38
|
+
...envelope.integration,
|
|
39
|
+
admissions: envelope.integration.admissions.map((decision) => ({ ...decision, evidence: cloneEvidence(decision.evidence) })),
|
|
40
|
+
rejections: envelope.integration.rejections.map((decision) => ({ ...decision, evidence: cloneEvidence(decision.evidence) })),
|
|
41
|
+
conflicts: envelope.integration.conflicts.map((conflict) => ({ ...conflict, taskIds: [...conflict.taskIds], evidence: cloneEvidence(conflict.evidence) })),
|
|
42
|
+
},
|
|
43
|
+
globalVerification: { ...envelope.globalVerification, evidence: cloneEvidence(envelope.globalVerification.evidence) },
|
|
44
|
+
parentOutcome: { ...envelope.parentOutcome, evidence: cloneEvidence(envelope.parentOutcome.evidence) },
|
|
45
|
+
taskVerificationState: envelope.taskVerificationState,
|
|
46
|
+
receiptIntegrityState: envelope.receiptIntegrityState,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function projectSwarmTraceFacts(value) {
|
|
50
|
+
const envelope = validatedEnvelope(value);
|
|
51
|
+
const facts = [
|
|
52
|
+
...observedFacts(envelope),
|
|
53
|
+
...recoveryFacts(envelope),
|
|
54
|
+
...blockingDependencyFacts(envelope),
|
|
55
|
+
].sort(compareFacts);
|
|
56
|
+
return {
|
|
57
|
+
schemaVersion: "martin.swarm-sansa-view.v1",
|
|
58
|
+
envelopeId: envelope.envelopeId,
|
|
59
|
+
sourceIdentities: { ...envelope.sourceIdentities },
|
|
60
|
+
facts,
|
|
61
|
+
parentOutcome: { ...envelope.parentOutcome, evidence: cloneEvidence(envelope.parentOutcome.evidence) },
|
|
62
|
+
globalVerification: { ...envelope.globalVerification, evidence: cloneEvidence(envelope.globalVerification.evidence) },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function projectSwarmDashboardView(value) {
|
|
66
|
+
const envelope = validatedEnvelope(value);
|
|
67
|
+
return {
|
|
68
|
+
schemaVersion: "martin.swarm-dashboard-view.v1",
|
|
69
|
+
envelopeId: envelope.envelopeId,
|
|
70
|
+
sourceIdentities: { ...envelope.sourceIdentities },
|
|
71
|
+
swarmId: envelope.swarm.swarmId,
|
|
72
|
+
children: envelope.topology.children.map((child) => ({
|
|
73
|
+
...child,
|
|
74
|
+
taskIds: [...child.taskIds],
|
|
75
|
+
evidence: cloneEvidence(child.evidence),
|
|
76
|
+
})),
|
|
77
|
+
parentOutcome: { ...envelope.parentOutcome, evidence: cloneEvidence(envelope.parentOutcome.evidence) },
|
|
78
|
+
globalVerification: { ...envelope.globalVerification, evidence: cloneEvidence(envelope.globalVerification.evidence) },
|
|
79
|
+
taskVerificationState: envelope.taskVerificationState,
|
|
80
|
+
receiptIntegrityState: envelope.receiptIntegrityState,
|
|
81
|
+
budget: { ...envelope.budget },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function validatedEnvelope(value) {
|
|
85
|
+
const validation = validateSwarmHostedEnvelope(value);
|
|
86
|
+
if (!validation.ok) {
|
|
87
|
+
const error = new Error(`INVALID_SWARM_HOSTED_ENVELOPE: ${validation.errors.map((item) => `${item.code}:${item.path}`).join(",")}`);
|
|
88
|
+
Object.assign(error, { code: "INVALID_SWARM_HOSTED_ENVELOPE" });
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
function observedFacts(envelope) {
|
|
94
|
+
return envelope.events.flatMap((event) => {
|
|
95
|
+
if (FAILURE_EVENT_TYPES.has(event.type))
|
|
96
|
+
return [factFromEvent("failure", event)];
|
|
97
|
+
if (event.type === "TASK_REASSIGNED")
|
|
98
|
+
return [factFromEvent("reassignment", event)];
|
|
99
|
+
if (event.type === "GLOBAL_VERIFIER_PASSED" || event.type === "GLOBAL_VERIFIER_FAILED") {
|
|
100
|
+
return [factFromEvent("verification_boundary", event)];
|
|
101
|
+
}
|
|
102
|
+
return [];
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function recoveryFacts(envelope) {
|
|
106
|
+
const facts = [];
|
|
107
|
+
for (const reassignment of envelope.interventions.reassignments) {
|
|
108
|
+
const task = envelope.topology.tasks.find((item) => item.taskId === reassignment.taskId);
|
|
109
|
+
if (task?.status !== "accepted" || task.effectiveAgentId !== reassignment.toAgentId)
|
|
110
|
+
continue;
|
|
111
|
+
const reassigned = referencedEvent(envelope.events, reassignment.evidence, "TASK_REASSIGNED");
|
|
112
|
+
if (!reassigned
|
|
113
|
+
|| reassigned.taskId !== reassignment.taskId
|
|
114
|
+
|| reassigned.agentId !== reassignment.fromAgentId
|
|
115
|
+
|| reassigned.toAgentId !== reassignment.toAgentId
|
|
116
|
+
|| reassigned.attemptId !== reassignment.fromAttemptId
|
|
117
|
+
|| reassigned.relatedAttemptId !== reassignment.toAttemptId)
|
|
118
|
+
continue;
|
|
119
|
+
const failure = latestBefore(envelope.events, reassigned.sequence, (event) => FAILURE_EVENT_TYPES.has(event.type)
|
|
120
|
+
&& event.taskId === reassignment.taskId
|
|
121
|
+
&& event.agentId === reassignment.fromAgentId
|
|
122
|
+
&& event.attemptId === reassignment.fromAttemptId);
|
|
123
|
+
const start = firstAfter(envelope.events, reassigned.sequence, (event) => event.type === "CHILD_STARTED"
|
|
124
|
+
&& event.taskId === reassignment.taskId
|
|
125
|
+
&& event.agentId === reassignment.toAgentId
|
|
126
|
+
&& event.attemptId === reassignment.toAttemptId);
|
|
127
|
+
const completed = start && firstAfter(envelope.events, start.sequence, (event) => event.type === "CHILD_VERIFIED"
|
|
128
|
+
&& event.taskId === reassignment.taskId
|
|
129
|
+
&& event.agentId === reassignment.toAgentId
|
|
130
|
+
&& event.attemptId === reassignment.toAttemptId
|
|
131
|
+
&& event.childRunId === start.childRunId);
|
|
132
|
+
const admitted = completed && firstAfter(envelope.events, completed.sequence, (event) => event.type === "CHILD_PATCH_ADMITTED"
|
|
133
|
+
&& event.taskId === reassignment.taskId
|
|
134
|
+
&& event.agentId === reassignment.toAgentId
|
|
135
|
+
&& event.attemptId === reassignment.toAttemptId
|
|
136
|
+
&& event.childRunId === completed.childRunId);
|
|
137
|
+
const admission = admitted && envelope.integration.admissions.find((decision) => decision.taskId === reassignment.taskId
|
|
138
|
+
&& decision.agentId === reassignment.toAgentId
|
|
139
|
+
&& decision.childRunId === admitted.childRunId
|
|
140
|
+
&& decision.candidateId === admitted.candidateId
|
|
141
|
+
&& evidenceIncludes(decision.evidence, admitted));
|
|
142
|
+
if (!failure || !start || !completed || !admitted || !admission)
|
|
143
|
+
continue;
|
|
144
|
+
facts.push({
|
|
145
|
+
factId: `derived:recovery:${reassignment.taskId}:${reassignment.fromAttemptId}:${reassignment.toAttemptId}`,
|
|
146
|
+
kind: "recovery",
|
|
147
|
+
derivationKind: "deterministic_graph_derivation",
|
|
148
|
+
taskIds: [reassignment.taskId],
|
|
149
|
+
agentIds: [reassignment.fromAgentId, reassignment.toAgentId],
|
|
150
|
+
childRunIds: compactUnique([failure.childRunId, completed.childRunId]),
|
|
151
|
+
attemptIds: [reassignment.fromAttemptId, reassignment.toAttemptId],
|
|
152
|
+
verificationIds: [],
|
|
153
|
+
evidence: [failure, reassigned, start, completed, admitted].map(eventReference),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return facts;
|
|
157
|
+
}
|
|
158
|
+
function blockingDependencyFacts(envelope) {
|
|
159
|
+
const tasks = new Map(envelope.topology.tasks.map((task) => [task.taskId, task]));
|
|
160
|
+
const facts = [];
|
|
161
|
+
for (const task of [...envelope.topology.tasks].sort((left, right) => left.taskId.localeCompare(right.taskId))) {
|
|
162
|
+
if (!new Set(["queued", "ready", "running"]).has(task.status))
|
|
163
|
+
continue;
|
|
164
|
+
for (const path of terminalDependencyPaths(task.taskId, tasks)) {
|
|
165
|
+
const blocking = tasks.get(path.at(-1));
|
|
166
|
+
const terminalType = blocking && TERMINAL_EVENT_TYPES[blocking.status];
|
|
167
|
+
if (!blocking || !terminalType)
|
|
168
|
+
continue;
|
|
169
|
+
const terminal = [...envelope.events].reverse().find((event) => event.type === terminalType && event.taskId === blocking.taskId);
|
|
170
|
+
if (!terminal)
|
|
171
|
+
continue;
|
|
172
|
+
facts.push({
|
|
173
|
+
factId: `derived:blocking:${path.join("->")}`,
|
|
174
|
+
kind: "blocking_dependency",
|
|
175
|
+
derivationKind: "deterministic_graph_derivation",
|
|
176
|
+
taskIds: path,
|
|
177
|
+
agentIds: compactUnique([terminal.agentId]),
|
|
178
|
+
childRunIds: compactUnique([terminal.childRunId]),
|
|
179
|
+
attemptIds: compactUnique([terminal.attemptId]),
|
|
180
|
+
verificationIds: [],
|
|
181
|
+
evidence: [eventReference(terminal)],
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return facts;
|
|
186
|
+
}
|
|
187
|
+
function terminalDependencyPaths(startTaskId, tasks) {
|
|
188
|
+
const paths = [];
|
|
189
|
+
const visit = (currentId, path, seen) => {
|
|
190
|
+
const current = tasks.get(currentId);
|
|
191
|
+
if (!current)
|
|
192
|
+
return;
|
|
193
|
+
for (const dependencyId of [...current.dependsOn].sort()) {
|
|
194
|
+
if (seen.has(dependencyId))
|
|
195
|
+
continue;
|
|
196
|
+
const dependency = tasks.get(dependencyId);
|
|
197
|
+
if (!dependency)
|
|
198
|
+
continue;
|
|
199
|
+
const next = [...path, dependencyId];
|
|
200
|
+
if (TERMINAL_EVENT_TYPES[dependency.status])
|
|
201
|
+
paths.push(next);
|
|
202
|
+
else
|
|
203
|
+
visit(dependencyId, next, new Set([...seen, dependencyId]));
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
visit(startTaskId, [startTaskId], new Set([startTaskId]));
|
|
207
|
+
return paths;
|
|
208
|
+
}
|
|
209
|
+
function factFromEvent(kind, event) {
|
|
210
|
+
return {
|
|
211
|
+
factId: `observed:${event.sequence}:${event.eventId}`,
|
|
212
|
+
kind,
|
|
213
|
+
derivationKind: "observed",
|
|
214
|
+
taskIds: compactUnique([event.taskId]),
|
|
215
|
+
agentIds: compactUnique([event.agentId, event.toAgentId]),
|
|
216
|
+
childRunIds: compactUnique([event.childRunId]),
|
|
217
|
+
attemptIds: compactUnique([event.attemptId, event.relatedAttemptId]),
|
|
218
|
+
verificationIds: compactUnique([event.verificationId]),
|
|
219
|
+
evidence: [eventReference(event)],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function referencedEvent(events, evidence, type) {
|
|
223
|
+
return evidence
|
|
224
|
+
.map((reference) => events.find((event) => event.eventId === reference.eventId && event.sequence === reference.sequence))
|
|
225
|
+
.find((event) => event?.type === type);
|
|
226
|
+
}
|
|
227
|
+
function latestBefore(events, sequence, predicate) {
|
|
228
|
+
return [...events].reverse().find((event) => event.sequence < sequence && predicate(event));
|
|
229
|
+
}
|
|
230
|
+
function firstAfter(events, sequence, predicate) {
|
|
231
|
+
return events.find((event) => event.sequence > sequence && predicate(event));
|
|
232
|
+
}
|
|
233
|
+
function evidenceIncludes(evidence, event) {
|
|
234
|
+
return evidence.some((reference) => reference.eventId === event.eventId && reference.sequence === event.sequence);
|
|
235
|
+
}
|
|
236
|
+
function eventReference(event) {
|
|
237
|
+
return { eventId: event.eventId, sequence: event.sequence };
|
|
238
|
+
}
|
|
239
|
+
function cloneEvidence(evidence) {
|
|
240
|
+
return evidence.map((reference) => ({ ...reference }));
|
|
241
|
+
}
|
|
242
|
+
function cloneTask(task) {
|
|
243
|
+
return { ...task, dependsOn: [...task.dependsOn] };
|
|
244
|
+
}
|
|
245
|
+
function compactUnique(values) {
|
|
246
|
+
return [...new Set(values.filter((value) => typeof value === "string"))];
|
|
247
|
+
}
|
|
248
|
+
function compareDependencyEdges(left, right) {
|
|
249
|
+
return left.fromTaskId.localeCompare(right.fromTaskId) || left.toTaskId.localeCompare(right.toTaskId);
|
|
250
|
+
}
|
|
251
|
+
function compareFacts(left, right) {
|
|
252
|
+
const leftSequence = left.evidence.at(-1)?.sequence ?? Number.MAX_SAFE_INTEGER;
|
|
253
|
+
const rightSequence = right.evidence.at(-1)?.sequence ?? Number.MAX_SAFE_INTEGER;
|
|
254
|
+
const derivationOrder = (fact) => fact.derivationKind === "observed" ? 0 : 1;
|
|
255
|
+
return leftSequence - rightSequence
|
|
256
|
+
|| derivationOrder(left) - derivationOrder(right)
|
|
257
|
+
|| left.factId.localeCompare(right.factId);
|
|
258
|
+
}
|