@deksden-com/dd-flow-cli 0.9.0-beta.124 → 0.9.0-beta.127
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/CHANGELOG.md +16 -0
- package/README.md +10 -1
- package/dist/build-info.json +7 -7
- package/dist/cli/argument-values.js +38 -0
- package/dist/cli/command-inputs.js +4 -3
- package/dist/cli/help.js +1 -0
- package/dist/cli/hook-ingress.js +1 -1
- package/dist/cli/run-cli.js +126 -193
- package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
- package/dist/cli/runtime-maintenance-ingress.js +51 -0
- package/dist/cli/runtime-process.js +128 -0
- package/dist/cli.js +8 -5
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
- package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
- package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
- package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
- package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
- package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
- package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
- package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +60 -13
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +151 -72
- package/dist/harness-runtime/lib/dd-agy.mjs +11 -23
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +9 -14
- package/dist/harness-runtime/lib/dd-codex.mjs +252 -80
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +14 -19
- package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +56 -30
- package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +18 -27
- package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +56 -53
- package/dist/harness-runtime/lib/dd-zcode.mjs +200 -111
- package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
- package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
- package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
- package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
- package/dist/harness-runtime/lib/managed-daemon.d.mts +10 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +228 -40
- package/dist/harness-runtime/lib/native-children.d.mts +13 -0
- package/dist/harness-runtime/lib/native-children.mjs +78 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
- package/dist/harness-runtime/lib/native-streams.mjs +45 -0
- package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
- package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
- package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
- package/dist/harness-runtime/lib/process-json.mjs +136 -38
- package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
- package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
- package/dist/runtime/context.js +8 -4
- package/dist/schemas/run-control-receipt.schema.json +19 -0
- package/dist/services/code-checks.js +79 -9
- package/dist/services/codex-capacity.js +117 -41
- package/dist/services/codex-hook-delivery.js +2 -2
- package/dist/services/compatibility-preflight.js +4 -1
- package/dist/services/controller-fanout.js +9 -76
- package/dist/services/engines.js +70 -20
- package/dist/services/eval-snapshots.js +179 -40
- package/dist/services/external-work-launch.js +13 -8
- package/dist/services/harness-adapter.js +175 -11
- package/dist/services/hooks.js +65 -21
- package/dist/services/lifecycle-invocations.js +404 -32
- package/dist/services/managed-lease-monitor.js +128 -0
- package/dist/services/managed-processes.js +103 -47
- package/dist/services/merge-server.js +58 -21
- package/dist/services/recovery-observation-budget.js +67 -7
- package/dist/services/recovery-snapshot-database.js +7 -4
- package/dist/services/run-control-receipt.js +1 -1
- package/dist/services/run-control-worker.js +177 -118
- package/dist/services/run-control.js +99 -8
- package/dist/services/run-controller-adapter.js +169 -46
- package/dist/services/run-controller-capture.js +14 -2
- package/dist/services/run-controller-process.js +11 -4
- package/dist/services/run-controller-state.js +68 -7
- package/dist/services/run-controller.js +166 -94
- package/dist/services/run-fork.js +8 -3
- package/dist/services/run-recovery-guard.js +75 -0
- package/dist/services/run-recovery-runtime.js +1 -56
- package/dist/services/run-recovery.js +17 -13
- package/dist/services/runs.js +39 -6
- package/dist/services/runtime-budget.js +32 -15
- package/dist/services/runtime-scope-control.js +8 -0
- package/dist/services/runtime-scope-resume.js +30 -8
- package/dist/services/runtime-scope-worker.js +118 -73
- package/dist/services/runtime-service.js +6 -7
- package/dist/services/scope-writer-cleanup.js +3 -2
- package/dist/services/stage-context.js +135 -0
- package/dist/services/stage-lifecycle.js +3 -15
- package/dist/services/stage-pause.js +3 -2
- package/dist/services/usage.js +14 -3
- package/dist/services/vnext-code-review.js +10 -9
- package/dist/services/vnext-code.js +10 -8
- package/dist/services/vnext-contracts.js +10 -1
- package/dist/services/vnext-execution-profile.js +75 -2
- package/dist/services/vnext-fanout.js +5 -0
- package/dist/services/vnext-merge.js +10 -9
- package/dist/services/vnext-plan-review.js +21 -17
- package/dist/services/vnext-plan.js +9 -7
- package/dist/services/vnext-protocolize.js +15 -9
- package/dist/services/vnext-specify.js +6 -6
- package/dist/services/work-registry.js +34 -4
- package/dist/services/workspace-bootstrap.js +14 -3
- package/dist/shared/errors.js +11 -6
- package/dist/storage/agent-output.js +26 -0
- package/dist/storage/database.js +80 -21
- package/package.json +7 -4
|
@@ -2,6 +2,34 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import { CAPACITY_POLICY, capacityBackoff, nativeItemsSettled, normalizeCodexFailure, overloadBurst, parseCapacityOperation, refusalObservation, terminalCodexOverload } from "../harness-runtime/lib/codex-capacity-policy.mjs";
|
|
4
4
|
import { AppError } from "../shared/errors.js";
|
|
5
|
+
import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
|
|
6
|
+
export function eligibleCodexChildOverload(child, parentSessionId) {
|
|
7
|
+
return typeof child.session_id === "string" && child.session_id !== parentSessionId && child.parent_session_id === parentSessionId && child.provenance !== "parent_mismatch" && child.status === "failed" && typeof child.terminal_turn_id === "string" && nativeItemsSettled(child.native_turn_items)
|
|
8
|
+
&& terminalCodexOverload({ code: "turn_interrupted", details: { provider_session_id: child.session_id, turn_id: child.terminal_turn_id, native_turn_id: child.terminal_turn_id, terminal_status: "failed", provider_error: child.provider_error } }, child.session_id, child.terminal_turn_id);
|
|
9
|
+
}
|
|
10
|
+
/** A terminal root is not a settled tree. Only the daemon's exact owned tree
|
|
11
|
+
* with a settled root and positively live descendants permits waiting. */
|
|
12
|
+
export function capacityReadiness(seen, sessionId, operationId, turnId, items, allowDescendants = false) {
|
|
13
|
+
if (seen.provider_session_id !== sessionId || seen.terminal_turn_id !== undefined && seen.terminal_turn_id !== turnId || !nativeItemsSettled(items))
|
|
14
|
+
return "unproven";
|
|
15
|
+
const ids = Array.isArray(seen.tree_session_ids) ? seen.tree_session_ids : [];
|
|
16
|
+
if (!ids.length || ids[0] !== sessionId || ids.some(id => typeof id !== "string" || !id) || new Set(ids).size !== ids.length)
|
|
17
|
+
return "unproven";
|
|
18
|
+
const observations = Array.isArray(seen.tree_observations) ? seen.tree_observations : [];
|
|
19
|
+
const exactTree = observations.length === ids.length && ids.every(id => observations.filter(item => item.provider_session_id === id).length === 1);
|
|
20
|
+
const settlement = seen.settlement;
|
|
21
|
+
const budgetSettled = !settlement || settlement.state === "not_required" || settlement.state === "settled" && settlement.operation_ids?.includes(operationId) === true;
|
|
22
|
+
const treeSettled = allowDescendants ? exactTree && observations.every(item => item.settled === true) && Array.isArray(seen.unsettled_sessions) && seen.unsettled_sessions.length === 0 : ids.length === 1;
|
|
23
|
+
if (seen.settled === true && treeSettled && budgetSettled)
|
|
24
|
+
return "ready";
|
|
25
|
+
if (!allowDescendants || seen.settled !== false || !exactTree || ids.length < 2 || observations[0]?.provider_session_id !== sessionId || observations[0].settled !== true
|
|
26
|
+
|| settlement && settlement.state !== "not_required" && !(settlement.state === "pending" && settlement.reason === "tree_unsettled"))
|
|
27
|
+
return "unproven";
|
|
28
|
+
const live = observations.filter(item => item.settled === false);
|
|
29
|
+
const unsettled = seen.unsettled_sessions;
|
|
30
|
+
const knownLive = live.length > 0 && live.every(item => item.provider_session_id !== sessionId && ["active", "running", "inProgress"].includes(String(typeof item.status === "object" && item.status ? item.status.type : item.status)));
|
|
31
|
+
return knownLive && observations.every(item => typeof item.settled === "boolean") && Array.isArray(unsettled) && unsettled.length === live.length && live.every(item => unsettled.includes(item.provider_session_id)) ? "owned_children_live" : "unproven";
|
|
32
|
+
}
|
|
5
33
|
/** Permit acquisition must still belong to the original task, not merely an
|
|
6
34
|
* open RUN. Controls/inspection and ordinary successor prompts are unaffected. */
|
|
7
35
|
export function assertCapacityOwnerAdmission(context, record, operationId, binding) {
|
|
@@ -96,6 +124,20 @@ export async function continueCodexCapacity(input) {
|
|
|
96
124
|
const promptSha = crypto.createHash("sha256").update(text).digest("hex");
|
|
97
125
|
let ordinal = parsed.ordinal + 1;
|
|
98
126
|
const conflict = (message) => { throw new AppError("capacity_continuation_conflict", message, 1, { operation_id: operationId, root_operation_id: rootId, session_id: input.sessionId }); };
|
|
127
|
+
const blocked = (message) => new AppError("capacity_continuation_blocked", message, 1, { capacity_uncertainty: true, operation_id: operationId, root_operation_id: rootId, provider_session_id: input.sessionId, primary_error: error });
|
|
128
|
+
const observe = async (action, observedOperationId = operationId) => {
|
|
129
|
+
try {
|
|
130
|
+
return await action();
|
|
131
|
+
}
|
|
132
|
+
catch (cause) {
|
|
133
|
+
let primary = cause;
|
|
134
|
+
for (let depth = 0; depth < 4 && primary?.code === "harness_adapter_failed"; depth++)
|
|
135
|
+
primary = (primary.cause ?? primary.details?.cause);
|
|
136
|
+
if (isObservationLoss(primary))
|
|
137
|
+
throw new AppError("operation_observation_lost", "Capacity native observation was lost; do not cancel or resend", 1, { capacity_uncertainty: true, root_operation_id: rootId, operation_id: observedOperationId, provider_session_id: input.sessionId, predecessor_error: error, cause });
|
|
138
|
+
throw cause;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
99
141
|
const refusals = [...(input.retainedFailures?.() ?? [])];
|
|
100
142
|
if (parsed.ordinal > history.length)
|
|
101
143
|
conflict("Continuation has no complete retained predecessor chain");
|
|
@@ -132,7 +174,7 @@ export async function continueCodexCapacity(input) {
|
|
|
132
174
|
throw error;
|
|
133
175
|
let observed;
|
|
134
176
|
try {
|
|
135
|
-
observed = await input.inspectOperation(intent.operation_id);
|
|
177
|
+
observed = await observe(() => input.inspectOperation(intent.operation_id), intent.operation_id);
|
|
136
178
|
}
|
|
137
179
|
catch (cause) {
|
|
138
180
|
let code = cause;
|
|
@@ -142,28 +184,51 @@ export async function continueCodexCapacity(input) {
|
|
|
142
184
|
throw cause;
|
|
143
185
|
break;
|
|
144
186
|
}
|
|
145
|
-
if (observed.operation_id !== intent.operation_id || observed.session_id !== input.sessionId || observed.operation !== "session.prompt"
|
|
146
|
-
|
|
187
|
+
if (observed.operation_id !== intent.operation_id || observed.session_id !== input.sessionId || observed.operation !== "session.prompt")
|
|
188
|
+
conflict("Retained native successor identity changed");
|
|
189
|
+
if (observed.primary_error)
|
|
190
|
+
throw observed.primary_error;
|
|
191
|
+
while (["prepared", "dispatching", "running", "awaiting_provider", "outcome_unknown"].includes(String(observed.state))) {
|
|
192
|
+
input.signal?.throwIfAborted();
|
|
193
|
+
input.assertCurrent();
|
|
194
|
+
const boundary = await observe(async () => await input.decision?.() ?? "continue", intent.operation_id);
|
|
195
|
+
if (boundary === "completed" || boundary === "paused")
|
|
196
|
+
return { provider_session_id: input.sessionId, capacity_decision: boundary };
|
|
197
|
+
if (boundary === "reconciliation_blocked")
|
|
198
|
+
throw new AppError("capacity_continuation_blocked", "Retained native successor has an unresolved owner boundary", 1, { operation_id: intent.operation_id, primary_error: error });
|
|
199
|
+
if (input.deadline !== undefined && Date.now() >= input.deadline)
|
|
200
|
+
throw new AppError("capacity_owner_deadline_exceeded", "Retained successor exhausted its original deadline", 1);
|
|
201
|
+
await delay(250, undefined, { signal: input.signal });
|
|
202
|
+
observed = await observe(() => input.inspectOperation(intent.operation_id), intent.operation_id);
|
|
203
|
+
input.signal?.throwIfAborted();
|
|
204
|
+
input.assertCurrent();
|
|
205
|
+
if (observed.operation_id !== intent.operation_id || observed.session_id !== input.sessionId || observed.operation !== "session.prompt")
|
|
206
|
+
conflict("Retained native successor identity changed");
|
|
207
|
+
if (observed.primary_error)
|
|
208
|
+
throw observed.primary_error;
|
|
209
|
+
}
|
|
147
210
|
if (observed.state === "completed" && observed.result && typeof observed.result === "object") {
|
|
148
211
|
input.signal?.throwIfAborted();
|
|
149
212
|
input.assertCurrent();
|
|
150
|
-
const boundary = await input.decision?.() ?? "continue";
|
|
213
|
+
const boundary = await observe(async () => await input.decision?.() ?? "continue", intent.operation_id);
|
|
151
214
|
input.signal?.throwIfAborted();
|
|
152
215
|
input.assertCurrent();
|
|
153
216
|
if (boundary === "reconciliation_blocked")
|
|
154
217
|
throw new AppError("capacity_continuation_blocked", "Retained completion cannot be reconciled with its owner boundary", 1);
|
|
155
218
|
return boundary === "paused" || boundary === "completed" ? { provider_session_id: input.sessionId, capacity_decision: boundary } : observed.result;
|
|
156
219
|
}
|
|
220
|
+
if (observed.state === "failed" && observed.error && !terminalCodexOverload(observed.error, input.sessionId))
|
|
221
|
+
throw observed.error;
|
|
157
222
|
if (observed.state !== "failed" || !terminalCodexOverload(observed.error, input.sessionId))
|
|
158
|
-
throw error;
|
|
223
|
+
throw new AppError("operation_observation_lost", "Retained successor outcome is not proven; do not resend", 1, { operation_id: intent.operation_id, primary_error: error, observed });
|
|
159
224
|
operationId = intent.operation_id;
|
|
160
225
|
error = observed.error;
|
|
161
226
|
ordinal += 1;
|
|
162
227
|
}
|
|
163
|
-
const decide = async () => {
|
|
228
|
+
const decide = async (observedOperationId = operationId) => {
|
|
164
229
|
input.signal?.throwIfAborted();
|
|
165
230
|
input.assertCurrent();
|
|
166
|
-
const decision = await input.decision?.() ?? "continue";
|
|
231
|
+
const decision = await observe(async () => await input.decision?.() ?? "continue", observedOperationId);
|
|
167
232
|
input.signal?.throwIfAborted();
|
|
168
233
|
input.assertCurrent();
|
|
169
234
|
if (decision === "reconciliation_blocked")
|
|
@@ -183,45 +248,30 @@ export async function continueCodexCapacity(input) {
|
|
|
183
248
|
const completed = await decide();
|
|
184
249
|
if (completed)
|
|
185
250
|
return completed;
|
|
186
|
-
const prior = await input.inspectOperation(operationId);
|
|
251
|
+
const prior = await observe(() => input.inspectOperation(operationId));
|
|
187
252
|
const failure = prior.error;
|
|
188
253
|
const confirmed = normalizeCodexFailure(failure, input.sessionId);
|
|
189
254
|
const turnId = confirmed?.turn_id;
|
|
190
255
|
input.signal?.throwIfAborted();
|
|
191
256
|
input.assertCurrent();
|
|
257
|
+
if (prior.primary_error)
|
|
258
|
+
throw prior.primary_error;
|
|
192
259
|
if (prior.operation_id !== operationId || prior.operation !== "session.prompt" || prior.session_id !== input.sessionId
|
|
193
260
|
|| prior.state !== "failed" || prior.primary_error || !turnId || !terminalCodexOverload(failure, input.sessionId, turnId)) {
|
|
194
|
-
|
|
195
|
-
return null;
|
|
196
|
-
throw error;
|
|
261
|
+
throw blocked("Retained predecessor does not prove the exact terminal overloaded Turn");
|
|
197
262
|
}
|
|
198
|
-
const
|
|
263
|
+
const readiness = async () => {
|
|
199
264
|
input.signal?.throwIfAborted();
|
|
200
265
|
input.assertCurrent();
|
|
201
|
-
const seen = await input.inspectSession(turnId);
|
|
266
|
+
const seen = await observe(() => input.inspectSession(turnId));
|
|
202
267
|
input.signal?.throwIfAborted();
|
|
203
268
|
input.assertCurrent();
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const items = seen.terminal_turn_id === turnId && seen.terminal_status === "failed" ? seen.native_turn_items : confirmed?.native_turn_items;
|
|
207
|
-
if (!nativeItemsSettled(items))
|
|
208
|
-
return false;
|
|
209
|
-
const settlement = seen.settlement;
|
|
210
|
-
const ids = Array.isArray(seen.tree_session_ids) ? seen.tree_session_ids : [];
|
|
211
|
-
const observations = Array.isArray(seen.tree_observations) ? seen.tree_observations : [];
|
|
212
|
-
const treeProven = input.allowSettledDescendants
|
|
213
|
-
? ids.length > 0 && ids[0] === input.sessionId && observations.length === ids.length
|
|
214
|
-
&& ids.every(id => observations.some(item => item.provider_session_id === id && item.settled === true))
|
|
215
|
-
&& Array.isArray(seen.unsettled_sessions) && seen.unsettled_sessions.length === 0
|
|
216
|
-
: ids.length === 1 && ids[0] === input.sessionId;
|
|
217
|
-
return seen.provider_session_id === input.sessionId && seen.settled === true && treeProven
|
|
218
|
-
&& (!settlement || settlement.state === "not_required" || settlement.state === "settled" && settlement.operation_ids?.includes(operationId) === true);
|
|
269
|
+
const items = (seen.terminal_turn_id === turnId && seen.terminal_status === "failed" ? seen.native_turn_items : confirmed?.native_turn_items);
|
|
270
|
+
return capacityReadiness(seen, input.sessionId, operationId, turnId, items, input.allowSettledDescendants);
|
|
219
271
|
};
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
throw error;
|
|
224
|
-
}
|
|
272
|
+
const ready = await readiness();
|
|
273
|
+
if (ready === "unproven")
|
|
274
|
+
throw blocked("Native predecessor readiness is unproven; do not cancel or replay to manufacture proof");
|
|
225
275
|
const native = normalizeCodexFailure(failure, input.sessionId, turnId);
|
|
226
276
|
let refusal = refusals.find(item => item.ordinal === ordinal - 1);
|
|
227
277
|
if (refusal && (refusal.turn_id !== turnId || refusal.session_id !== input.sessionId || refusal.policy !== CAPACITY_POLICY))
|
|
@@ -239,27 +289,52 @@ export async function continueCodexCapacity(input) {
|
|
|
239
289
|
conflict("Retained continuation has no original failure time");
|
|
240
290
|
const observedAt = typeof native.observed_at === "string" && Number.isFinite(Date.parse(native.observed_at)) ? native.observed_at : new Date().toISOString();
|
|
241
291
|
refusal = refusalObservation(failure, ordinal - 1, observedAt);
|
|
242
|
-
input.
|
|
292
|
+
if (!input.atomicIntent)
|
|
293
|
+
input.retainFailure?.(operationId, refusal);
|
|
243
294
|
refusals.push(refusal);
|
|
244
295
|
}
|
|
245
296
|
const burst = overloadBurst(refusals);
|
|
246
|
-
if (burst)
|
|
297
|
+
if (burst) {
|
|
298
|
+
if (input.atomicIntent)
|
|
299
|
+
input.retainFailure?.(operationId, refusal);
|
|
247
300
|
throw new AppError("provider_overload_burst", "Two consecutive continuation Turns were overloaded within two minutes", 1, { ...burst, root_operation_id: rootId, provider_session_id: input.sessionId, continuation_count: ordinal - 1, primary_error: failure });
|
|
301
|
+
}
|
|
248
302
|
const nextId = `${rootId}:capacity:${ordinal}:session.prompt`;
|
|
249
303
|
const existing = history.find(item => item.operation_id === nextId);
|
|
250
304
|
if (existing && (existing.predecessor !== operationId || existing.turn_id !== turnId || existing.prompt_sha256 !== promptSha || existing.ordinal !== ordinal))
|
|
251
305
|
throw error;
|
|
252
306
|
const wait = existing ? Math.max(0, Date.parse(existing.not_before) - Date.now()) : capacityDelay(failure, ordinal);
|
|
253
|
-
if (!Number.isFinite(wait) || Date.now() + wait > 8.64e15 || input.deadline !== undefined && Date.now() + wait >= input.deadline)
|
|
307
|
+
if (!Number.isFinite(wait) || Date.now() + wait > 8.64e15 || input.deadline !== undefined && Date.now() + wait >= input.deadline) {
|
|
308
|
+
if (input.atomicIntent)
|
|
309
|
+
input.retainFailure?.(operationId, refusal);
|
|
254
310
|
throw new AppError("capacity_owner_deadline_exceeded", "Continuation wait exceeds its original owner deadline", 1, { primary_error: failure, deadline: input.deadline ?? null });
|
|
311
|
+
}
|
|
255
312
|
if (!existing) {
|
|
256
313
|
const intent = { predecessor: operationId, operation_id: nextId, turn_id: turnId, ordinal, prompt_sha256: promptSha, not_before: new Date(Date.now() + wait).toISOString(), policy: CAPACITY_POLICY, root_operation_id: rootId, session_id: input.sessionId, deadline: input.deadline ?? null, refusal };
|
|
257
314
|
input.retainIntent(intent);
|
|
258
315
|
history.push(intent);
|
|
259
316
|
}
|
|
317
|
+
// Intent/refusal are durable before the first wait; no permit or writer is
|
|
318
|
+
// held here, and live siblings are never cancelled as a settlement proof.
|
|
319
|
+
const waitForOwnedChildren = async (initial) => {
|
|
320
|
+
let current = initial;
|
|
321
|
+
while (current === "owned_children_live") {
|
|
322
|
+
await delay(250, undefined, { signal: input.signal });
|
|
323
|
+
const boundary = await decide();
|
|
324
|
+
if (boundary)
|
|
325
|
+
return boundary;
|
|
326
|
+
current = await readiness();
|
|
327
|
+
}
|
|
328
|
+
if (current !== "ready")
|
|
329
|
+
throw blocked("Owned native child readiness became unproven");
|
|
330
|
+
return null;
|
|
331
|
+
};
|
|
332
|
+
const childBoundary = await waitForOwnedChildren(ready);
|
|
333
|
+
if (childBoundary)
|
|
334
|
+
return childBoundary;
|
|
260
335
|
// Node timers truncate large delays. Bounded slices preserve cancellation
|
|
261
336
|
// and owner checks; monotonic elapsed time does not renew a Retry-After.
|
|
262
|
-
const until = performance.now() +
|
|
337
|
+
const until = performance.now() + Math.max(0, Date.parse(history.find(item => item.operation_id === nextId).not_before) - Date.now());
|
|
263
338
|
while (performance.now() < until) {
|
|
264
339
|
await delay(Math.min(250, Math.max(0, until - performance.now())), undefined, { signal: input.signal });
|
|
265
340
|
const boundary = await decide();
|
|
@@ -269,19 +344,20 @@ export async function continueCodexCapacity(input) {
|
|
|
269
344
|
const afterWait = await decide();
|
|
270
345
|
if (afterWait)
|
|
271
346
|
return afterWait;
|
|
272
|
-
|
|
273
|
-
|
|
347
|
+
const refreshedBoundary = await waitForOwnedChildren(await readiness());
|
|
348
|
+
if (refreshedBoundary)
|
|
349
|
+
return refreshedBoundary;
|
|
274
350
|
const beforeDispatch = await decide();
|
|
275
351
|
if (beforeDispatch)
|
|
276
352
|
return beforeDispatch;
|
|
277
353
|
try {
|
|
278
|
-
return await input.prompt(nextId, text, ordinal);
|
|
354
|
+
return await observe(() => input.prompt(nextId, text, ordinal), nextId);
|
|
279
355
|
}
|
|
280
356
|
catch (nextError) {
|
|
281
357
|
// A task can complete/pause while the daemon queues for its permit.
|
|
282
358
|
// Admission then refuses the stale prompt; accept that owner decision
|
|
283
359
|
// before treating the transport's rejection as another provider failure.
|
|
284
|
-
const finished = await decide();
|
|
360
|
+
const finished = await decide(nextId);
|
|
285
361
|
if (finished)
|
|
286
362
|
return finished;
|
|
287
363
|
error = nextError;
|
|
@@ -5,8 +5,8 @@ import { commandOption, lifecycleCommandFromPayload, parseLifecycleCommand, cont
|
|
|
5
5
|
/** The hook asks the existing daemon about native evidence; it cannot mint it. */
|
|
6
6
|
export async function verifyCodexHookDelivery(env, payload) {
|
|
7
7
|
const command = lifecycleCommandFromPayload(payload);
|
|
8
|
-
const parsed = command ? parseLifecycleCommand(command) : { kind: "none" };
|
|
9
|
-
if (parsed.kind === "none" && !parsed.
|
|
8
|
+
const parsed = command ? parseLifecycleCommand(command) : { kind: "none", participation: "none" };
|
|
9
|
+
if (parsed.kind === "none" && !["lifecycle", "auxiliary"].includes(parsed.participation) && !(command && continuationCommand(command)))
|
|
10
10
|
return undefined;
|
|
11
11
|
const managed = (parsed.kind !== "none" && commandOption(parsed.invocation, "invocation-id")) || env.DD_FLOW_DAEMON_ID;
|
|
12
12
|
if (!managed)
|
|
@@ -11,7 +11,10 @@ export function preflightCliCompatibility(context, args, env = process.env, reso
|
|
|
11
11
|
if (!projectRoot) {
|
|
12
12
|
return { ok: true, classification, compatibility: null };
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
// Compatibility checks the executing build/manifest identity. Routing and
|
|
15
|
+
// binding validate snapshot bytes before dispatch; maintenance must not
|
|
16
|
+
// rehash the dependency closure or spawn health probes on every lease ACK.
|
|
17
|
+
const selection = selectEngine(context, { projectRoot, env }, { allowCurrentInProcess: true, verifyCurrentContent: false }, classification);
|
|
15
18
|
const compatibility = compatibilityReport(selection, classification);
|
|
16
19
|
if (classification.mode === "read_only_diagnostics" || classification.mode === "mb_upgrade") {
|
|
17
20
|
return { ok: true, classification, compatibility };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { AppError } from "../shared/errors.js";
|
|
1
|
+
import { AppError, errorRecord } from "../shared/errors.js";
|
|
2
|
+
import { normalizeNativeChildren } from "../harness-runtime/lib/native-children.mjs";
|
|
2
3
|
import { getVnextFanoutStatus, observeVnextNativeChildren, reconcileVnextFanout, subagentCapacityKey } from "./vnext-fanout.js";
|
|
3
4
|
import { getFlowRunVariables } from "./runs.js";
|
|
4
5
|
import { dispatchVnextPlanReview } from "./vnext-plan-review.js";
|
|
@@ -57,7 +58,7 @@ export async function nextControllerFanout(context, input) {
|
|
|
57
58
|
throw new AppError("stage_graph_compatibility_required", "The retained graph cannot be dispatched by this controller; use the RUN's pinned engine", 1, { stage: input.stage, run_id: input.runId, graph: fanout });
|
|
58
59
|
if (children.length) {
|
|
59
60
|
const reconciled = await reconcileVnextFanout(context, { ...input, observations });
|
|
60
|
-
const issues = reconciled.issues.filter(issue => issue.code
|
|
61
|
+
const issues = reconciled.issues.filter(issue => !["child_check_in_progress", "child_capacity_pending"].includes(issue.code));
|
|
61
62
|
if (issues.length)
|
|
62
63
|
throw new AppError("fanout_reconciliation_required", "Native child outcome requires reconciliation, not replay", 1, { issues });
|
|
63
64
|
if (children.some(child => child.status === "running" || child.status === "unknown"))
|
|
@@ -106,81 +107,13 @@ export async function nextControllerFanout(context, input) {
|
|
|
106
107
|
throw new AppError("fanout_graph_stalled", "Created Work has no ready or running dependency path", 1);
|
|
107
108
|
return prompt("semantic_completion", semanticCompletionPrompt(input.stage, fanout.works.completed));
|
|
108
109
|
}
|
|
109
|
-
/**
|
|
110
|
+
/** Shared pinned runtime normalization; preserve structured errors at the TS boundary. */
|
|
110
111
|
export function controllerNativeChildren(receipt, rootSessionId) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const list = (value) => Array.isArray(value) ? value : [];
|
|
114
|
-
const text = (value, keys) => { for (const key of keys)
|
|
115
|
-
if (typeof value[key] === "string" && value[key])
|
|
116
|
-
return value[key]; return null; };
|
|
117
|
-
const settledTree = new Set(list(receipt.tree_observations)
|
|
118
|
-
.filter(value => object(value).settled === true)
|
|
119
|
-
.map(value => text(object(value), ["provider_session_id", "session_id"]))
|
|
120
|
-
.filter((id) => id !== null));
|
|
121
|
-
const add = (raw, source, scopedParent = null, fallback = "unknown") => {
|
|
122
|
-
const value = object(raw);
|
|
123
|
-
const id = text(value, ["provider_session_id", "session_id", "sessionId", "childSessionId", "conversation_id", "conversationId", "id", "subagentId"]);
|
|
124
|
-
const parent = text(value, ["parent_provider_session_id", "parent_session_id", "parentSessionId", "parentID", "parentId"]) ?? scopedParent;
|
|
125
|
-
if (!id)
|
|
126
|
-
return;
|
|
127
|
-
const outcomeError = object(value.outcome_error);
|
|
128
|
-
if (typeof outcomeError.code === "string")
|
|
129
|
-
throw new AppError(outcomeError.code, String(outcomeError.message ?? "Native child outcome is unconfirmed"), 1, { ...object(outcomeError.details), session_id: id, source });
|
|
130
|
-
const state = String(value.status ?? value.state ?? fallback).toLowerCase();
|
|
131
|
-
if (state === "lost")
|
|
132
|
-
throw new AppError("native_child_outcome_lost", "Native child ended without a recoverable outcome", 1, { session_id: id, parent_session_id: parent, source, native_status: state });
|
|
133
|
-
// Provider idle means no current activity, not a Work result. Only an
|
|
134
|
-
// explicit terminal result or a separately proven root settlement may be
|
|
135
|
-
// terminal here; unknown states stay unknown for reconciliation.
|
|
136
|
-
const status = ["completed", "complete", "done", "success", "succeeded"].includes(state) ? "completed" : state === "settled_by_root" ? "settled_by_root" : ["failed", "error"].includes(state) ? "failed" : ["cancelled", "canceled", "interrupted"].includes(state) ? "cancelled" : ["running", "active", "pending"].includes(state) ? "running" : fallback;
|
|
137
|
-
const rootChild = parent === rootSessionId;
|
|
138
|
-
const settled = rootChild && status === "unknown" && settledTree.has(id) ? "settled_by_root" : status;
|
|
139
|
-
candidates.push({ session_id: id, parent_session_id: parent ?? "", status: settled, source, ...(rootChild ? {} : { provenance: "parent_mismatch" }),
|
|
140
|
-
...(text(value, ["terminal_turn_id"]) ? { terminal_turn_id: text(value, ["terminal_turn_id"]) } : {}),
|
|
141
|
-
...(text(value, ["terminal_observed_at"]) ? { terminal_observed_at: text(value, ["terminal_observed_at"]) } : {}),
|
|
142
|
-
...(value.provider_error !== undefined ? { provider_error: value.provider_error } : {}),
|
|
143
|
-
...(value.native_turn_items && typeof value.native_turn_items === "object" ? { native_turn_items: value.native_turn_items } : {}) });
|
|
144
|
-
};
|
|
145
|
-
for (const value of list(receipt.descendants))
|
|
146
|
-
add(value, "adapter.descendants");
|
|
147
|
-
for (const value of list(receipt.children))
|
|
148
|
-
add(value, "opencode.session.children");
|
|
149
|
-
const subagents = object(object(receipt.evidence).subagents ?? receipt.subagents);
|
|
150
|
-
for (const value of list(subagents.running))
|
|
151
|
-
add(value, "zcode/session/subagents", rootSessionId, "running");
|
|
152
|
-
for (const value of list(subagents.completed))
|
|
153
|
-
add(value, "zcode/session/subagents", rootSessionId, "completed");
|
|
154
|
-
for (const value of list(object(subagents.ended).items)) {
|
|
155
|
-
const status = String(object(value).status ?? "unknown");
|
|
156
|
-
if (!["success", "failed", "cancelled", "lost"].includes(status))
|
|
157
|
-
throw new AppError("native_child_outcome_unavailable", "ZCode ended child has no recognized native outcome", 1, { child: value });
|
|
158
|
-
add(value, "zcode/session/subagents", rootSessionId);
|
|
112
|
+
try {
|
|
113
|
+
return normalizeNativeChildren(receipt, rootSessionId);
|
|
159
114
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const previous = unique.get(child.session_id);
|
|
164
|
-
if (previous && previous.parent_session_id !== child.parent_session_id) {
|
|
165
|
-
// Keep the contradictory parent visible to the existing provenance gate,
|
|
166
|
-
// independently of input order or which source happened to say success.
|
|
167
|
-
const foreign = [previous, child].filter(item => item.parent_session_id !== rootSessionId).sort((a, b) => a.parent_session_id.localeCompare(b.parent_session_id))[0];
|
|
168
|
-
unique.set(child.session_id, { ...foreign, provenance: "parent_mismatch" });
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
if (previous && rank[previous.status] === 3 && rank[child.status] === 3 && previous.status !== child.status)
|
|
172
|
-
throw new AppError("native_child_outcome_conflict", "Native child has contradictory terminal outcomes", 1, { session_id: child.session_id, observations: [previous, child] });
|
|
173
|
-
if (previous && previous.status === child.status && previous.terminal_turn_id && child.terminal_turn_id && previous.terminal_turn_id !== child.terminal_turn_id)
|
|
174
|
-
throw new AppError("native_child_turn_conflict", "Native child has contradictory terminal Turn identities", 1, { session_id: child.session_id, observations: [previous, child] });
|
|
175
|
-
if (previous && previous.status === child.status) {
|
|
176
|
-
unique.set(child.session_id, { ...previous, ...child,
|
|
177
|
-
...(child.terminal_turn_id ?? previous.terminal_turn_id ? { terminal_turn_id: child.terminal_turn_id ?? previous.terminal_turn_id } : {}),
|
|
178
|
-
...(child.provider_error ?? previous.provider_error ? { provider_error: child.provider_error ?? previous.provider_error } : {}),
|
|
179
|
-
...(child.native_turn_items ?? previous.native_turn_items ? { native_turn_items: child.native_turn_items ?? previous.native_turn_items } : {}) });
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
if (!previous || rank[child.status] > rank[previous.status])
|
|
183
|
-
unique.set(child.session_id, child);
|
|
115
|
+
catch (error) {
|
|
116
|
+
const record = errorRecord(error);
|
|
117
|
+
throw Object.assign(new AppError(record.code, record.message, record.exitCode, record.details), record);
|
|
184
118
|
}
|
|
185
|
-
return [...unique.values()].sort((left, right) => left.session_id.localeCompare(right.session_id));
|
|
186
119
|
}
|
package/dist/services/engines.js
CHANGED
|
@@ -82,7 +82,7 @@ export function installCurrentEngine(context, input = {}) {
|
|
|
82
82
|
packageVersion: build.version,
|
|
83
83
|
integrityChecksum: stagedChecksum
|
|
84
84
|
}) : null;
|
|
85
|
-
if (duplicate) {
|
|
85
|
+
if (duplicate && manifestDiagnostics(duplicate, { checkEntrypoint: false }).length === 0) {
|
|
86
86
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
87
87
|
return {
|
|
88
88
|
ok: true,
|
|
@@ -141,7 +141,7 @@ export function doctorEngines(context, input = {}) {
|
|
|
141
141
|
const installed = readInstalledManifests(root);
|
|
142
142
|
const checks = installed.map(manifest => {
|
|
143
143
|
const diagnostics = manifestDiagnostics(manifest);
|
|
144
|
-
return { engine: manifestSummary(manifest, diagnostics), status: diagnostics.length ? "corrupt" : "ok", diagnostics };
|
|
144
|
+
return { engine: manifestSummary(manifest, diagnostics), status: diagnostics.includes("entrypoint_probe_unavailable") ? "unverified" : diagnostics.length ? "corrupt" : "ok", diagnostics };
|
|
145
145
|
});
|
|
146
146
|
const classification = input.classification ?? classifyCliOperation(["engine", "doctor"], input.env ?? context.env);
|
|
147
147
|
const selection = selectEngine(context, input, {}, classification);
|
|
@@ -223,6 +223,8 @@ export function bindRunEngine(context, input, prepared = prepareRunEngineBind(co
|
|
|
223
223
|
encoding: "utf8",
|
|
224
224
|
timeout: 30_000
|
|
225
225
|
});
|
|
226
|
+
if (probe.error || probe.signal)
|
|
227
|
+
throw new AppError("run_engine_probe_unavailable", "Selected engine observation could not complete; compatibility is unknown", 1, { run_id: located.run_id, cause: probe.error?.message, signal: probe.signal });
|
|
226
228
|
if (probe.status !== 0) {
|
|
227
229
|
throw new AppError("run_engine_probe_failed", "Selected engine cannot read the RUN authority", 1, {
|
|
228
230
|
run_id: located.run_id,
|
|
@@ -291,6 +293,8 @@ export function upgradeForkedRunEngine(context, input) {
|
|
|
291
293
|
env: { ...process.env, ...context.env, DD_FLOW_HOME: context.ddFlowHome, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: manifest.snapshot_root },
|
|
292
294
|
encoding: "utf8", timeout: 30_000
|
|
293
295
|
});
|
|
296
|
+
if (probe.error || probe.signal)
|
|
297
|
+
throw new AppError("fork_engine_probe_unavailable", "Selected engine observation could not complete; compatibility is unknown", 1, { run_id: located.run_id, cause: probe.error?.message, signal: probe.signal });
|
|
294
298
|
if (probe.status !== 0)
|
|
295
299
|
throw new AppError("fork_engine_probe_failed", "Selected engine cannot read the forked RUN", 1, { run_id: located.run_id, engine: manifestSummary(manifest), stderr: probe.stderr.slice(0, 2000) });
|
|
296
300
|
const binding = { schema_id: runEngineBindingSchemaId, run_id: located.run_id, project_root: projectRoot, bound_at: context.now(), source: "fork_upgrade", reason: requiredText(input.reason, "reason"), engine: requested, probe: { status: "passed", command: `dd-flow ${probeArgs.join(" ")}` } };
|
|
@@ -429,7 +433,11 @@ export function selectEngine(context, input, options = {}, classification = inpu
|
|
|
429
433
|
const requiredRange = stringValue(compatibility?.engine?.version_range) ?? (classification.mode === "mb_upgrade" ? null : `>=${build.version} <9999.0.0`);
|
|
430
434
|
const recommended = stringValue(compatibility?.engine?.recommended_version) ?? build.version;
|
|
431
435
|
const installHintTemplate = stringValue(compatibility?.engine?.old_engine_install_hint) ?? `npx ${packageName}@<version> engine install`;
|
|
432
|
-
|
|
436
|
+
// An engine-mode caller already executes this module's build. Another
|
|
437
|
+
// installed engine cannot establish compatibility for its current writes.
|
|
438
|
+
// Keep full snapshot validation in routing/binding, not lease maintenance.
|
|
439
|
+
const executingCurrent = options.allowCurrentInProcess && isEngineMode(input.env ?? process.env);
|
|
440
|
+
const manifests = executingCurrent ? [] : readInstalledManifests(engineStoreRoot(context.ddFlowHome))
|
|
433
441
|
.filter((manifest) => manifest.package_name === packageName)
|
|
434
442
|
.filter((manifest) => requiredRange !== null && satisfiesRange(manifest.package_version, requiredRange))
|
|
435
443
|
.filter((manifest) => manifestHealthy(manifest))
|
|
@@ -447,7 +455,7 @@ export function selectEngine(context, input, options = {}, classification = inpu
|
|
|
447
455
|
requiredRange !== null &&
|
|
448
456
|
packageName === build.package_name &&
|
|
449
457
|
satisfiesRange(build.version, requiredRange)
|
|
450
|
-
? currentBuildManifest(context, build)
|
|
458
|
+
? currentBuildManifest(context, build, options.verifyCurrentContent !== false)
|
|
451
459
|
: null);
|
|
452
460
|
const diagnostics = [
|
|
453
461
|
...targetBlockers,
|
|
@@ -465,9 +473,38 @@ export function selectEngine(context, input, options = {}, classification = inpu
|
|
|
465
473
|
diagnostics
|
|
466
474
|
};
|
|
467
475
|
}
|
|
468
|
-
function currentBuildManifest(context, build) {
|
|
476
|
+
function currentBuildManifest(context, build, verifyContent) {
|
|
469
477
|
const packageRoot = findPackageRoot(path.dirname(sourceFile)) ?? process.cwd();
|
|
470
|
-
|
|
478
|
+
const manifestFile = path.join(packageRoot, "engine.json");
|
|
479
|
+
const installedLayout = path.basename(path.dirname(path.dirname(packageRoot))) === "engines"
|
|
480
|
+
&& path.basename(path.dirname(packageRoot)) === path.basename(path.dirname(engineVersionRoot(context.ddFlowHome, build.package_name, build.version)));
|
|
481
|
+
if (fs.existsSync(manifestFile) || installedLayout) {
|
|
482
|
+
const installed = readManifest(manifestFile);
|
|
483
|
+
// Use the executing module's physical root, never an environment alias.
|
|
484
|
+
// A damaged installed manifest must not become a development fallback.
|
|
485
|
+
if (!installed || installed.package_name !== build.package_name || installed.package_version !== build.version
|
|
486
|
+
|| installed.engine_version !== build.version || installed.engine_id !== "dd-flow-engine"
|
|
487
|
+
|| typeof installed.snapshot_root !== "string" || !fs.existsSync(installed.snapshot_root)
|
|
488
|
+
|| fs.realpathSync(installed.snapshot_root) !== fs.realpathSync(packageRoot)
|
|
489
|
+
|| typeof installed.entrypoint !== "string" || installed.integrity?.source !== "package_snapshot"
|
|
490
|
+
|| typeof installed.integrity?.checksum !== "string" || !/^[a-f0-9]{64}$/.test(installed.integrity.checksum)
|
|
491
|
+
|| (installed.schema_registry && (!Array.isArray(installed.schema_registry.entries)
|
|
492
|
+
|| installed.schema_registry.entries.some(entry => !entry || typeof entry.path !== "string"
|
|
493
|
+
|| typeof entry.id !== "string" || typeof entry.name !== "string" || !/^[a-f0-9]{64}$/.test(entry.checksum ?? ""))))) {
|
|
494
|
+
throw new AppError("unhealthy_engine", "Executing engine snapshot manifest does not match its build", 1, { manifest: manifestFile });
|
|
495
|
+
}
|
|
496
|
+
const diagnostics = manifestDiagnostics(installed, { checkEntrypoint: false, verifyContent });
|
|
497
|
+
if (diagnostics.length)
|
|
498
|
+
throw new AppError("unhealthy_engine", "Executing engine snapshot is unhealthy", 1, { manifest: manifestFile, diagnostics });
|
|
499
|
+
return installed;
|
|
500
|
+
}
|
|
501
|
+
// Only an unmaterialized package has no snapshot proof. Canon and
|
|
502
|
+
// package-manager aliases are expected in development checkouts.
|
|
503
|
+
const manifest = buildManifest(context, build.package_name, build.version, packageRoot, packageRoot, packageRoot, null);
|
|
504
|
+
// An ephemeral routing choice has no snapshot proof. Hash bytes only when
|
|
505
|
+
// materializing/validating an installed snapshot, never on every CLI call.
|
|
506
|
+
manifest.integrity.mode = "degraded_local_development";
|
|
507
|
+
return manifest;
|
|
471
508
|
}
|
|
472
509
|
export function compatibilityContexts(context, input = {}) {
|
|
473
510
|
const env = input.env ?? process.env;
|
|
@@ -614,7 +651,7 @@ function option(parsed, key) {
|
|
|
614
651
|
function positional(parsed, index) {
|
|
615
652
|
return parsed.positional[index] ?? null;
|
|
616
653
|
}
|
|
617
|
-
function buildManifest(context, packageName, version, packageRoot, snapshotRoot, checksumRoot) {
|
|
654
|
+
function buildManifest(context, packageName, version, packageRoot, snapshotRoot, checksumRoot, checksum = engineSnapshotChecksum(checksumRoot)) {
|
|
618
655
|
return {
|
|
619
656
|
schema_id: engineManifestSchemaId,
|
|
620
657
|
package_name: packageName,
|
|
@@ -635,7 +672,7 @@ function buildManifest(context, packageName, version, packageRoot, snapshotRoot,
|
|
|
635
672
|
schema_registry: buildSchemaRegistry(checksumRoot),
|
|
636
673
|
integrity: {
|
|
637
674
|
source: "package_snapshot",
|
|
638
|
-
checksum
|
|
675
|
+
checksum,
|
|
639
676
|
mode: "full_content"
|
|
640
677
|
}
|
|
641
678
|
};
|
|
@@ -703,19 +740,23 @@ export function prepareForkEngine(engineVersion, integrityChecksum) {
|
|
|
703
740
|
throw new AppError("harness_runtime_unbuilt", "Build the CLI before preparing a fork", 1);
|
|
704
741
|
const files = packageSnapshotFiles(packageRoot);
|
|
705
742
|
if (integrityChecksum !== undefined) {
|
|
706
|
-
const checksum =
|
|
707
|
-
|
|
708
|
-
if (path.basename(relative) === "engine.json")
|
|
709
|
-
continue;
|
|
710
|
-
checksum.update(relative);
|
|
711
|
-
checksum.update("\0");
|
|
712
|
-
checksum.update(fs.readFileSync(source));
|
|
713
|
-
checksum.update("\0");
|
|
714
|
-
}
|
|
715
|
-
if (checksum.digest("hex") !== integrityChecksum)
|
|
743
|
+
const checksum = snapshotFilesChecksum(files);
|
|
744
|
+
if (checksum !== integrityChecksum)
|
|
716
745
|
throw new AppError("fork_engine_unavailable", "Requested engine checksum differs from the invoking CLI artifact", 2, { requested: integrityChecksum });
|
|
717
746
|
}
|
|
718
747
|
}
|
|
748
|
+
function snapshotFilesChecksum(files) {
|
|
749
|
+
const checksum = crypto.createHash("sha256");
|
|
750
|
+
for (const [relative, source] of [...files].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
751
|
+
if (path.basename(relative) === "engine.json")
|
|
752
|
+
continue;
|
|
753
|
+
checksum.update(relative);
|
|
754
|
+
checksum.update("\0");
|
|
755
|
+
checksum.update(fs.readFileSync(source));
|
|
756
|
+
checksum.update("\0");
|
|
757
|
+
}
|
|
758
|
+
return checksum.digest("hex");
|
|
759
|
+
}
|
|
719
760
|
function readProductionDependencies(packageRoot) {
|
|
720
761
|
try {
|
|
721
762
|
const manifest = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
@@ -795,6 +836,9 @@ function readInstalledManifests(root) {
|
|
|
795
836
|
if (!fs.statSync(packageRoot).isDirectory())
|
|
796
837
|
continue;
|
|
797
838
|
for (const versionDir of fs.readdirSync(packageRoot)) {
|
|
839
|
+
// Failed staging, activation backups and locks are not installations.
|
|
840
|
+
if (versionDir.startsWith(".") || /\.previous-\d+-\d+$/.test(versionDir) || versionDir.endsWith(".lock"))
|
|
841
|
+
continue;
|
|
798
842
|
const manifest = readManifest(path.join(packageRoot, versionDir, "engine.json"));
|
|
799
843
|
if (manifest)
|
|
800
844
|
manifests.push(manifest);
|
|
@@ -847,7 +891,7 @@ function manifestDiagnostics(manifest, options = {}) {
|
|
|
847
891
|
if (!manifest.integrity || manifest.integrity.mode !== "full_content" || !manifest.integrity.checksum) {
|
|
848
892
|
diagnostics.push("content_checksum_missing");
|
|
849
893
|
}
|
|
850
|
-
else if (fs.existsSync(snapshotRoot)) {
|
|
894
|
+
else if (options.verifyContent !== false && fs.existsSync(snapshotRoot)) {
|
|
851
895
|
try {
|
|
852
896
|
if (engineSnapshotChecksum(snapshotRoot) !== manifest.integrity.checksum)
|
|
853
897
|
diagnostics.push("content_checksum_mismatch");
|
|
@@ -861,6 +905,8 @@ function manifestDiagnostics(manifest, options = {}) {
|
|
|
861
905
|
const schemaPath = safeResolveSnapshotPath(manifest, entry.path);
|
|
862
906
|
if (!schemaPath || !fs.existsSync(schemaPath))
|
|
863
907
|
diagnostics.push(`schema_registry_missing:${entry.name}`);
|
|
908
|
+
else if (options.verifyContent === false)
|
|
909
|
+
continue;
|
|
864
910
|
else if (fileChecksum(schemaPath) !== entry.checksum)
|
|
865
911
|
diagnostics.push(`schema_registry_checksum_mismatch:${entry.name}`);
|
|
866
912
|
else {
|
|
@@ -887,7 +933,9 @@ function manifestDiagnostics(manifest, options = {}) {
|
|
|
887
933
|
stdio: ["ignore", "pipe", "pipe"],
|
|
888
934
|
timeout: 15_000
|
|
889
935
|
});
|
|
890
|
-
if (health.
|
|
936
|
+
if (health.error || health.signal)
|
|
937
|
+
diagnostics.push("entrypoint_probe_unavailable");
|
|
938
|
+
else if (health.status !== 0)
|
|
891
939
|
diagnostics.push("entrypoint_dependency_load_failed");
|
|
892
940
|
}
|
|
893
941
|
return diagnostics;
|
|
@@ -1072,6 +1120,8 @@ function collectFiles(root, current, files) {
|
|
|
1072
1120
|
if (entry.name === "engine.json")
|
|
1073
1121
|
continue;
|
|
1074
1122
|
const full = path.join(current, entry.name);
|
|
1123
|
+
if (entry.isSymbolicLink())
|
|
1124
|
+
throw new AppError("engine_integrity_symlink", "Pinned engine snapshot contains an unhashed symbolic link", 1, { path: path.relative(root, full) });
|
|
1075
1125
|
if (entry.isDirectory())
|
|
1076
1126
|
collectFiles(root, full, files);
|
|
1077
1127
|
else if (entry.isFile())
|