@martinloop/mcp 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,76 @@
|
|
|
1
|
+
import { type SwarmConflictRecord, type SwarmCleanupRecord, type SwarmEvent, type SwarmPatchAdmission, type SwarmRunRecord } from "../../contracts/index.js";
|
|
2
|
+
import { type PersistedSwarmCandidateArtifact } from "./candidates.js";
|
|
3
|
+
import type { SwarmWorkspaceManager, SwarmWorkspaceRuntimeHandle } from "./workspaces.js";
|
|
4
|
+
export interface SwarmIntegrationGitCommand {
|
|
5
|
+
readonly cwd: string;
|
|
6
|
+
readonly args: readonly string[];
|
|
7
|
+
readonly stdin?: Buffer;
|
|
8
|
+
}
|
|
9
|
+
export interface SwarmIntegrationGitResult {
|
|
10
|
+
readonly stdout: Buffer;
|
|
11
|
+
readonly stderr: Buffer;
|
|
12
|
+
}
|
|
13
|
+
export type SwarmIntegrationGitRunner = (command: SwarmIntegrationGitCommand) => Promise<SwarmIntegrationGitResult>;
|
|
14
|
+
export interface SwarmIntegrationCandidate {
|
|
15
|
+
readonly candidateId: string;
|
|
16
|
+
}
|
|
17
|
+
export interface SwarmIntegrationReconstruction {
|
|
18
|
+
readonly reconstructionId: string;
|
|
19
|
+
readonly swarmId: string;
|
|
20
|
+
readonly failedCandidateId: string;
|
|
21
|
+
readonly previousWorkspaceId: string;
|
|
22
|
+
readonly replacementWorkspaceId: string;
|
|
23
|
+
readonly baselineCommit: string;
|
|
24
|
+
readonly replayedCandidateIds: readonly string[];
|
|
25
|
+
readonly expectedTreeHash: string;
|
|
26
|
+
readonly actualTreeHash: string;
|
|
27
|
+
readonly state: "completed" | "failed";
|
|
28
|
+
readonly recordedAt: string;
|
|
29
|
+
readonly cleanup?: SwarmCleanupRecord;
|
|
30
|
+
readonly diagnostic?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface SwarmIntegrationEvidenceBundle {
|
|
33
|
+
readonly decision: SwarmPatchAdmission;
|
|
34
|
+
readonly event: SwarmEvent;
|
|
35
|
+
readonly conflict?: SwarmConflictRecord;
|
|
36
|
+
readonly reconstruction?: SwarmIntegrationReconstruction;
|
|
37
|
+
}
|
|
38
|
+
export interface SwarmIntegrationEvidenceStore {
|
|
39
|
+
loadPersistedCandidate(candidateId: string): Promise<PersistedSwarmCandidateArtifact | undefined>;
|
|
40
|
+
loadPersistedAdmission(candidateId: string): Promise<SwarmPatchAdmission | undefined>;
|
|
41
|
+
/** Atomically claims the swarm/candidate pair; identity is retained as evidence, not as replay-key entropy. */
|
|
42
|
+
claimCandidateIntegration(input: {
|
|
43
|
+
swarmId: string;
|
|
44
|
+
candidateId: string;
|
|
45
|
+
identitySha256: string;
|
|
46
|
+
}): Promise<"claimed" | "already_claimed">;
|
|
47
|
+
persistOutcome(bundle: SwarmIntegrationEvidenceBundle): Promise<void>;
|
|
48
|
+
persistReconstruction(record: SwarmIntegrationReconstruction): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
export interface IntegrateSwarmCandidatesInput {
|
|
51
|
+
readonly swarmRecord: SwarmRunRecord;
|
|
52
|
+
readonly workspaceManager: SwarmWorkspaceManager;
|
|
53
|
+
readonly integrationWorkspaceId: string;
|
|
54
|
+
readonly candidates: readonly SwarmIntegrationCandidate[];
|
|
55
|
+
readonly evidenceStore: SwarmIntegrationEvidenceStore;
|
|
56
|
+
readonly conflictPolicy?: "stop" | "continue";
|
|
57
|
+
readonly gitRunner?: SwarmIntegrationGitRunner;
|
|
58
|
+
readonly now?: () => string;
|
|
59
|
+
readonly createId?: (prefix: string) => string;
|
|
60
|
+
readonly createReconstructionWorkspaceId?: (sequence: number) => string;
|
|
61
|
+
/** Internal lifecycle seam used to retain exact manager authority on exceptional paths. */
|
|
62
|
+
readonly onWorkspaceCreated?: (workspace: SwarmWorkspaceRuntimeHandle) => void;
|
|
63
|
+
}
|
|
64
|
+
export interface SwarmIntegrationResult {
|
|
65
|
+
readonly completed: boolean;
|
|
66
|
+
readonly workspace: SwarmWorkspaceRuntimeHandle;
|
|
67
|
+
readonly finalTreeHash: string;
|
|
68
|
+
readonly admittedCandidateIds: readonly string[];
|
|
69
|
+
readonly decisions: readonly SwarmPatchAdmission[];
|
|
70
|
+
readonly events: readonly SwarmEvent[];
|
|
71
|
+
readonly conflicts: readonly SwarmConflictRecord[];
|
|
72
|
+
readonly reconstructions: readonly SwarmIntegrationReconstruction[];
|
|
73
|
+
readonly cleanupAuthorized: boolean;
|
|
74
|
+
}
|
|
75
|
+
export declare function integrateSwarmCandidates(input: IntegrateSwarmCandidatesInput): Promise<SwarmIntegrationResult>;
|
|
76
|
+
export declare function hashSwarmIntegrationPatch(patch: Buffer): string;
|
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { SWARM_SCHEMA_VERSION, swarmConcretePathMatchesPattern, validateSwarmChildContract, validateSwarmRunRecord } from "../../contracts/index.js";
|
|
4
|
+
import { verifyPersistedSwarmCandidateArtifact } from "./candidates.js";
|
|
5
|
+
import { validateSwarmTaskGraph } from "./scheduler.js";
|
|
6
|
+
const MAX_GIT_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
7
|
+
const MAX_DIAGNOSTIC_LENGTH = 512;
|
|
8
|
+
export async function integrateSwarmCandidates(input) {
|
|
9
|
+
const gitRunner = input.gitRunner ?? runGit;
|
|
10
|
+
const now = input.now ?? (() => new Date().toISOString());
|
|
11
|
+
const createId = input.createId ?? ((prefix) => `${prefix}-${randomUUID()}`);
|
|
12
|
+
const createReconstructionWorkspaceId = input.createReconstructionWorkspaceId
|
|
13
|
+
?? ((sequence) => `integration-recovery-${String(sequence)}`);
|
|
14
|
+
const conflictPolicy = input.conflictPolicy ?? "stop";
|
|
15
|
+
assertIntegrationAuthority(input);
|
|
16
|
+
assertCandidateReferencesUnique(input.candidates);
|
|
17
|
+
const persistedCandidates = [];
|
|
18
|
+
for (const reference of input.candidates) {
|
|
19
|
+
const stored = await input.evidenceStore.loadPersistedCandidate(reference.candidateId);
|
|
20
|
+
if (!stored || stored.candidate.candidateId !== reference.candidateId) {
|
|
21
|
+
throw new Error(`Persisted swarm candidate is missing: ${reference.candidateId}`);
|
|
22
|
+
}
|
|
23
|
+
persistedCandidates.push({ artifact: snapshotCandidateArtifact(stored) });
|
|
24
|
+
}
|
|
25
|
+
const ordered = orderCandidates(input.swarmRecord.tasks, persistedCandidates);
|
|
26
|
+
assertCandidateAliasesUnique(ordered);
|
|
27
|
+
let workspace = await input.workspaceManager.createWorkspace({
|
|
28
|
+
kind: "integration",
|
|
29
|
+
workspaceId: input.integrationWorkspaceId
|
|
30
|
+
});
|
|
31
|
+
input.onWorkspaceCreated?.(workspace);
|
|
32
|
+
assertManagerIntegrationWorkspace(input, workspace, input.integrationWorkspaceId);
|
|
33
|
+
let expectedTreeHash = await baselineTreeHash(gitRunner, workspace.path, input.workspaceManager.baselineCommit);
|
|
34
|
+
const initialTree = await writeTree(gitRunner, workspace.path);
|
|
35
|
+
if (initialTree !== expectedTreeHash) {
|
|
36
|
+
throw new Error("Integration workspace index does not match the immutable baseline tree.");
|
|
37
|
+
}
|
|
38
|
+
await assertNoUnstagedOrUntracked(gitRunner, workspace.path);
|
|
39
|
+
const admitted = [];
|
|
40
|
+
const decisions = [];
|
|
41
|
+
const events = [];
|
|
42
|
+
const conflicts = [];
|
|
43
|
+
const reconstructions = [];
|
|
44
|
+
let cleanupAuthorized = true;
|
|
45
|
+
for (const entry of ordered) {
|
|
46
|
+
const artifact = entry.artifact;
|
|
47
|
+
const persistedAdmission = await input.evidenceStore.loadPersistedAdmission(artifact.candidate.candidateId);
|
|
48
|
+
const preconditionFailure = validateIntegrationPrecondition(input, artifact, persistedAdmission, expectedTreeHash);
|
|
49
|
+
if (preconditionFailure) {
|
|
50
|
+
const bundle = createRejectedBundle(input, artifact, {
|
|
51
|
+
reasonCode: preconditionFailure.reasonCode,
|
|
52
|
+
diagnostic: preconditionFailure.diagnostic,
|
|
53
|
+
preTreeHash: expectedTreeHash,
|
|
54
|
+
now: now(),
|
|
55
|
+
createId
|
|
56
|
+
});
|
|
57
|
+
cleanupAuthorized = await persistBundle(input.evidenceStore, bundle);
|
|
58
|
+
decisions.push(bundle.decision);
|
|
59
|
+
events.push(bundle.event);
|
|
60
|
+
if (!cleanupAuthorized || conflictPolicy === "stop")
|
|
61
|
+
break;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const claim = await input.evidenceStore.claimCandidateIntegration({
|
|
65
|
+
swarmId: input.swarmRecord.swarmId,
|
|
66
|
+
candidateId: artifact.candidate.candidateId,
|
|
67
|
+
identitySha256: artifact.identitySha256
|
|
68
|
+
});
|
|
69
|
+
if (claim === "already_claimed") {
|
|
70
|
+
const bundle = createRejectedBundle(input, artifact, {
|
|
71
|
+
reasonCode: "integration_precondition_failed",
|
|
72
|
+
diagnostic: "Candidate integration claim already exists.",
|
|
73
|
+
preTreeHash: expectedTreeHash,
|
|
74
|
+
now: now(),
|
|
75
|
+
createId
|
|
76
|
+
});
|
|
77
|
+
cleanupAuthorized = await persistBundle(input.evidenceStore, bundle);
|
|
78
|
+
decisions.push(bundle.decision);
|
|
79
|
+
events.push(bundle.event);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
const actualPreTree = await writeTree(gitRunner, workspace.path);
|
|
83
|
+
if (actualPreTree !== expectedTreeHash) {
|
|
84
|
+
const bundle = createRejectedBundle(input, artifact, {
|
|
85
|
+
reasonCode: "integration_precondition_failed",
|
|
86
|
+
diagnostic: "Integration tree changed before candidate application.",
|
|
87
|
+
preTreeHash: actualPreTree,
|
|
88
|
+
now: now(),
|
|
89
|
+
createId
|
|
90
|
+
});
|
|
91
|
+
cleanupAuthorized = await persistBundle(input.evidenceStore, bundle);
|
|
92
|
+
decisions.push(bundle.decision);
|
|
93
|
+
events.push(bundle.event);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
await assertNoUnstagedOrUntracked(gitRunner, workspace.path);
|
|
97
|
+
try {
|
|
98
|
+
await gitRunner({
|
|
99
|
+
cwd: workspace.path,
|
|
100
|
+
args: ["-c", "core.autocrlf=false", "apply", "--check", "--index", "-"],
|
|
101
|
+
stdin: artifact.patch
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
const postFailureTree = await writeTree(gitRunner, workspace.path);
|
|
106
|
+
if (postFailureTree !== expectedTreeHash) {
|
|
107
|
+
throw new Error("Git apply --check mutated the integration index.");
|
|
108
|
+
}
|
|
109
|
+
const conflict = createConflict(input, artifact, expectedTreeHash, error, now(), createId);
|
|
110
|
+
const bundle = createRejectedBundle(input, artifact, {
|
|
111
|
+
reasonCode: "integration_conflict",
|
|
112
|
+
diagnostic: conflict.diagnostic ?? "Candidate conflicts with the integrated tree.",
|
|
113
|
+
preTreeHash: expectedTreeHash,
|
|
114
|
+
now: now(),
|
|
115
|
+
createId,
|
|
116
|
+
conflict
|
|
117
|
+
});
|
|
118
|
+
cleanupAuthorized = await persistBundle(input.evidenceStore, bundle);
|
|
119
|
+
decisions.push(bundle.decision);
|
|
120
|
+
events.push(bundle.event);
|
|
121
|
+
conflicts.push(conflict);
|
|
122
|
+
if (!cleanupAuthorized || conflictPolicy === "stop")
|
|
123
|
+
break;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
await gitRunner({
|
|
128
|
+
cwd: workspace.path,
|
|
129
|
+
args: ["-c", "core.autocrlf=false", "apply", "--index", "-"],
|
|
130
|
+
stdin: artifact.patch
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
const failedBundle = createRejectedBundle(input, artifact, {
|
|
135
|
+
reasonCode: "integration_apply_failed",
|
|
136
|
+
diagnostic: sanitizeDiagnostic(error),
|
|
137
|
+
preTreeHash: expectedTreeHash,
|
|
138
|
+
now: now(),
|
|
139
|
+
createId
|
|
140
|
+
});
|
|
141
|
+
const failurePersisted = await persistBundle(input.evidenceStore, failedBundle);
|
|
142
|
+
decisions.push(failedBundle.decision);
|
|
143
|
+
events.push(failedBundle.event);
|
|
144
|
+
if (!failurePersisted) {
|
|
145
|
+
cleanupAuthorized = false;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
const reconstructed = await reconstructIntegration({
|
|
149
|
+
input,
|
|
150
|
+
gitRunner,
|
|
151
|
+
currentWorkspace: workspace,
|
|
152
|
+
admitted,
|
|
153
|
+
failedArtifact: artifact,
|
|
154
|
+
expectedTreeHash,
|
|
155
|
+
sequence: reconstructions.length + 1,
|
|
156
|
+
now,
|
|
157
|
+
createId,
|
|
158
|
+
createReconstructionWorkspaceId
|
|
159
|
+
});
|
|
160
|
+
workspace = reconstructed.workspace;
|
|
161
|
+
reconstructions.push(reconstructed.record);
|
|
162
|
+
try {
|
|
163
|
+
await input.evidenceStore.persistReconstruction(reconstructed.record);
|
|
164
|
+
cleanupAuthorized = true;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
cleanupAuthorized = false;
|
|
168
|
+
}
|
|
169
|
+
if (!reconstructed.ok || !cleanupAuthorized)
|
|
170
|
+
break;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
await assertNoUnstagedOrUntracked(gitRunner, workspace.path);
|
|
174
|
+
const postTreeHash = await writeTree(gitRunner, workspace.path);
|
|
175
|
+
const admittedBundle = createAdmittedBundle(input, artifact, expectedTreeHash, postTreeHash, now(), createId);
|
|
176
|
+
try {
|
|
177
|
+
await input.evidenceStore.persistOutcome(admittedBundle);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
const reconstructed = await reconstructIntegration({
|
|
181
|
+
input,
|
|
182
|
+
gitRunner,
|
|
183
|
+
currentWorkspace: workspace,
|
|
184
|
+
admitted,
|
|
185
|
+
failedArtifact: artifact,
|
|
186
|
+
expectedTreeHash,
|
|
187
|
+
sequence: reconstructions.length + 1,
|
|
188
|
+
now,
|
|
189
|
+
createId,
|
|
190
|
+
createReconstructionWorkspaceId,
|
|
191
|
+
preserveFailedWorkspace: true,
|
|
192
|
+
diagnostic: sanitizeDiagnostic(error)
|
|
193
|
+
});
|
|
194
|
+
workspace = reconstructed.workspace;
|
|
195
|
+
reconstructions.push(reconstructed.record);
|
|
196
|
+
const failureBundle = createRejectedBundle(input, artifact, {
|
|
197
|
+
reasonCode: "artifact_persistence_failed",
|
|
198
|
+
diagnostic: sanitizeDiagnostic(error),
|
|
199
|
+
preTreeHash: expectedTreeHash,
|
|
200
|
+
now: now(),
|
|
201
|
+
createId,
|
|
202
|
+
reconstruction: reconstructed.record
|
|
203
|
+
});
|
|
204
|
+
cleanupAuthorized = await persistBundle(input.evidenceStore, failureBundle);
|
|
205
|
+
cleanupAuthorized = false;
|
|
206
|
+
decisions.push(failureBundle.decision);
|
|
207
|
+
events.push(failureBundle.event);
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
admitted.push({ artifact, treeHash: postTreeHash });
|
|
211
|
+
expectedTreeHash = postTreeHash;
|
|
212
|
+
decisions.push(admittedBundle.decision);
|
|
213
|
+
events.push(admittedBundle.event);
|
|
214
|
+
}
|
|
215
|
+
const allAdmitted = admitted.length === ordered.length;
|
|
216
|
+
return Object.freeze({
|
|
217
|
+
completed: allAdmitted,
|
|
218
|
+
workspace,
|
|
219
|
+
finalTreeHash: expectedTreeHash,
|
|
220
|
+
admittedCandidateIds: Object.freeze(admitted.map(({ artifact }) => artifact.candidate.candidateId)),
|
|
221
|
+
decisions: Object.freeze(decisions),
|
|
222
|
+
events: Object.freeze(events),
|
|
223
|
+
conflicts: Object.freeze(conflicts),
|
|
224
|
+
reconstructions: Object.freeze(reconstructions),
|
|
225
|
+
cleanupAuthorized
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
function assertIntegrationAuthority(input) {
|
|
229
|
+
const graph = validateSwarmTaskGraph(input.swarmRecord.tasks);
|
|
230
|
+
if (!graph.ok)
|
|
231
|
+
throw new Error("Swarm task graph is invalid for deterministic integration.");
|
|
232
|
+
if (!validateSwarmRunRecord(input.swarmRecord).ok
|
|
233
|
+
|| input.swarmRecord.outcome.state !== "running"
|
|
234
|
+
|| input.integrationWorkspaceId.trim().length === 0) {
|
|
235
|
+
throw new Error("Integration requires current parent authority and the manager baseline.");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function assertManagerIntegrationWorkspace(input, workspace, expectedWorkspaceId) {
|
|
239
|
+
if (workspace.record.kind !== "integration"
|
|
240
|
+
|| workspace.record.workspaceId !== expectedWorkspaceId
|
|
241
|
+
|| workspace.record.swarmId !== input.swarmRecord.swarmId
|
|
242
|
+
|| workspace.record.baselineCommit !== input.workspaceManager.baselineCommit
|
|
243
|
+
|| workspace.record.state !== "active") {
|
|
244
|
+
throw new Error("Workspace manager returned an integration handle outside current parent authority.");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function validateIntegrationPrecondition(input, artifact, persisted, expectedTreeHash) {
|
|
248
|
+
if (!verifyPersistedSwarmCandidateArtifact(artifact)) {
|
|
249
|
+
return { reasonCode: "patch_hash_mismatch", diagnostic: "Persisted candidate bytes or identity hash do not match." };
|
|
250
|
+
}
|
|
251
|
+
const candidate = artifact.candidate;
|
|
252
|
+
if (candidate.swarmId !== input.swarmRecord.swarmId
|
|
253
|
+
|| candidate.baselineCommit !== input.workspaceManager.baselineCommit
|
|
254
|
+
|| !persisted
|
|
255
|
+
|| persisted.schemaVersion !== SWARM_SCHEMA_VERSION
|
|
256
|
+
|| persisted.admissionId.trim().length === 0
|
|
257
|
+
|| persisted.state !== "admitted"
|
|
258
|
+
|| persisted.reasonCode !== "admitted"
|
|
259
|
+
|| persisted.receiptIntegrity !== "verified"
|
|
260
|
+
|| !admissionMatchesCandidate(persisted, artifact)
|
|
261
|
+
|| (persisted.preIntegrationTreeHash !== undefined && persisted.preIntegrationTreeHash !== expectedTreeHash)) {
|
|
262
|
+
return {
|
|
263
|
+
reasonCode: "integration_precondition_failed",
|
|
264
|
+
diagnostic: "Candidate identity, baseline, or persisted admission does not match integration authority."
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
const agent = input.swarmRecord.agents.find((entry) => entry.agentId === candidate.agentId);
|
|
268
|
+
if (!agent
|
|
269
|
+
|| agent.childRunId !== candidate.childRunId
|
|
270
|
+
|| agent.status !== "verified"
|
|
271
|
+
|| !validateSwarmChildContract(input.swarmRecord.parentContract, agent.contract).ok
|
|
272
|
+
|| candidate.taskIds.length !== 1
|
|
273
|
+
|| !sameStrings(candidate.taskIds, agent.contract.taskIds)) {
|
|
274
|
+
return { reasonCode: "integration_precondition_failed", diagnostic: "Candidate agent/task binding is stale." };
|
|
275
|
+
}
|
|
276
|
+
const task = input.swarmRecord.tasks.find((entry) => entry.taskId === candidate.taskIds[0]);
|
|
277
|
+
if (!task
|
|
278
|
+
|| task.status !== "accepted"
|
|
279
|
+
|| task.assignedAgentId !== candidate.agentId
|
|
280
|
+
|| candidate.changedPaths.some((path) => (input.swarmRecord.parentContract.scope.deniedPaths.some((pattern) => swarmConcretePathMatchesPattern(path, pattern))
|
|
281
|
+
|| agent.contract.scope.deniedPaths.some((pattern) => swarmConcretePathMatchesPattern(path, pattern))
|
|
282
|
+
|| !input.swarmRecord.parentContract.scope.allowedPaths.some((pattern) => swarmConcretePathMatchesPattern(path, pattern))
|
|
283
|
+
|| !agent.contract.scope.allowedPaths.some((pattern) => swarmConcretePathMatchesPattern(path, pattern))
|
|
284
|
+
|| !task.writeScope.some((pattern) => swarmConcretePathMatchesPattern(path, pattern))))) {
|
|
285
|
+
return { reasonCode: "integration_precondition_failed", diagnostic: "Candidate task or scope authority is stale." };
|
|
286
|
+
}
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
function admissionMatchesCandidate(admission, artifact) {
|
|
290
|
+
const candidate = artifact.candidate;
|
|
291
|
+
return admission.candidateId === candidate.candidateId
|
|
292
|
+
&& admission.swarmId === candidate.swarmId
|
|
293
|
+
&& admission.workspaceId === candidate.workspaceId
|
|
294
|
+
&& admission.childRunId === candidate.childRunId
|
|
295
|
+
&& admission.agentId === candidate.agentId
|
|
296
|
+
&& admission.baselineCommit === candidate.baselineCommit
|
|
297
|
+
&& sameStrings(admission.taskIds, candidate.taskIds)
|
|
298
|
+
&& sameStrings(admission.changedPaths, candidate.changedPaths);
|
|
299
|
+
}
|
|
300
|
+
function orderCandidates(tasks, candidates) {
|
|
301
|
+
const order = stableTopologicalOrder(tasks);
|
|
302
|
+
const rank = new Map(order.map((taskId, index) => [taskId, index]));
|
|
303
|
+
return [...candidates].sort((left, right) => {
|
|
304
|
+
const leftRanks = left.artifact.candidate.taskIds.map((taskId) => rank.get(taskId) ?? Number.MAX_SAFE_INTEGER);
|
|
305
|
+
const rightRanks = right.artifact.candidate.taskIds.map((taskId) => rank.get(taskId) ?? Number.MAX_SAFE_INTEGER);
|
|
306
|
+
const byDependency = Math.max(...leftRanks) - Math.max(...rightRanks);
|
|
307
|
+
if (byDependency !== 0)
|
|
308
|
+
return byDependency;
|
|
309
|
+
const byTasks = [...left.artifact.candidate.taskIds].sort().join("\0")
|
|
310
|
+
.localeCompare([...right.artifact.candidate.taskIds].sort().join("\0"));
|
|
311
|
+
if (byTasks !== 0)
|
|
312
|
+
return byTasks;
|
|
313
|
+
return left.artifact.candidate.candidateId.localeCompare(right.artifact.candidate.candidateId);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
function stableTopologicalOrder(tasks) {
|
|
317
|
+
const byId = new Map(tasks.map((task) => [task.taskId, task]));
|
|
318
|
+
const indegree = new Map(tasks.map((task) => [task.taskId, task.dependsOn.length]));
|
|
319
|
+
const dependents = new Map();
|
|
320
|
+
for (const task of tasks) {
|
|
321
|
+
for (const dependency of task.dependsOn) {
|
|
322
|
+
const list = dependents.get(dependency) ?? [];
|
|
323
|
+
list.push(task.taskId);
|
|
324
|
+
dependents.set(dependency, list);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const ready = tasks.filter((task) => indegree.get(task.taskId) === 0)
|
|
328
|
+
.map((task) => task.taskId)
|
|
329
|
+
.sort();
|
|
330
|
+
const ordered = [];
|
|
331
|
+
while (ready.length > 0) {
|
|
332
|
+
const taskId = ready.shift();
|
|
333
|
+
if (!taskId || !byId.has(taskId))
|
|
334
|
+
continue;
|
|
335
|
+
ordered.push(taskId);
|
|
336
|
+
for (const dependent of (dependents.get(taskId) ?? []).sort()) {
|
|
337
|
+
const next = (indegree.get(dependent) ?? 0) - 1;
|
|
338
|
+
indegree.set(dependent, next);
|
|
339
|
+
if (next === 0) {
|
|
340
|
+
ready.push(dependent);
|
|
341
|
+
ready.sort();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (ordered.length !== tasks.length)
|
|
346
|
+
throw new Error("Swarm task graph contains a cycle.");
|
|
347
|
+
return ordered;
|
|
348
|
+
}
|
|
349
|
+
function assertCandidateReferencesUnique(candidates) {
|
|
350
|
+
const ids = new Set();
|
|
351
|
+
for (const candidate of candidates) {
|
|
352
|
+
if (candidate.candidateId.trim().length === 0 || ids.has(candidate.candidateId)) {
|
|
353
|
+
throw new Error("Duplicate or empty swarm candidate references are forbidden.");
|
|
354
|
+
}
|
|
355
|
+
ids.add(candidate.candidateId);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function assertCandidateAliasesUnique(candidates) {
|
|
359
|
+
const candidateIds = new Set();
|
|
360
|
+
const identities = new Set();
|
|
361
|
+
for (const { artifact } of candidates) {
|
|
362
|
+
if (candidateIds.has(artifact.candidate.candidateId)
|
|
363
|
+
|| identities.has(artifact.identitySha256)) {
|
|
364
|
+
throw new Error("Duplicate or aliased swarm candidates are forbidden.");
|
|
365
|
+
}
|
|
366
|
+
candidateIds.add(artifact.candidate.candidateId);
|
|
367
|
+
identities.add(artifact.identitySha256);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
async function reconstructIntegration(context) {
|
|
371
|
+
let cleanup;
|
|
372
|
+
if (!context.preserveFailedWorkspace) {
|
|
373
|
+
cleanup = await context.input.workspaceManager.removeWorkspace(context.currentWorkspace, {
|
|
374
|
+
evidencePersisted: true,
|
|
375
|
+
processTreeClosed: true,
|
|
376
|
+
force: true
|
|
377
|
+
});
|
|
378
|
+
if (cleanup.state !== "completed") {
|
|
379
|
+
const actualTreeHash = await writeTree(context.gitRunner, context.currentWorkspace.path);
|
|
380
|
+
const record = Object.freeze({
|
|
381
|
+
reconstructionId: context.createId("reconstruction"),
|
|
382
|
+
swarmId: context.input.swarmRecord.swarmId,
|
|
383
|
+
failedCandidateId: context.failedArtifact.candidate.candidateId,
|
|
384
|
+
previousWorkspaceId: context.currentWorkspace.record.workspaceId,
|
|
385
|
+
replacementWorkspaceId: context.currentWorkspace.record.workspaceId,
|
|
386
|
+
baselineCommit: context.input.workspaceManager.baselineCommit,
|
|
387
|
+
replayedCandidateIds: Object.freeze(context.admitted.map(({ artifact }) => artifact.candidate.candidateId)),
|
|
388
|
+
expectedTreeHash: context.expectedTreeHash,
|
|
389
|
+
actualTreeHash,
|
|
390
|
+
state: "failed",
|
|
391
|
+
recordedAt: context.now(),
|
|
392
|
+
cleanup,
|
|
393
|
+
diagnostic: "Integration workspace cleanup did not complete; reconstruction was not started."
|
|
394
|
+
});
|
|
395
|
+
return { ok: false, workspace: context.currentWorkspace, record };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const replacementWorkspaceId = context.createReconstructionWorkspaceId(context.sequence);
|
|
399
|
+
const workspace = await context.input.workspaceManager.createWorkspace({
|
|
400
|
+
kind: "integration",
|
|
401
|
+
workspaceId: replacementWorkspaceId
|
|
402
|
+
});
|
|
403
|
+
context.input.onWorkspaceCreated?.(workspace);
|
|
404
|
+
assertManagerIntegrationWorkspace(context.input, workspace, replacementWorkspaceId);
|
|
405
|
+
let actualTreeHash = await writeTree(context.gitRunner, workspace.path);
|
|
406
|
+
let diagnostic = context.diagnostic;
|
|
407
|
+
let ok = true;
|
|
408
|
+
for (const replay of context.admitted) {
|
|
409
|
+
try {
|
|
410
|
+
await applyPatch(context.gitRunner, workspace.path, replay.artifact.patch);
|
|
411
|
+
actualTreeHash = await writeTree(context.gitRunner, workspace.path);
|
|
412
|
+
if (actualTreeHash !== replay.treeHash) {
|
|
413
|
+
throw new Error("Replayed candidate tree hash does not match persisted integration evidence.");
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
ok = false;
|
|
418
|
+
diagnostic = sanitizeDiagnostic(error);
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (actualTreeHash !== context.expectedTreeHash)
|
|
423
|
+
ok = false;
|
|
424
|
+
const record = Object.freeze({
|
|
425
|
+
reconstructionId: context.createId("reconstruction"),
|
|
426
|
+
swarmId: context.input.swarmRecord.swarmId,
|
|
427
|
+
failedCandidateId: context.failedArtifact.candidate.candidateId,
|
|
428
|
+
previousWorkspaceId: context.currentWorkspace.record.workspaceId,
|
|
429
|
+
replacementWorkspaceId: workspace.record.workspaceId,
|
|
430
|
+
baselineCommit: context.input.workspaceManager.baselineCommit,
|
|
431
|
+
replayedCandidateIds: Object.freeze(context.admitted.map(({ artifact }) => artifact.candidate.candidateId)),
|
|
432
|
+
expectedTreeHash: context.expectedTreeHash,
|
|
433
|
+
actualTreeHash,
|
|
434
|
+
state: ok ? "completed" : "failed",
|
|
435
|
+
recordedAt: context.now(),
|
|
436
|
+
...(cleanup ? { cleanup } : {}),
|
|
437
|
+
...(diagnostic ? { diagnostic } : {})
|
|
438
|
+
});
|
|
439
|
+
return { ok, workspace, record };
|
|
440
|
+
}
|
|
441
|
+
function snapshotCandidateArtifact(artifact) {
|
|
442
|
+
return {
|
|
443
|
+
candidate: {
|
|
444
|
+
...artifact.candidate,
|
|
445
|
+
taskIds: [...artifact.candidate.taskIds],
|
|
446
|
+
changedPaths: [...artifact.candidate.changedPaths]
|
|
447
|
+
},
|
|
448
|
+
attemptId: artifact.attemptId,
|
|
449
|
+
proposalId: artifact.proposalId,
|
|
450
|
+
identitySha256: artifact.identitySha256,
|
|
451
|
+
candidateTreeHash: artifact.candidateTreeHash,
|
|
452
|
+
patch: Buffer.from(artifact.patch),
|
|
453
|
+
manifest: Buffer.from(artifact.manifest),
|
|
454
|
+
manifestSha256: artifact.manifestSha256,
|
|
455
|
+
receiptIntegrity: { ...artifact.receiptIntegrity }
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
async function applyPatch(gitRunner, cwd, patch) {
|
|
459
|
+
await gitRunner({ cwd, args: ["-c", "core.autocrlf=false", "apply", "--check", "--index", "-"], stdin: patch });
|
|
460
|
+
await gitRunner({ cwd, args: ["-c", "core.autocrlf=false", "apply", "--index", "-"], stdin: patch });
|
|
461
|
+
}
|
|
462
|
+
function createAdmittedBundle(input, artifact, preTreeHash, postTreeHash, decidedAt, createId) {
|
|
463
|
+
return {
|
|
464
|
+
decision: createDecision(input, artifact, {
|
|
465
|
+
state: "admitted",
|
|
466
|
+
reasonCode: "admitted",
|
|
467
|
+
preTreeHash,
|
|
468
|
+
postTreeHash,
|
|
469
|
+
decidedAt,
|
|
470
|
+
createId
|
|
471
|
+
}),
|
|
472
|
+
event: createEvent(input, artifact, "CHILD_PATCH_ADMITTED", decidedAt, {
|
|
473
|
+
candidateId: artifact.candidate.candidateId,
|
|
474
|
+
patchSha256: artifact.candidate.patchSha256,
|
|
475
|
+
preIntegrationTreeHash: preTreeHash,
|
|
476
|
+
postIntegrationTreeHash: postTreeHash
|
|
477
|
+
})
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
function createRejectedBundle(input, artifact, context) {
|
|
481
|
+
return {
|
|
482
|
+
decision: createDecision(input, artifact, {
|
|
483
|
+
state: "rejected",
|
|
484
|
+
reasonCode: context.reasonCode,
|
|
485
|
+
preTreeHash: context.preTreeHash,
|
|
486
|
+
decidedAt: context.now,
|
|
487
|
+
diagnostic: context.diagnostic,
|
|
488
|
+
createId: context.createId
|
|
489
|
+
}),
|
|
490
|
+
event: createEvent(input, artifact, context.conflict ? "INTEGRATION_CONFLICT" : "CHILD_PATCH_REJECTED", context.now, {
|
|
491
|
+
candidateId: artifact.candidate.candidateId,
|
|
492
|
+
reasonCode: context.reasonCode,
|
|
493
|
+
preIntegrationTreeHash: context.preTreeHash,
|
|
494
|
+
diagnostic: context.diagnostic
|
|
495
|
+
}),
|
|
496
|
+
...(context.conflict ? { conflict: context.conflict } : {}),
|
|
497
|
+
...(context.reconstruction ? { reconstruction: context.reconstruction } : {})
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function createDecision(input, artifact, context) {
|
|
501
|
+
const candidate = artifact.candidate;
|
|
502
|
+
return Object.freeze({
|
|
503
|
+
schemaVersion: SWARM_SCHEMA_VERSION,
|
|
504
|
+
admissionId: context.createId("integration-admission"),
|
|
505
|
+
candidateId: candidate.candidateId,
|
|
506
|
+
swarmId: input.swarmRecord.swarmId,
|
|
507
|
+
workspaceId: candidate.workspaceId,
|
|
508
|
+
childRunId: candidate.childRunId,
|
|
509
|
+
agentId: candidate.agentId,
|
|
510
|
+
taskIds: Object.freeze([...candidate.taskIds]),
|
|
511
|
+
baselineCommit: candidate.baselineCommit,
|
|
512
|
+
changedPaths: Object.freeze([...candidate.changedPaths]),
|
|
513
|
+
state: context.state,
|
|
514
|
+
reasonCode: context.reasonCode,
|
|
515
|
+
receiptIntegrity: artifact.receiptIntegrity.state,
|
|
516
|
+
decidedAt: context.decidedAt,
|
|
517
|
+
preIntegrationTreeHash: context.preTreeHash,
|
|
518
|
+
...(context.postTreeHash ? { postIntegrationTreeHash: context.postTreeHash } : {}),
|
|
519
|
+
...(context.diagnostic ? { diagnostic: context.diagnostic } : {})
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
function createEvent(input, artifact, type, timestamp, payload) {
|
|
523
|
+
return Object.freeze({
|
|
524
|
+
type,
|
|
525
|
+
swarmId: input.swarmRecord.swarmId,
|
|
526
|
+
timestamp,
|
|
527
|
+
parentPolicyVersion: input.swarmRecord.parentContract.policyVersion,
|
|
528
|
+
taskId: artifact.candidate.taskIds[0],
|
|
529
|
+
agentId: artifact.candidate.agentId,
|
|
530
|
+
childRunId: artifact.candidate.childRunId,
|
|
531
|
+
payload: Object.freeze(payload)
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
function createConflict(input, artifact, preTreeHash, error, recordedAt, createId) {
|
|
535
|
+
return Object.freeze({
|
|
536
|
+
conflictId: createId("conflict"),
|
|
537
|
+
taskIds: [...artifact.candidate.taskIds],
|
|
538
|
+
paths: [...artifact.candidate.changedPaths],
|
|
539
|
+
state: "blocking",
|
|
540
|
+
schemaVersion: SWARM_SCHEMA_VERSION,
|
|
541
|
+
candidateId: artifact.candidate.candidateId,
|
|
542
|
+
childRunId: artifact.candidate.childRunId,
|
|
543
|
+
agentId: artifact.candidate.agentId,
|
|
544
|
+
preIntegrationTreeHash: preTreeHash,
|
|
545
|
+
recordedAt,
|
|
546
|
+
diagnostic: sanitizeDiagnostic(error)
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
async function persistBundle(store, bundle) {
|
|
550
|
+
try {
|
|
551
|
+
await store.persistOutcome(bundle);
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
async function baselineTreeHash(gitRunner, cwd, baselineCommit) {
|
|
559
|
+
return gitScalar(gitRunner, cwd, ["rev-parse", `${baselineCommit}^{tree}`]);
|
|
560
|
+
}
|
|
561
|
+
async function writeTree(gitRunner, cwd) {
|
|
562
|
+
return gitScalar(gitRunner, cwd, ["write-tree"]);
|
|
563
|
+
}
|
|
564
|
+
async function assertNoUnstagedOrUntracked(gitRunner, cwd) {
|
|
565
|
+
await gitRunner({ cwd, args: ["diff", "--quiet", "--no-ext-diff", "--"] });
|
|
566
|
+
const untracked = await gitRunner({
|
|
567
|
+
cwd,
|
|
568
|
+
args: ["ls-files", "--others", "--exclude-standard", "-z"]
|
|
569
|
+
});
|
|
570
|
+
if (untracked.stdout.length !== 0) {
|
|
571
|
+
throw new Error("Integration workspace contains untracked mutation.");
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
async function gitScalar(gitRunner, cwd, args) {
|
|
575
|
+
const result = await gitRunner({ cwd, args });
|
|
576
|
+
return result.stdout.toString("utf8").trim();
|
|
577
|
+
}
|
|
578
|
+
function runGit(command) {
|
|
579
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
580
|
+
const child = execFile("git", [...command.args], {
|
|
581
|
+
cwd: command.cwd,
|
|
582
|
+
encoding: null,
|
|
583
|
+
windowsHide: true,
|
|
584
|
+
maxBuffer: MAX_GIT_OUTPUT_BYTES
|
|
585
|
+
}, (error, stdout, stderr) => {
|
|
586
|
+
if (error) {
|
|
587
|
+
rejectPromise(Object.assign(error, { stderr }));
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
resolvePromise({
|
|
591
|
+
stdout: Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout),
|
|
592
|
+
stderr: Buffer.isBuffer(stderr) ? stderr : Buffer.from(stderr)
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
if (command.stdin)
|
|
596
|
+
child.stdin?.end(command.stdin);
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
function sameStrings(left, right) {
|
|
600
|
+
if (left.length !== right.length)
|
|
601
|
+
return false;
|
|
602
|
+
const a = [...left].sort();
|
|
603
|
+
const b = [...right].sort();
|
|
604
|
+
return a.every((value, index) => value === b[index]);
|
|
605
|
+
}
|
|
606
|
+
function sanitizeDiagnostic(error) {
|
|
607
|
+
const raw = error instanceof Error
|
|
608
|
+
? (error.stderr?.toString() || error.message)
|
|
609
|
+
: String(error);
|
|
610
|
+
return raw.replace(/[\u0000-\u001f\u007f]+/gu, " ").replace(/\s+/gu, " ").trim()
|
|
611
|
+
.slice(0, MAX_DIAGNOSTIC_LENGTH);
|
|
612
|
+
}
|
|
613
|
+
export function hashSwarmIntegrationPatch(patch) {
|
|
614
|
+
return createHash("sha256").update(patch).digest("hex");
|
|
615
|
+
}
|