@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,550 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { FAILURE_CLASSES } from "./index.js";
|
|
3
|
+
export const SWARM_SCHEMA_VERSION = "martin.swarm.v1";
|
|
4
|
+
export const SWARM_EVENT_TYPES = [
|
|
5
|
+
"SWARM_CREATED",
|
|
6
|
+
"SWARM_PLAN_CREATED",
|
|
7
|
+
"SWARM_PLAN_APPROVED",
|
|
8
|
+
"TASK_READY",
|
|
9
|
+
"TASK_ASSIGNED",
|
|
10
|
+
"CHILD_STARTED",
|
|
11
|
+
"CHILD_PROGRESS",
|
|
12
|
+
"CHILD_VERIFIER_STARTED",
|
|
13
|
+
"CHILD_VERIFIED",
|
|
14
|
+
"CHILD_STOPPED",
|
|
15
|
+
"CHILD_NEEDS_REVIEW",
|
|
16
|
+
"CHILD_PATCH_PROPOSED",
|
|
17
|
+
"CHILD_PATCH_REJECTED",
|
|
18
|
+
"CHILD_PATCH_ADMITTED",
|
|
19
|
+
"ACTION_BLOCKED",
|
|
20
|
+
"TASK_REASSIGNED",
|
|
21
|
+
"DEPENDENCY_SATISFIED",
|
|
22
|
+
"INTEGRATION_STARTED",
|
|
23
|
+
"INTEGRATION_CONFLICT",
|
|
24
|
+
"INTEGRATION_COMPLETED",
|
|
25
|
+
"GLOBAL_VERIFIER_STARTED",
|
|
26
|
+
"GLOBAL_VERIFIER_PASSED",
|
|
27
|
+
"GLOBAL_VERIFIER_FAILED",
|
|
28
|
+
"SWARM_CANCEL_REQUESTED",
|
|
29
|
+
"SWARM_STOPPED",
|
|
30
|
+
"SWARM_NEEDS_REVIEW",
|
|
31
|
+
"SWARM_VERIFIED"
|
|
32
|
+
];
|
|
33
|
+
export const SWARM_ORCHESTRATION_STRATEGIES = [
|
|
34
|
+
"hierarchical_dag",
|
|
35
|
+
"pipeline_dag",
|
|
36
|
+
"parallel_dag",
|
|
37
|
+
"hybrid"
|
|
38
|
+
];
|
|
39
|
+
export const SWARM_LIVE_ENGINES = ["claude", "codex", "gemini", "openai"];
|
|
40
|
+
export const SWARM_PARENT_RECEIPT_SCHEMA_VERSION = "martin.swarm-receipt.v1";
|
|
41
|
+
export function validateSwarmChildReceiptLink(value) {
|
|
42
|
+
const errors = [];
|
|
43
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
44
|
+
addError(errors, "INVALID_SWARM_CHILD_RECEIPT_LINK", "swarmChild", "must be an object");
|
|
45
|
+
return { ok: false, errors };
|
|
46
|
+
}
|
|
47
|
+
const link = value;
|
|
48
|
+
requireText(link.parentSwarmId, "parentSwarmId", errors);
|
|
49
|
+
requireText(link.agentId, "agentId", errors);
|
|
50
|
+
requireText(link.attemptId, "attemptId", errors);
|
|
51
|
+
if (!Array.isArray(link.taskIds) || link.taskIds.length === 0) {
|
|
52
|
+
addError(errors, "EMPTY_SWARM_CHILD_TASKS", "taskIds", "at least one task ID is required");
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
link.taskIds.forEach((taskId, index) => requireText(taskId, `taskIds[${index}]`, errors));
|
|
56
|
+
addDuplicateErrors(link.taskIds, "DUPLICATE_SWARM_CHILD_TASK_ID", "taskIds", errors);
|
|
57
|
+
}
|
|
58
|
+
return errors.length === 0 ? { ok: true, errors: [] } : { ok: false, errors };
|
|
59
|
+
}
|
|
60
|
+
export function createSwarmLivePlan(draft) {
|
|
61
|
+
const planWithoutHash = {
|
|
62
|
+
...cloneLivePlanDraft(draft),
|
|
63
|
+
schemaVersion: SWARM_SCHEMA_VERSION
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
...planWithoutHash,
|
|
67
|
+
planHash: computeSwarmLivePlanHash(planWithoutHash)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function computeSwarmLivePlanHash(plan) {
|
|
71
|
+
const { planHash: _ignored, ...hashInput } = plan;
|
|
72
|
+
return createHash("sha256").update(canonicalJson(hashInput)).digest("hex");
|
|
73
|
+
}
|
|
74
|
+
export function validateSwarmLiveRevision(value) {
|
|
75
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
76
|
+
}
|
|
77
|
+
export function validateSwarmLivePlan(plan) {
|
|
78
|
+
const errors = [];
|
|
79
|
+
if (plan.schemaVersion !== SWARM_SCHEMA_VERSION) {
|
|
80
|
+
addError(errors, "UNKNOWN_SCHEMA_VERSION", "schemaVersion", "unknown swarm schema version");
|
|
81
|
+
}
|
|
82
|
+
for (const key of ["planId", "swarmId", "workspaceId", "projectId"]) {
|
|
83
|
+
requireText(plan[key], key, errors);
|
|
84
|
+
}
|
|
85
|
+
if (!/^[0-9a-f]{40,64}$/iu.test(plan.baselineCommit)) {
|
|
86
|
+
addError(errors, "INVALID_BASELINE_COMMIT", "baselineCommit", "must be an immutable 40-64 character Git object id");
|
|
87
|
+
}
|
|
88
|
+
if (!SWARM_LIVE_ENGINES.includes(plan.engine.engine)) {
|
|
89
|
+
addError(errors, "INVALID_LIVE_ENGINE", "engine.engine", "must select exactly one concrete live engine");
|
|
90
|
+
}
|
|
91
|
+
requireText(plan.engine.model, "engine.model", errors);
|
|
92
|
+
if (!isPositiveInteger(plan.childMaxIterations)) {
|
|
93
|
+
addError(errors, "INVALID_CHILD_ITERATION_LIMIT", "childMaxIterations", "must be a positive integer");
|
|
94
|
+
}
|
|
95
|
+
if (!isCanonicalIsoDate(plan.createdAt)) {
|
|
96
|
+
addError(errors, "INVALID_CREATED_AT", "createdAt", "must be a canonical ISO timestamp");
|
|
97
|
+
}
|
|
98
|
+
errors.push(...validateSwarmParentContract(plan.parentContract).errors);
|
|
99
|
+
if (plan.tasks.length === 0) {
|
|
100
|
+
addError(errors, "EMPTY_TASK_GRAPH", "tasks", "at least one task is required");
|
|
101
|
+
}
|
|
102
|
+
if (plan.agents.length === 0) {
|
|
103
|
+
addError(errors, "EMPTY_AGENT_POOL", "agents", "at least one agent is required");
|
|
104
|
+
}
|
|
105
|
+
addDuplicateErrors(plan.tasks.map((task) => task.taskId), "DUPLICATE_TASK_ID", "tasks", errors);
|
|
106
|
+
addDuplicateErrors(plan.agents.map((agent) => agent.agentId), "DUPLICATE_AGENT_ID", "agents", errors);
|
|
107
|
+
const taskIds = new Set(plan.tasks.map((task) => task.taskId));
|
|
108
|
+
const agentIds = new Set(plan.agents.map((agent) => agent.agentId));
|
|
109
|
+
plan.tasks.forEach((task, index) => {
|
|
110
|
+
requireText(task.taskId, `tasks[${index}].taskId`, errors);
|
|
111
|
+
if (!hasText(task.assignedAgentId)) {
|
|
112
|
+
addError(errors, "MISSING_TASK_AGENT", `tasks[${index}].assignedAgentId`, "every task must have one assigned agent");
|
|
113
|
+
}
|
|
114
|
+
if (task.dependsOn.some((dependency) => !taskIds.has(dependency))) {
|
|
115
|
+
addError(errors, "UNKNOWN_TASK_DEPENDENCY", `tasks[${index}].dependsOn`, "every dependency must name a plan task");
|
|
116
|
+
}
|
|
117
|
+
if (task.assignedAgentId !== undefined && !agentIds.has(task.assignedAgentId)) {
|
|
118
|
+
addError(errors, "UNKNOWN_TASK_AGENT", `tasks[${index}].assignedAgentId`, "assigned agent must be present in the plan");
|
|
119
|
+
}
|
|
120
|
+
const assignedAgent = plan.agents.find((agent) => agent.agentId === task.assignedAgentId);
|
|
121
|
+
if (assignedAgent && !assignedAgent.contract.taskIds.includes(task.taskId)) {
|
|
122
|
+
addError(errors, "TASK_ASSIGNMENT_NOT_COVERED", `tasks[${index}].assignedAgentId`, "assigned agent contract must include the exact task");
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
plan.agents.forEach((agent, index) => {
|
|
126
|
+
const child = validateSwarmChildContract(plan.parentContract, agent.contract);
|
|
127
|
+
errors.push(...child.errors.map((error) => ({ ...error, path: `agents[${index}].contract.${error.path}` })));
|
|
128
|
+
if (agent.contract.taskIds.some((taskId) => !taskIds.has(taskId))) {
|
|
129
|
+
addError(errors, "UNKNOWN_CHILD_TASK", `agents[${index}].contract.taskIds`, "every child task must be present in the plan");
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
if (!/^[0-9a-f]{64}$/u.test(plan.planHash) || computeSwarmLivePlanHash(plan) !== plan.planHash) {
|
|
133
|
+
addError(errors, "LIVE_PLAN_HASH_MISMATCH", "planHash", "plan hash must bind the complete canonical live plan");
|
|
134
|
+
}
|
|
135
|
+
return validationResult(errors);
|
|
136
|
+
}
|
|
137
|
+
export function validateSwarmLiveEvent(event, plan) {
|
|
138
|
+
const errors = [];
|
|
139
|
+
if (event.schemaVersion !== SWARM_SCHEMA_VERSION) {
|
|
140
|
+
addError(errors, "UNKNOWN_SCHEMA_VERSION", "schemaVersion", "unknown swarm schema version");
|
|
141
|
+
}
|
|
142
|
+
if (!Number.isSafeInteger(event.sequence) || event.sequence <= 0) {
|
|
143
|
+
addError(errors, "INVALID_EVENT_SEQUENCE", "sequence", "must be a positive safe integer");
|
|
144
|
+
}
|
|
145
|
+
requireText(event.idempotencyKey, "idempotencyKey", errors);
|
|
146
|
+
if (!SWARM_EVENT_TYPES.includes(event.type)) {
|
|
147
|
+
addError(errors, "INVALID_EVENT_TYPE", "type", "must use a canonical swarm event type");
|
|
148
|
+
}
|
|
149
|
+
if (event.swarmId !== plan.swarmId) {
|
|
150
|
+
addError(errors, "EVENT_SWARM_MISMATCH", "swarmId", "must match the live plan");
|
|
151
|
+
}
|
|
152
|
+
if (event.parentPolicyVersion !== plan.parentContract.policyVersion) {
|
|
153
|
+
addError(errors, "EVENT_POLICY_MISMATCH", "parentPolicyVersion", "must match the live plan");
|
|
154
|
+
}
|
|
155
|
+
if (event.planHash !== plan.planHash) {
|
|
156
|
+
addError(errors, "EVENT_PLAN_HASH_MISMATCH", "planHash", "must match the approved live plan hash");
|
|
157
|
+
}
|
|
158
|
+
if (!isCanonicalIsoDate(event.timestamp)) {
|
|
159
|
+
addError(errors, "INVALID_EVENT_TIMESTAMP", "timestamp", "must be a canonical ISO timestamp");
|
|
160
|
+
}
|
|
161
|
+
if (event.failureClass !== undefined && !FAILURE_CLASSES.includes(event.failureClass)) {
|
|
162
|
+
addError(errors, "INVALID_FAILURE_CLASS", "failureClass", "must use a canonical MartinLoop failure class");
|
|
163
|
+
}
|
|
164
|
+
if (event.payload === null || typeof event.payload !== "object" || Array.isArray(event.payload)) {
|
|
165
|
+
addError(errors, "INVALID_EVENT_PAYLOAD", "payload", "must be a JSON object");
|
|
166
|
+
}
|
|
167
|
+
return validationResult(errors);
|
|
168
|
+
}
|
|
169
|
+
export function validateSwarmConcretePath(value) {
|
|
170
|
+
if (value.length === 0) {
|
|
171
|
+
return { ok: false, error: "concrete path must not be empty" };
|
|
172
|
+
}
|
|
173
|
+
if (value.includes("\0")) {
|
|
174
|
+
return { ok: false, error: "concrete path must not contain NUL" };
|
|
175
|
+
}
|
|
176
|
+
if (value.includes("\\")) {
|
|
177
|
+
return { ok: false, error: "concrete path must use repository-relative slash separators" };
|
|
178
|
+
}
|
|
179
|
+
if (/^(?:[a-z]:|\/)/iu.test(value)) {
|
|
180
|
+
return { ok: false, error: "concrete path must be repository-relative" };
|
|
181
|
+
}
|
|
182
|
+
if (value !== value.normalize("NFC")) {
|
|
183
|
+
return { ok: false, error: "concrete path must use Unicode NFC normalization" };
|
|
184
|
+
}
|
|
185
|
+
const segments = value.split("/");
|
|
186
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
187
|
+
return { ok: false, error: "concrete path contains an empty or traversal segment" };
|
|
188
|
+
}
|
|
189
|
+
if (segments.some((segment) => segment.toLowerCase() === ".git")) {
|
|
190
|
+
return { ok: false, error: "concrete path must not enter Git metadata" };
|
|
191
|
+
}
|
|
192
|
+
return { ok: true, value: segments.join("/") };
|
|
193
|
+
}
|
|
194
|
+
export function swarmConcretePathMatchesPattern(concretePath, pattern) {
|
|
195
|
+
const validatedPath = validateSwarmConcretePath(concretePath);
|
|
196
|
+
const normalizedPattern = normalizeSwarmPathPattern(pattern);
|
|
197
|
+
if (!validatedPath.ok || !normalizedPattern.ok)
|
|
198
|
+
return false;
|
|
199
|
+
return matchConcretePathSegments(validatedPath.value.split("/"), normalizedPattern.value.split("/"), 0, 0, new Map());
|
|
200
|
+
}
|
|
201
|
+
export function normalizeSwarmPathPattern(value) {
|
|
202
|
+
const candidate = value.trim().replaceAll("\\", "/");
|
|
203
|
+
if (candidate.length === 0) {
|
|
204
|
+
return { ok: false, error: "path pattern must not be empty" };
|
|
205
|
+
}
|
|
206
|
+
if (/^(?:[a-z]:|\/)/iu.test(candidate)) {
|
|
207
|
+
return { ok: false, error: "path pattern must be repository-relative" };
|
|
208
|
+
}
|
|
209
|
+
const segments = candidate.split("/");
|
|
210
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
211
|
+
return { ok: false, error: "path pattern contains an empty or traversal segment" };
|
|
212
|
+
}
|
|
213
|
+
for (const segment of segments) {
|
|
214
|
+
if (segment === "*" || segment === "**")
|
|
215
|
+
continue;
|
|
216
|
+
if (!/^[a-z0-9._@+-]+$/iu.test(segment)) {
|
|
217
|
+
return { ok: false, error: `malformed path pattern segment: ${segment}` };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return { ok: true, value: segments.join("/") };
|
|
221
|
+
}
|
|
222
|
+
export function swarmPathPatternContains(parent, child) {
|
|
223
|
+
const normalizedParent = normalizeSwarmPathPattern(parent);
|
|
224
|
+
const normalizedChild = normalizeSwarmPathPattern(child);
|
|
225
|
+
if (!normalizedParent.ok || !normalizedChild.ok)
|
|
226
|
+
return false;
|
|
227
|
+
if (normalizedParent.value === "**" || normalizedParent.value === normalizedChild.value)
|
|
228
|
+
return true;
|
|
229
|
+
if (normalizedParent.value.endsWith("/**")) {
|
|
230
|
+
const prefix = normalizedParent.value.slice(0, -3);
|
|
231
|
+
return normalizedChild.value === prefix || normalizedChild.value.startsWith(`${prefix}/`);
|
|
232
|
+
}
|
|
233
|
+
if (normalizedParent.value.startsWith("**/")) {
|
|
234
|
+
const suffix = normalizedParent.value.slice(3);
|
|
235
|
+
return normalizedChild.value === suffix || normalizedChild.value.endsWith(`/${suffix}`);
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
export function swarmPathPatternsOverlap(left, right) {
|
|
240
|
+
const normalizedLeft = normalizeSwarmPathPattern(left);
|
|
241
|
+
const normalizedRight = normalizeSwarmPathPattern(right);
|
|
242
|
+
if (!normalizedLeft.ok || !normalizedRight.ok)
|
|
243
|
+
return true;
|
|
244
|
+
if (swarmPathPatternContains(normalizedLeft.value, normalizedRight.value)
|
|
245
|
+
|| swarmPathPatternContains(normalizedRight.value, normalizedLeft.value)) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
const leftPrefix = literalPatternPrefix(normalizedLeft.value);
|
|
249
|
+
const rightPrefix = literalPatternPrefix(normalizedRight.value);
|
|
250
|
+
if (leftPrefix.length === 0 || rightPrefix.length === 0)
|
|
251
|
+
return true;
|
|
252
|
+
return leftPrefix.startsWith(`${rightPrefix}/`) || rightPrefix.startsWith(`${leftPrefix}/`);
|
|
253
|
+
}
|
|
254
|
+
export function validateSwarmParentContract(parent) {
|
|
255
|
+
const errors = [];
|
|
256
|
+
requireText(parent.policyVersion, "policyVersion", errors);
|
|
257
|
+
requireText(parent.objective, "objective", errors);
|
|
258
|
+
if (parent.definitionOfDone.length === 0 || parent.definitionOfDone.some((item) => !hasText(item))) {
|
|
259
|
+
addError(errors, "INVALID_DEFINITION_OF_DONE", "definitionOfDone", "at least one non-empty criterion is required");
|
|
260
|
+
}
|
|
261
|
+
validateBudget(parent.budget, "budget", errors);
|
|
262
|
+
if (!isPositiveInteger(parent.maxConcurrency)) {
|
|
263
|
+
addError(errors, "INVALID_CONCURRENCY", "maxConcurrency", "must be a positive integer");
|
|
264
|
+
}
|
|
265
|
+
if (!isPositiveFinite(parent.maxWallClockMs)) {
|
|
266
|
+
addError(errors, "INVALID_WALL_CLOCK", "maxWallClockMs", "must be a positive finite number");
|
|
267
|
+
}
|
|
268
|
+
if (parent.scope.allowedPaths.length === 0) {
|
|
269
|
+
addError(errors, "INVALID_SCOPE", "scope.allowedPaths", "at least one non-empty allowed path is required");
|
|
270
|
+
}
|
|
271
|
+
validatePathPatterns(parent.scope.allowedPaths, "scope.allowedPaths", errors);
|
|
272
|
+
validatePathPatterns(parent.scope.deniedPaths, "scope.deniedPaths", errors);
|
|
273
|
+
if (parent.globalVerifierStack.length === 0) {
|
|
274
|
+
addError(errors, "INVALID_GLOBAL_VERIFIER", "globalVerifierStack", "at least one global verifier step is required");
|
|
275
|
+
}
|
|
276
|
+
if (!SWARM_ORCHESTRATION_STRATEGIES.includes(parent.orchestrationStrategy)) {
|
|
277
|
+
addError(errors, "INVALID_ORCHESTRATION_STRATEGY", "orchestrationStrategy", "must use a canonical swarm orchestration strategy");
|
|
278
|
+
}
|
|
279
|
+
if (!Number.isInteger(parent.recoveryPolicy.maxReassignmentsPerTask) || parent.recoveryPolicy.maxReassignmentsPerTask < 0) {
|
|
280
|
+
addError(errors, "INVALID_RECOVERY_POLICY", "recoveryPolicy.maxReassignmentsPerTask", "must be a nonnegative integer");
|
|
281
|
+
}
|
|
282
|
+
return validationResult(errors);
|
|
283
|
+
}
|
|
284
|
+
export function validateSwarmDeterministicDemoReceiptEvidence(evidence) {
|
|
285
|
+
const errors = [];
|
|
286
|
+
if (evidence.evidenceKind !== "deterministic_demo") {
|
|
287
|
+
addError(errors, "INVALID_DEMO_EVIDENCE_KIND", "evidenceKind", "must identify deterministic demo evidence");
|
|
288
|
+
}
|
|
289
|
+
for (const key of ["swarmId", "agentId", "childRunId"]) {
|
|
290
|
+
requireText(evidence[key], key, errors);
|
|
291
|
+
}
|
|
292
|
+
if (!Array.isArray(evidence.taskIds)
|
|
293
|
+
|| evidence.taskIds.length === 0
|
|
294
|
+
|| evidence.taskIds.some((taskId) => !hasText(taskId))
|
|
295
|
+
|| new Set(evidence.taskIds).size !== evidence.taskIds.length) {
|
|
296
|
+
addError(errors, "INVALID_DEMO_TASK_BINDING", "taskIds", "must contain a non-empty, duplicate-free assigned task set");
|
|
297
|
+
}
|
|
298
|
+
if (!["passed", "failed", "unknown"].includes(evidence.referentialBinding)) {
|
|
299
|
+
addError(errors, "INVALID_DEMO_REFERENTIAL_BINDING", "referentialBinding", "must be passed, failed, or unknown");
|
|
300
|
+
}
|
|
301
|
+
if (evidence.signedIntegrity !== "not_evaluated") {
|
|
302
|
+
addError(errors, "DEMO_SIGNED_INTEGRITY_FORBIDDEN", "signedIntegrity", "deterministic demo evidence does not evaluate signed receipt integrity");
|
|
303
|
+
}
|
|
304
|
+
return validationResult(errors);
|
|
305
|
+
}
|
|
306
|
+
export function validateSwarmChildContract(parent, child) {
|
|
307
|
+
const errors = [];
|
|
308
|
+
requireText(child.agentId, "agentId", errors);
|
|
309
|
+
if (child.taskIds.length === 0 || child.taskIds.some((taskId) => !hasText(taskId))) {
|
|
310
|
+
addError(errors, "INVALID_CHILD_TASKS", "taskIds", "at least one non-empty task identifier is required");
|
|
311
|
+
}
|
|
312
|
+
validateBudget(child.budget, "budget", errors);
|
|
313
|
+
if (child.hardMaxUsd !== undefined && (!isNonnegativeFinite(child.hardMaxUsd) || child.hardMaxUsd > parent.budget.maxUsd)) {
|
|
314
|
+
addError(errors, "CHILD_HARD_USD_CAP_INVALID", "hardMaxUsd", "child hard USD cap must be finite, nonnegative, and within the parent cap");
|
|
315
|
+
}
|
|
316
|
+
if (child.hardMaxTokens !== undefined && (!isPositiveInteger(child.hardMaxTokens)
|
|
317
|
+
|| parent.budget.maxTokens === undefined
|
|
318
|
+
|| child.hardMaxTokens > parent.budget.maxTokens)) {
|
|
319
|
+
addError(errors, "CHILD_HARD_TOKEN_CAP_INVALID", "hardMaxTokens", "child hard token cap must be positive and within the parent token cap");
|
|
320
|
+
}
|
|
321
|
+
validatePathPatterns(child.scope.allowedPaths, "scope.allowedPaths", errors);
|
|
322
|
+
validatePathPatterns(child.scope.deniedPaths, "scope.deniedPaths", errors);
|
|
323
|
+
if (child.scope.allowedPaths.some((path) => !parent.scope.allowedPaths.some((allowed) => swarmPathPatternContains(allowed, path)))) {
|
|
324
|
+
addError(errors, "CHILD_SCOPE_WIDENED", "scope.allowedPaths", "child allowed paths must be contained by parent scope");
|
|
325
|
+
}
|
|
326
|
+
if (parent.scope.deniedPaths.some((denied) => !child.scope.deniedPaths.includes(denied))) {
|
|
327
|
+
addError(errors, "CHILD_DENIAL_DROPPED", "scope.deniedPaths", "child must inherit every parent denied path");
|
|
328
|
+
}
|
|
329
|
+
if (budgetWidened(parent.budget, child.budget)) {
|
|
330
|
+
addError(errors, "CHILD_BUDGET_WIDENED", "budget", "child budget must not exceed parent budget");
|
|
331
|
+
}
|
|
332
|
+
if (!isPositiveFinite(child.maxWallClockMs) || child.maxWallClockMs > parent.maxWallClockMs) {
|
|
333
|
+
addError(errors, "CHILD_WALL_CLOCK_WIDENED", "maxWallClockMs", "child wall-clock cap must not exceed parent cap");
|
|
334
|
+
}
|
|
335
|
+
if (!isSubset(child.permissions.networkDomains, parent.permissions.networkDomains)) {
|
|
336
|
+
addError(errors, "CHILD_NETWORK_WIDENED", "permissions.networkDomains", "child network permissions must be a parent subset");
|
|
337
|
+
}
|
|
338
|
+
if (!isSubset(child.permissions.commands, parent.permissions.commands)) {
|
|
339
|
+
addError(errors, "CHILD_COMMAND_WIDENED", "permissions.commands", "child commands must be a parent subset");
|
|
340
|
+
}
|
|
341
|
+
if (approvalWidened(parent.approvalPolicy, child.approvalPolicy)) {
|
|
342
|
+
addError(errors, "CHILD_APPROVAL_WIDENED", "approvalPolicy", "child approval authority must not exceed parent authority");
|
|
343
|
+
}
|
|
344
|
+
if (child.verifierAuthority !== "child_only") {
|
|
345
|
+
addError(errors, "CHILD_GLOBAL_VERIFIER_FORBIDDEN", "verifierAuthority", "only the parent may hold global verifier authority");
|
|
346
|
+
}
|
|
347
|
+
return validationResult(errors);
|
|
348
|
+
}
|
|
349
|
+
export function createSwarmRunRecord(draft, options = {}) {
|
|
350
|
+
const now = options.now ?? new Date().toISOString();
|
|
351
|
+
const events = draft.events
|
|
352
|
+
? draft.events.map((event) => ({ ...event, payload: { ...event.payload } }))
|
|
353
|
+
: [{
|
|
354
|
+
type: "SWARM_CREATED",
|
|
355
|
+
swarmId: draft.swarmId,
|
|
356
|
+
timestamp: now,
|
|
357
|
+
parentPolicyVersion: draft.parentContract.policyVersion,
|
|
358
|
+
payload: {}
|
|
359
|
+
}];
|
|
360
|
+
return {
|
|
361
|
+
schemaVersion: SWARM_SCHEMA_VERSION,
|
|
362
|
+
swarmId: draft.swarmId,
|
|
363
|
+
workspaceId: draft.workspaceId,
|
|
364
|
+
projectId: draft.projectId,
|
|
365
|
+
parentContract: cloneParentContract(draft.parentContract),
|
|
366
|
+
tasks: draft.tasks.map(cloneTask),
|
|
367
|
+
agents: draft.agents.map(cloneAgent),
|
|
368
|
+
dependencyWaivers: (draft.dependencyWaivers ?? []).map((waiver) => ({ ...waiver })),
|
|
369
|
+
budgetLedger: draft.budgetLedger
|
|
370
|
+
? cloneBudgetLedger(draft.budgetLedger)
|
|
371
|
+
: {
|
|
372
|
+
capUsd: draft.parentContract.budget.maxUsd,
|
|
373
|
+
settledUsd: 0,
|
|
374
|
+
settledTokens: 0,
|
|
375
|
+
leases: [],
|
|
376
|
+
...(draft.parentContract.budget.maxTokens !== undefined
|
|
377
|
+
? { capTokens: draft.parentContract.budget.maxTokens }
|
|
378
|
+
: {})
|
|
379
|
+
},
|
|
380
|
+
conflicts: (draft.conflicts ?? []).map((conflict) => ({ ...conflict, taskIds: [...conflict.taskIds], paths: [...conflict.paths] })),
|
|
381
|
+
verification: (draft.verification ?? []).map((item) => ({ ...item, steps: item.steps.map((step) => ({ ...step })) })),
|
|
382
|
+
outcome: { ...(draft.outcome ?? { state: "running", reason: "Swarm execution has not reached a parent terminal decision." }) },
|
|
383
|
+
events,
|
|
384
|
+
createdAt: draft.createdAt ?? now,
|
|
385
|
+
updatedAt: draft.updatedAt ?? now
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
export function validateSwarmRunRecord(record) {
|
|
389
|
+
const errors = [];
|
|
390
|
+
requireText(record.swarmId, "swarmId", errors);
|
|
391
|
+
requireText(record.workspaceId, "workspaceId", errors);
|
|
392
|
+
requireText(record.projectId, "projectId", errors);
|
|
393
|
+
errors.push(...validateSwarmParentContract(record.parentContract).errors);
|
|
394
|
+
addDuplicateErrors(record.tasks.map((task) => task.taskId), "DUPLICATE_TASK_ID", "tasks", errors);
|
|
395
|
+
addDuplicateErrors(record.agents.map((agent) => agent.agentId), "DUPLICATE_AGENT_ID", "agents", errors);
|
|
396
|
+
for (const [index, agent] of record.agents.entries()) {
|
|
397
|
+
const childResult = validateSwarmChildContract(record.parentContract, agent.contract);
|
|
398
|
+
errors.push(...childResult.errors.map((error) => ({ ...error, path: `agents[${index}].contract.${error.path}` })));
|
|
399
|
+
}
|
|
400
|
+
if (record.schemaVersion !== SWARM_SCHEMA_VERSION) {
|
|
401
|
+
addError(errors, "UNKNOWN_SCHEMA_VERSION", "schemaVersion", "unknown swarm schema version");
|
|
402
|
+
}
|
|
403
|
+
return validationResult(errors);
|
|
404
|
+
}
|
|
405
|
+
function cloneParentContract(parent) {
|
|
406
|
+
return {
|
|
407
|
+
...parent,
|
|
408
|
+
definitionOfDone: [...parent.definitionOfDone],
|
|
409
|
+
budget: { ...parent.budget },
|
|
410
|
+
scope: { allowedPaths: [...parent.scope.allowedPaths], deniedPaths: [...parent.scope.deniedPaths] },
|
|
411
|
+
permissions: { networkDomains: [...parent.permissions.networkDomains], commands: [...parent.permissions.commands] },
|
|
412
|
+
globalVerifierStack: parent.globalVerifierStack.map((step) => ({ ...step })),
|
|
413
|
+
stopPolicy: { ...parent.stopPolicy },
|
|
414
|
+
recoveryPolicy: { ...parent.recoveryPolicy },
|
|
415
|
+
approvalPolicy: { ...parent.approvalPolicy }
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
function cloneTask(task) {
|
|
419
|
+
return { ...task, dependsOn: [...task.dependsOn], writeScope: [...task.writeScope] };
|
|
420
|
+
}
|
|
421
|
+
function cloneAgent(agent) {
|
|
422
|
+
return {
|
|
423
|
+
...agent,
|
|
424
|
+
contract: {
|
|
425
|
+
...agent.contract,
|
|
426
|
+
taskIds: [...agent.contract.taskIds],
|
|
427
|
+
scope: { allowedPaths: [...agent.contract.scope.allowedPaths], deniedPaths: [...agent.contract.scope.deniedPaths] },
|
|
428
|
+
budget: { ...agent.contract.budget },
|
|
429
|
+
permissions: { networkDomains: [...agent.contract.permissions.networkDomains], commands: [...agent.contract.permissions.commands] },
|
|
430
|
+
approvalPolicy: { ...agent.contract.approvalPolicy }
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
function cloneLivePlanDraft(draft) {
|
|
435
|
+
return {
|
|
436
|
+
...draft,
|
|
437
|
+
parentContract: cloneParentContract(draft.parentContract),
|
|
438
|
+
tasks: draft.tasks.map(cloneTask),
|
|
439
|
+
agents: draft.agents.map(cloneAgent),
|
|
440
|
+
engine: { ...draft.engine }
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
function cloneBudgetLedger(ledger) {
|
|
444
|
+
return { ...ledger, leases: ledger.leases.map((lease) => ({ ...lease, ...(lease.actualUsage ? { actualUsage: { ...lease.actualUsage } } : {}) })) };
|
|
445
|
+
}
|
|
446
|
+
function validateBudget(budget, path, errors) {
|
|
447
|
+
if (!isNonnegativeFinite(budget.maxUsd) || !isNonnegativeFinite(budget.softLimitUsd) || budget.softLimitUsd > budget.maxUsd || !isPositiveInteger(budget.maxIterations) || (budget.maxTokens !== undefined && !isPositiveInteger(budget.maxTokens))) {
|
|
448
|
+
addError(errors, "INVALID_BUDGET", path, "budget caps must be finite, nonnegative, ordered, and use positive iteration/token limits");
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function budgetWidened(parent, child) {
|
|
452
|
+
return child.maxUsd > parent.maxUsd
|
|
453
|
+
|| child.softLimitUsd > parent.softLimitUsd
|
|
454
|
+
|| child.maxIterations > parent.maxIterations
|
|
455
|
+
|| (parent.maxTokens !== undefined
|
|
456
|
+
&& (child.maxTokens === undefined || child.maxTokens > parent.maxTokens));
|
|
457
|
+
}
|
|
458
|
+
function approvalWidened(parent, child) {
|
|
459
|
+
const keys = ["dependencyAdds", "migrations", "configChanges", "externalWrites"];
|
|
460
|
+
return keys.some((key) => child[key] === true && parent[key] !== true);
|
|
461
|
+
}
|
|
462
|
+
function addDuplicateErrors(values, code, path, errors) {
|
|
463
|
+
const seen = new Set();
|
|
464
|
+
for (const value of values) {
|
|
465
|
+
if (seen.has(value)) {
|
|
466
|
+
addError(errors, code, path, `duplicate identifier: ${value}`);
|
|
467
|
+
}
|
|
468
|
+
seen.add(value);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function isSubset(values, allowed) {
|
|
472
|
+
return values.every((value) => allowed.includes(value));
|
|
473
|
+
}
|
|
474
|
+
function validationResult(errors) {
|
|
475
|
+
return errors.length === 0 ? { ok: true, errors: [] } : { ok: false, errors };
|
|
476
|
+
}
|
|
477
|
+
function requireText(value, path, errors) {
|
|
478
|
+
if (!hasText(value))
|
|
479
|
+
addError(errors, "REQUIRED_FIELD", path, "non-empty text is required");
|
|
480
|
+
}
|
|
481
|
+
function addError(errors, code, path, message) {
|
|
482
|
+
errors.push({ code, path, message });
|
|
483
|
+
}
|
|
484
|
+
function hasText(value) {
|
|
485
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
486
|
+
}
|
|
487
|
+
function isNonnegativeFinite(value) {
|
|
488
|
+
return Number.isFinite(value) && value >= 0;
|
|
489
|
+
}
|
|
490
|
+
function isPositiveFinite(value) {
|
|
491
|
+
return Number.isFinite(value) && value > 0;
|
|
492
|
+
}
|
|
493
|
+
function isPositiveInteger(value) {
|
|
494
|
+
return Number.isInteger(value) && value > 0;
|
|
495
|
+
}
|
|
496
|
+
function isCanonicalIsoDate(value) {
|
|
497
|
+
if (typeof value !== "string")
|
|
498
|
+
return false;
|
|
499
|
+
const parsed = Date.parse(value);
|
|
500
|
+
return Number.isFinite(parsed) && new Date(parsed).toISOString() === value;
|
|
501
|
+
}
|
|
502
|
+
function canonicalJson(value) {
|
|
503
|
+
if (value === null || typeof value !== "object")
|
|
504
|
+
return JSON.stringify(value);
|
|
505
|
+
if (Array.isArray(value))
|
|
506
|
+
return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
|
|
507
|
+
const entries = Object.entries(value)
|
|
508
|
+
.filter(([, item]) => item !== undefined)
|
|
509
|
+
.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
|
|
510
|
+
return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(",")}}`;
|
|
511
|
+
}
|
|
512
|
+
function validatePathPatterns(patterns, path, errors) {
|
|
513
|
+
patterns.forEach((pattern, index) => {
|
|
514
|
+
const result = normalizeSwarmPathPattern(pattern);
|
|
515
|
+
if (!result.ok) {
|
|
516
|
+
addError(errors, "INVALID_SCOPE_PATTERN", `${path}[${index}]`, result.error);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
function literalPatternPrefix(pattern) {
|
|
521
|
+
const segments = [];
|
|
522
|
+
for (const segment of pattern.split("/")) {
|
|
523
|
+
if (segment === "*" || segment === "**")
|
|
524
|
+
break;
|
|
525
|
+
segments.push(segment);
|
|
526
|
+
}
|
|
527
|
+
return segments.join("/");
|
|
528
|
+
}
|
|
529
|
+
function matchConcretePathSegments(pathSegments, patternSegments, pathIndex, patternIndex, memo) {
|
|
530
|
+
const key = `${pathIndex}:${patternIndex}`;
|
|
531
|
+
const cached = memo.get(key);
|
|
532
|
+
if (cached !== undefined)
|
|
533
|
+
return cached;
|
|
534
|
+
let matched;
|
|
535
|
+
if (patternIndex === patternSegments.length) {
|
|
536
|
+
matched = pathIndex === pathSegments.length;
|
|
537
|
+
}
|
|
538
|
+
else if (patternSegments[patternIndex] === "**") {
|
|
539
|
+
matched = matchConcretePathSegments(pathSegments, patternSegments, pathIndex, patternIndex + 1, memo)
|
|
540
|
+
|| (pathIndex < pathSegments.length
|
|
541
|
+
&& matchConcretePathSegments(pathSegments, patternSegments, pathIndex + 1, patternIndex, memo));
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
matched = pathIndex < pathSegments.length
|
|
545
|
+
&& (patternSegments[patternIndex] === "*" || patternSegments[patternIndex] === pathSegments[pathIndex])
|
|
546
|
+
&& matchConcretePathSegments(pathSegments, patternSegments, pathIndex + 1, patternIndex + 1, memo);
|
|
547
|
+
}
|
|
548
|
+
memo.set(key, matched);
|
|
549
|
+
return matched;
|
|
550
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type ExternalOutcomeContract, type ExternalOutcomeEvidenceReference, type ExternalOutcomeResult } from "../contracts/index.js";
|
|
2
|
+
export declare class ExternalOutcomeContractError extends Error {
|
|
3
|
+
readonly errors: Array<{
|
|
4
|
+
path: string;
|
|
5
|
+
message: string;
|
|
6
|
+
}>;
|
|
7
|
+
constructor(errors: Array<{
|
|
8
|
+
path: string;
|
|
9
|
+
message: string;
|
|
10
|
+
}>);
|
|
11
|
+
}
|
|
12
|
+
export declare class ExternalOutcomePolicyError extends Error {
|
|
13
|
+
readonly code: string;
|
|
14
|
+
constructor(code: string, message: string);
|
|
15
|
+
}
|
|
16
|
+
export interface VerifyExternalOutcomesOptions {
|
|
17
|
+
env?: NodeJS.ProcessEnv;
|
|
18
|
+
allowLocal?: boolean;
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
now?: () => Date;
|
|
21
|
+
}
|
|
22
|
+
export interface WriteExternalOutcomeEvidenceOptions {
|
|
23
|
+
runsRoot: string;
|
|
24
|
+
runId?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface ExternalOutcomeEvidenceWrite {
|
|
27
|
+
path: string;
|
|
28
|
+
relativePath: string;
|
|
29
|
+
sha256: string;
|
|
30
|
+
reference: ExternalOutcomeEvidenceReference;
|
|
31
|
+
}
|
|
32
|
+
export declare function hashExternalOutcomeContract(contract: ExternalOutcomeContract): string;
|
|
33
|
+
export declare function verifyExternalOutcomes(contract: ExternalOutcomeContract, options?: VerifyExternalOutcomesOptions): Promise<ExternalOutcomeResult>;
|
|
34
|
+
export declare function writeExternalOutcomeEvidence(result: ExternalOutcomeResult, options: WriteExternalOutcomeEvidenceOptions): Promise<ExternalOutcomeEvidenceWrite>;
|