intentdna 1.9.0 → 1.9.3
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +227 -87
- package/dist/cli/commands/run-lifecycle.d.ts +61 -11
- package/dist/cli/commands/run-lifecycle.js +184 -50
- package/dist/cli/commands/run-observer.d.ts +15 -0
- package/dist/cli/commands/run-observer.js +210 -0
- package/dist/cli/commands/run.d.ts +16 -20
- package/dist/cli/commands/run.js +461 -659
- package/dist/cli/commands/sync.js +16 -12
- package/dist/cli/index.js +14 -5
- package/dist/compiler/compile.d.ts +1 -0
- package/dist/compiler/compile.js +1 -1
- package/dist/compiler/controller.d.ts +16 -0
- package/dist/compiler/controller.js +693 -0
- package/dist/compiler/input-resolver.d.ts +2 -0
- package/dist/compiler/input-resolver.js +22 -6
- package/dist/hooks/cli.d.ts +45 -0
- package/dist/hooks/cli.js +263 -31
- package/dist/hooks/host-event.d.ts +57 -0
- package/dist/hooks/host-event.js +187 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/protocol.d.ts +7 -0
- package/dist/hooks/schema.d.ts +1 -0
- package/dist/hooks/schema.js +89 -1
- package/dist/hooks/state.d.ts +23 -1
- package/dist/hooks/state.js +9 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/tools-run.d.ts +4 -0
- package/dist/mcp/tools-run.js +419 -0
- package/dist/mcp/tools-state.d.ts +1 -1
- package/dist/mcp/tools-state.js +13 -241
- package/dist/runtime/artifact-store.d.ts +146 -0
- package/dist/runtime/artifact-store.js +1436 -0
- package/dist/runtime/canonical-attempt-outcome.d.ts +12 -0
- package/dist/runtime/canonical-attempt-outcome.js +195 -0
- package/dist/runtime/canonical-json.d.ts +10 -0
- package/dist/runtime/canonical-json.js +105 -0
- package/dist/runtime/canonical-run-application.d.ts +93 -0
- package/dist/runtime/canonical-run-application.js +229 -0
- package/dist/runtime/canonical-run-service.d.ts +454 -0
- package/dist/runtime/canonical-run-service.js +4760 -0
- package/dist/runtime/canonical-runtime-composition.d.ts +22 -0
- package/dist/runtime/canonical-runtime-composition.js +34 -0
- package/dist/runtime/canonical-runtime-projection.d.ts +3 -0
- package/dist/runtime/canonical-runtime-projection.js +4 -0
- package/dist/runtime/canonical-target-compiler.d.ts +46 -0
- package/dist/runtime/canonical-target-compiler.js +514 -0
- package/dist/runtime/claude-agent-identity.d.ts +2 -0
- package/dist/runtime/claude-agent-identity.js +21 -0
- package/dist/runtime/executable-run-plan.d.ts +328 -0
- package/dist/runtime/executable-run-plan.js +1485 -0
- package/dist/runtime/execution-authority.d.ts +83 -0
- package/dist/runtime/execution-authority.js +98 -0
- package/dist/runtime/handoff-resolver.js +4 -1
- package/dist/runtime/harness-pull-adapter.d.ts +146 -0
- package/dist/runtime/harness-pull-adapter.js +315 -0
- package/dist/runtime/index.d.ts +39 -14
- package/dist/runtime/index.js +29 -7
- package/dist/runtime/local-execution-authority.d.ts +194 -0
- package/dist/runtime/local-execution-authority.js +2897 -0
- package/dist/runtime/local-execution-reconciliation.d.ts +20 -0
- package/dist/runtime/local-execution-reconciliation.js +107 -0
- package/dist/runtime/local-execution-supervisor-script.d.ts +7 -0
- package/dist/runtime/local-execution-supervisor-script.js +793 -0
- package/dist/runtime/local-provider-sandbox.d.ts +42 -0
- package/dist/runtime/local-provider-sandbox.js +154 -0
- package/dist/runtime/local-verifier-execution.d.ts +24 -0
- package/dist/runtime/local-verifier-execution.js +290 -0
- package/dist/runtime/local-windows-execution-supervisor-script.d.ts +6 -0
- package/dist/runtime/local-windows-execution-supervisor-script.js +788 -0
- package/dist/runtime/plan-store.d.ts +30 -0
- package/dist/runtime/plan-store.js +231 -0
- package/dist/runtime/process-tree.d.ts +9 -0
- package/dist/runtime/process-tree.js +97 -24
- package/dist/runtime/providers/codex.js +2 -2
- package/dist/runtime/push-driver.d.ts +184 -0
- package/dist/runtime/push-driver.js +1002 -0
- package/dist/runtime/result-store.d.ts +34 -3
- package/dist/runtime/result-store.js +1073 -2
- package/dist/runtime/run-binding.d.ts +68 -0
- package/dist/runtime/run-binding.js +278 -0
- package/dist/runtime/run-contracts.d.ts +575 -0
- package/dist/runtime/run-contracts.js +58 -7
- package/dist/runtime/run-controller.d.ts +1 -6
- package/dist/runtime/run-controller.js +0 -41
- package/dist/runtime/run-store.d.ts +85 -7
- package/dist/runtime/run-store.js +2528 -186
- package/dist/runtime/skill-adapter.d.ts +18 -7
- package/dist/runtime/skill-adapter.js +100 -742
- package/dist/runtime/structured-output-validator.d.ts +44 -0
- package/dist/runtime/structured-output-validator.js +171 -0
- package/dist/runtime/verifier-command-binding.d.ts +22 -0
- package/dist/runtime/verifier-command-binding.js +162 -0
- package/dist/runtime/verifier.d.ts +46 -6
- package/dist/runtime/verifier.js +355 -53
- package/dist/runtime/windows-job-keeper.d.ts +47 -0
- package/dist/runtime/windows-job-keeper.js +231 -0
- package/dist/runtime/worker-executor.d.ts +17 -1
- package/dist/runtime/worker-executor.js +26 -5
- package/dist/runtime/workflow-plan-adapter.d.ts +19 -0
- package/dist/runtime/workflow-plan-adapter.js +502 -6
- package/dist/runtime/workflow-runtime-manifest.d.ts +1 -0
- package/dist/runtime/workflow-runtime-manifest.js +5 -0
- package/dist/runtime/workspace-isolation.d.ts +29 -2
- package/dist/runtime/workspace-isolation.js +488 -11
- package/dist/runtime/workspace-observation.d.ts +44 -0
- package/dist/runtime/workspace-observation.js +216 -0
- package/dist/schema/controller-registry.d.ts +0 -7
- package/dist/schema/controller-registry.js +0 -8
- package/dist/schema/types.d.ts +84 -10
- package/dist/schema/types.js +2 -0
- package/dist/schema/validate.js +151 -1
- package/dist/schema/validators/controllers.d.ts +1 -1
- package/dist/schema/validators/controllers.js +126 -41
- package/dist/schema/workflow-authoring-contract.js +2 -0
- package/dist/schema/yaml-parser.js +1 -1
- package/dist/templates/flutter-rewrite-new.dna.yaml +1 -0
- package/dist/templates/flutter-rewrite.dna.yaml +87 -60
- package/dist/templates/safe-refactoring.dna.yaml +1 -0
- package/native/windows-job-keeper/README.md +80 -0
- package/native/windows-job-keeper/bin/aarch64/intentdna-windows-job-keeper.exe +0 -0
- package/native/windows-job-keeper/bin/x86_64/intentdna-windows-job-keeper.exe +0 -0
- package/native/windows-job-keeper/keeper.c +979 -0
- package/native/windows-job-keeper/manifest.json +33 -0
- package/package.json +6 -2
- package/scripts/build-windows-job-keeper.mjs +172 -0
- package/scripts/verify-windows-job-keeper.mjs +184 -0
|
@@ -0,0 +1,1002 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { canonicalizeJson } from "./canonical-json.js";
|
|
3
|
+
import { parseStoredExecutableRunPlan } from "./executable-run-plan.js";
|
|
4
|
+
import { CALLER_ATTESTATION_SCHEMA_VERSION } from "./run-contracts.js";
|
|
5
|
+
import { parseStoredRunBinding, } from "./run-binding.js";
|
|
6
|
+
import { executeWorkerAttempt, } from "./worker-executor.js";
|
|
7
|
+
export class PushDriverError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(code, message, options) {
|
|
10
|
+
super(message, options);
|
|
11
|
+
this.name = "PushDriverError";
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class PushDriverIndeterminateAttachError extends PushDriverError {
|
|
16
|
+
disposition = "retain_preparation";
|
|
17
|
+
constructor(attemptId, cause) {
|
|
18
|
+
super("attach_indeterminate", `attempt ${attemptId} attach disposition is indeterminate; execution authority preparation was retained`, { cause });
|
|
19
|
+
this.name = "PushDriverIndeterminateAttachError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const DEFAULT_HEARTBEAT_INTERVAL_MS = 5_000;
|
|
23
|
+
const DEFAULT_WAIT_POLL_MS = 50;
|
|
24
|
+
const NO_ATTEMPT_ERROR = Symbol("no_attempt_error");
|
|
25
|
+
const DEFAULT_CAPABILITIES = {
|
|
26
|
+
worktree_isolation: true,
|
|
27
|
+
parallelism: true,
|
|
28
|
+
cancellation: false,
|
|
29
|
+
hook_identity: false,
|
|
30
|
+
caller_attestation: false,
|
|
31
|
+
human_interaction: false,
|
|
32
|
+
direct_structured_submission: true,
|
|
33
|
+
network_policy_enforcement: false,
|
|
34
|
+
};
|
|
35
|
+
const RECOVERABLE_WAIT_REASONS = new Set([
|
|
36
|
+
"attempt_active",
|
|
37
|
+
"dependencies_pending",
|
|
38
|
+
"retry_backoff",
|
|
39
|
+
"run_cancelling",
|
|
40
|
+
]);
|
|
41
|
+
function defaultSleep(milliseconds) {
|
|
42
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
43
|
+
}
|
|
44
|
+
function requirePositiveInteger(name, value) {
|
|
45
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
46
|
+
throw new PushDriverError("invalid_configuration", `${name} must be a positive safe integer`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function requireNonEmpty(name, value) {
|
|
50
|
+
if (value.length === 0) {
|
|
51
|
+
throw new PushDriverError("invalid_service_response", `${name} must not be empty`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function assertRequiredCapabilities(required, provided) {
|
|
55
|
+
for (const [name, needed] of Object.entries(required)) {
|
|
56
|
+
if (needed && provided[name] !== true) {
|
|
57
|
+
throw new PushDriverError("unsupported", `driver capability ${name} is required`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function defaultWaitStrategy(waitPollMs) {
|
|
62
|
+
return ({ reason_code }) => RECOVERABLE_WAIT_REASONS.has(reason_code)
|
|
63
|
+
? { action: "continue", delay_ms: waitPollMs }
|
|
64
|
+
: { action: "stop", reason: `unsupported wait reason: ${reason_code}` };
|
|
65
|
+
}
|
|
66
|
+
function validateRunPayloads(view) {
|
|
67
|
+
try {
|
|
68
|
+
const plan = parseStoredExecutableRunPlan(view.plan);
|
|
69
|
+
const binding = parseStoredRunBinding(view.run.run_binding);
|
|
70
|
+
if (view.run.plan_id !== plan.plan_id
|
|
71
|
+
|| view.run.plan_digest !== plan.plan_digest
|
|
72
|
+
|| binding.binding.plan_id !== plan.plan_id) {
|
|
73
|
+
throw new Error("run ledger, plan, and binding cross-references disagree");
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
plan_snapshot: canonicalizeJson(plan),
|
|
77
|
+
binding_snapshot: canonicalizeJson(binding),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
throw new PushDriverError("invalid_service_response", "service returned an invalid canonical plan or run binding", { cause: error });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function pinRun(view) {
|
|
85
|
+
const snapshots = validateRunPayloads(view);
|
|
86
|
+
return {
|
|
87
|
+
run_id: view.run.run_id,
|
|
88
|
+
plan_id: view.plan.plan_id,
|
|
89
|
+
plan_digest: view.plan.plan_digest,
|
|
90
|
+
binding_id: view.run.run_binding.binding_id,
|
|
91
|
+
max_concurrency: view.run.run_binding.binding.max_concurrency ?? 1,
|
|
92
|
+
...snapshots,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function assertPinnedRun(pinned, view) {
|
|
96
|
+
const snapshots = validateRunPayloads(view);
|
|
97
|
+
if (view.run.run_id !== pinned.run_id
|
|
98
|
+
|| view.plan.plan_id !== pinned.plan_id
|
|
99
|
+
|| view.plan.plan_digest !== pinned.plan_digest
|
|
100
|
+
|| view.run.plan_id !== pinned.plan_id
|
|
101
|
+
|| view.run.plan_digest !== pinned.plan_digest
|
|
102
|
+
|| view.run.run_binding.binding_id !== pinned.binding_id
|
|
103
|
+
|| view.run.run_binding.binding.plan_id !== pinned.plan_id
|
|
104
|
+
|| snapshots.plan_snapshot !== pinned.plan_snapshot
|
|
105
|
+
|| snapshots.binding_snapshot !== pinned.binding_snapshot) {
|
|
106
|
+
throw new PushDriverError("invalid_service_response", "service response changed the pinned run, plan, or binding identity");
|
|
107
|
+
}
|
|
108
|
+
return view;
|
|
109
|
+
}
|
|
110
|
+
function requireWorkerNode(view, stepId) {
|
|
111
|
+
const matches = view.plan.plan.nodes.filter((node) => node.node_id === stepId);
|
|
112
|
+
if (matches.length !== 1 || matches[0]?.kind !== "worker") {
|
|
113
|
+
throw new PushDriverError("invalid_service_response", `acquired step ${stepId} is not exactly one pinned worker node`);
|
|
114
|
+
}
|
|
115
|
+
return matches[0];
|
|
116
|
+
}
|
|
117
|
+
function assertAttemptView(lease, workerSessionId, attached) {
|
|
118
|
+
const packet = attached.packet;
|
|
119
|
+
if (attached.lease.run_id !== lease.run_id
|
|
120
|
+
|| attached.lease.step_id !== lease.step_id
|
|
121
|
+
|| attached.lease.activation_id !== lease.activation_id
|
|
122
|
+
|| attached.lease.attempt_id !== lease.attempt_id
|
|
123
|
+
|| attached.lease.attempt_number !== lease.attempt_number
|
|
124
|
+
|| attached.lease.claim_epoch !== lease.claim_epoch
|
|
125
|
+
|| attached.lease.owner_id !== lease.owner_id
|
|
126
|
+
|| attached.attempt.attempt_id !== lease.attempt_id
|
|
127
|
+
|| attached.attempt.activation_id !== lease.activation_id
|
|
128
|
+
|| attached.attempt.step_id !== lease.step_id
|
|
129
|
+
|| attached.attempt.phase !== "running"
|
|
130
|
+
|| attached.attempt.executor?.kind !== "worker"
|
|
131
|
+
|| attached.attempt.executor.worker_session_id !== workerSessionId
|
|
132
|
+
|| attached.attempt.started_at === null
|
|
133
|
+
|| packet === null
|
|
134
|
+
|| packet.run_id !== lease.run_id
|
|
135
|
+
|| packet.step_id !== lease.step_id
|
|
136
|
+
|| packet.attempt_id !== lease.attempt_id
|
|
137
|
+
|| packet.attempt_number !== lease.attempt_number
|
|
138
|
+
|| packet.worker_session_id !== workerSessionId) {
|
|
139
|
+
throw new PushDriverError("invalid_service_response", `attach response does not match attempt ${lease.attempt_id}`);
|
|
140
|
+
}
|
|
141
|
+
return packet;
|
|
142
|
+
}
|
|
143
|
+
function outputContractByName(node) {
|
|
144
|
+
return new Map(node.packet_template.output_contract.map((contract) => [
|
|
145
|
+
contract.name,
|
|
146
|
+
contract,
|
|
147
|
+
]));
|
|
148
|
+
}
|
|
149
|
+
function referenceArtifactKind(contract) {
|
|
150
|
+
if (contract.artifact_kind !== "file"
|
|
151
|
+
&& contract.artifact_kind !== "directory"
|
|
152
|
+
&& contract.artifact_kind !== "git_commit") {
|
|
153
|
+
throw new PushDriverError("unsupported_output_contract", `reference output ${contract.name} uses unsupported artifact kind ${contract.artifact_kind}`);
|
|
154
|
+
}
|
|
155
|
+
return contract.artifact_kind;
|
|
156
|
+
}
|
|
157
|
+
function mapProviderOutput(output, contract) {
|
|
158
|
+
if (output.kind !== contract.kind) {
|
|
159
|
+
throw new PushDriverError("invalid_provider_output", `provider output ${output.name} kind does not match its pinned contract`);
|
|
160
|
+
}
|
|
161
|
+
if (output.kind === "text") {
|
|
162
|
+
return { name: contract.name, kind: "text", text: output.text };
|
|
163
|
+
}
|
|
164
|
+
if (output.kind === "structured") {
|
|
165
|
+
if (output.schema_ref !== contract.schema_ref) {
|
|
166
|
+
throw new PushDriverError("invalid_provider_output", `provider output ${output.name} schema does not match its pinned contract`);
|
|
167
|
+
}
|
|
168
|
+
canonicalizeJson(output.value);
|
|
169
|
+
return {
|
|
170
|
+
name: contract.name,
|
|
171
|
+
kind: "structured",
|
|
172
|
+
value: output.value,
|
|
173
|
+
schema_ref: contract.schema_ref,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const artifactKind = referenceArtifactKind(contract);
|
|
177
|
+
if (artifactKind === "git_commit") {
|
|
178
|
+
if (output.reference.kind !== "artifact") {
|
|
179
|
+
throw new PushDriverError("invalid_provider_output", `git_commit output ${output.name} must use an artifact reference`);
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
name: contract.name,
|
|
183
|
+
kind: "reference",
|
|
184
|
+
artifact_kind: artifactKind,
|
|
185
|
+
source_path: output.reference.value,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (output.reference.kind !== artifactKind) {
|
|
189
|
+
throw new PushDriverError("invalid_provider_output", `provider output ${output.name} reference kind does not match ${artifactKind}`);
|
|
190
|
+
}
|
|
191
|
+
if (contract.relative_path_template === null) {
|
|
192
|
+
throw new PushDriverError("unsupported_output_contract", `reference output ${contract.name} has no pinned source path`);
|
|
193
|
+
}
|
|
194
|
+
if (output.reference.value !== contract.relative_path_template) {
|
|
195
|
+
throw new PushDriverError("invalid_provider_output", `provider output ${output.name} path does not match its pinned contract`);
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
name: contract.name,
|
|
199
|
+
kind: "reference",
|
|
200
|
+
artifact_kind: artifactKind,
|
|
201
|
+
source_path: contract.relative_path_template,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
export function mapProviderOutputsToSubmission(node, outputs, requireRequiredOutputs) {
|
|
205
|
+
const contracts = outputContractByName(node);
|
|
206
|
+
const seen = new Set();
|
|
207
|
+
const mapped = [];
|
|
208
|
+
for (const output of outputs) {
|
|
209
|
+
if (seen.has(output.name)) {
|
|
210
|
+
throw new PushDriverError("invalid_provider_output", `provider returned duplicate output ${output.name}`);
|
|
211
|
+
}
|
|
212
|
+
seen.add(output.name);
|
|
213
|
+
const contract = contracts.get(output.name);
|
|
214
|
+
if (!contract) {
|
|
215
|
+
throw new PushDriverError("invalid_provider_output", `provider returned undeclared output ${output.name}`);
|
|
216
|
+
}
|
|
217
|
+
mapped.push(mapProviderOutput(output, contract));
|
|
218
|
+
}
|
|
219
|
+
if (requireRequiredOutputs) {
|
|
220
|
+
const missing = [...contracts.values()].find((contract) => (contract.required && !seen.has(contract.name)));
|
|
221
|
+
if (missing) {
|
|
222
|
+
throw new PushDriverError("invalid_provider_output", `provider omitted required output ${missing.name}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return mapped;
|
|
226
|
+
}
|
|
227
|
+
export function terminalOutcomeToCanonicalAttemptOutcome(process) {
|
|
228
|
+
switch (process.kind) {
|
|
229
|
+
case "success":
|
|
230
|
+
return {
|
|
231
|
+
kind: "worker_process",
|
|
232
|
+
status: "succeeded",
|
|
233
|
+
reason_code: null,
|
|
234
|
+
message: null,
|
|
235
|
+
process,
|
|
236
|
+
};
|
|
237
|
+
case "cancelled":
|
|
238
|
+
return {
|
|
239
|
+
kind: "worker_process",
|
|
240
|
+
status: "cancelled",
|
|
241
|
+
reason_code: "cancelled",
|
|
242
|
+
message: process.reason,
|
|
243
|
+
process,
|
|
244
|
+
};
|
|
245
|
+
case "malformed_result":
|
|
246
|
+
case "launch_error":
|
|
247
|
+
return {
|
|
248
|
+
kind: "worker_process",
|
|
249
|
+
status: "failed",
|
|
250
|
+
reason_code: process.kind,
|
|
251
|
+
message: process.error,
|
|
252
|
+
process,
|
|
253
|
+
};
|
|
254
|
+
case "non_zero_exit":
|
|
255
|
+
case "timeout":
|
|
256
|
+
case "signal":
|
|
257
|
+
return {
|
|
258
|
+
kind: "worker_process",
|
|
259
|
+
status: "failed",
|
|
260
|
+
reason_code: process.kind,
|
|
261
|
+
message: null,
|
|
262
|
+
process,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function stableSubmissionKey(lease, workerSessionId) {
|
|
267
|
+
const digest = createHash("sha256").update(canonicalizeJson({
|
|
268
|
+
run_id: lease.run_id,
|
|
269
|
+
activation_id: lease.activation_id,
|
|
270
|
+
attempt_id: lease.attempt_id,
|
|
271
|
+
worker_session_id: workerSessionId,
|
|
272
|
+
}), "utf8").digest("hex");
|
|
273
|
+
return `push-driver:${digest}`;
|
|
274
|
+
}
|
|
275
|
+
function digest(value) {
|
|
276
|
+
return `sha256:${createHash("sha256")
|
|
277
|
+
.update(canonicalizeJson(value), "utf8")
|
|
278
|
+
.digest("hex")}`;
|
|
279
|
+
}
|
|
280
|
+
function assertCallerAttestation(authority, request, attestation) {
|
|
281
|
+
const payload = attestation.payload;
|
|
282
|
+
if (authority.authority_id.length === 0
|
|
283
|
+
|| payload === undefined
|
|
284
|
+
|| payload.schema_version !== CALLER_ATTESTATION_SCHEMA_VERSION
|
|
285
|
+
|| payload.authority_id !== authority.authority_id
|
|
286
|
+
|| payload.agent_id.length === 0
|
|
287
|
+
|| payload.session_id.length === 0
|
|
288
|
+
|| payload.run_id !== request.run_id
|
|
289
|
+
|| payload.step_id !== request.step_id
|
|
290
|
+
|| payload.activation_id !== request.activation_id
|
|
291
|
+
|| payload.attempt_id !== request.attempt_id
|
|
292
|
+
|| payload.claim_epoch !== request.claim_epoch
|
|
293
|
+
|| payload.worker_session_id !== request.worker_session_id
|
|
294
|
+
|| payload.submission_key_digest !== request.submission_key_digest
|
|
295
|
+
|| payload.execution_digest !== request.execution_digest
|
|
296
|
+
|| attestation.attestation_ref.length === 0
|
|
297
|
+
|| attestation.attestation_digest !== digest(payload)) {
|
|
298
|
+
throw new PushDriverError("invalid_execution_result", `caller attestation does not match attempt ${request.attempt_id}`);
|
|
299
|
+
}
|
|
300
|
+
return attestation;
|
|
301
|
+
}
|
|
302
|
+
function driverFailureKey(lease, workerSessionId) {
|
|
303
|
+
return `push-driver-failure:${createHash("sha256").update(canonicalizeJson({
|
|
304
|
+
run_id: lease.run_id,
|
|
305
|
+
attempt_id: lease.attempt_id,
|
|
306
|
+
worker_session_id: workerSessionId,
|
|
307
|
+
}), "utf8").digest("hex")}`;
|
|
308
|
+
}
|
|
309
|
+
function errorMessage(error) {
|
|
310
|
+
return error instanceof Error ? error.message : String(error);
|
|
311
|
+
}
|
|
312
|
+
function assertExecutionIdentity(context, provider, execution) {
|
|
313
|
+
const result = execution.result;
|
|
314
|
+
if (execution.worker_session_id !== context.worker_session_id
|
|
315
|
+
|| result.run_id !== context.lease.run_id
|
|
316
|
+
|| result.step_id !== context.lease.step_id
|
|
317
|
+
|| result.attempt_id !== context.lease.attempt_id
|
|
318
|
+
|| result.worker_session_id !== context.worker_session_id
|
|
319
|
+
|| result.provider !== provider.name
|
|
320
|
+
|| result.started_at !== context.attached.attempt.started_at) {
|
|
321
|
+
throw new PushDriverError("invalid_execution_result", `worker execution identity does not match attempt ${context.lease.attempt_id}`);
|
|
322
|
+
}
|
|
323
|
+
const startedAt = Date.parse(result.started_at);
|
|
324
|
+
const completedAt = Date.parse(result.completed_at);
|
|
325
|
+
if (!Number.isFinite(startedAt)
|
|
326
|
+
|| !Number.isFinite(completedAt)
|
|
327
|
+
|| completedAt < startedAt) {
|
|
328
|
+
throw new PushDriverError("invalid_execution_result", `worker execution timestamps are invalid for attempt ${context.lease.attempt_id}`);
|
|
329
|
+
}
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
export class PushDriver {
|
|
333
|
+
capabilities;
|
|
334
|
+
service;
|
|
335
|
+
providerFactory;
|
|
336
|
+
executionAuthority;
|
|
337
|
+
callerAttestationAuthority;
|
|
338
|
+
lifecycleHooks;
|
|
339
|
+
diagnosticSink;
|
|
340
|
+
maxConcurrency;
|
|
341
|
+
heartbeatIntervalMs;
|
|
342
|
+
waitStrategy;
|
|
343
|
+
driverId;
|
|
344
|
+
createWorkerSessionId;
|
|
345
|
+
executeAttempt;
|
|
346
|
+
workerOptions;
|
|
347
|
+
sleep;
|
|
348
|
+
constructor(options) {
|
|
349
|
+
const maxConcurrency = options.max_concurrency ?? 1;
|
|
350
|
+
const heartbeatIntervalMs = options.heartbeat_interval_ms
|
|
351
|
+
?? DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
352
|
+
const waitPollMs = options.wait_poll_ms ?? DEFAULT_WAIT_POLL_MS;
|
|
353
|
+
requirePositiveInteger("max_concurrency", maxConcurrency);
|
|
354
|
+
requirePositiveInteger("heartbeat_interval_ms", heartbeatIntervalMs);
|
|
355
|
+
requirePositiveInteger("wait_poll_ms", waitPollMs);
|
|
356
|
+
this.service = options.service;
|
|
357
|
+
this.providerFactory = options.provider_factory;
|
|
358
|
+
this.executionAuthority = options.execution_authority ?? null;
|
|
359
|
+
this.callerAttestationAuthority = options.caller_attestation_authority ?? null;
|
|
360
|
+
if (options.capabilities?.cancellation === true
|
|
361
|
+
&& this.executionAuthority === null) {
|
|
362
|
+
throw new PushDriverError("invalid_configuration", "cancellation capability requires an execution authority binding factory");
|
|
363
|
+
}
|
|
364
|
+
if (options.capabilities?.caller_attestation === true
|
|
365
|
+
&& this.callerAttestationAuthority === null) {
|
|
366
|
+
throw new PushDriverError("invalid_configuration", "caller attestation capability requires a trusted caller attestation authority");
|
|
367
|
+
}
|
|
368
|
+
this.lifecycleHooks = options.lifecycle_hooks ?? {};
|
|
369
|
+
this.diagnosticSink = options.diagnostic_sink ?? null;
|
|
370
|
+
this.maxConcurrency = maxConcurrency;
|
|
371
|
+
this.heartbeatIntervalMs = heartbeatIntervalMs;
|
|
372
|
+
this.waitStrategy = options.wait_strategy ?? defaultWaitStrategy(waitPollMs);
|
|
373
|
+
this.driverId = options.driver_id ?? `push-driver:${process.pid}:${randomUUID()}`;
|
|
374
|
+
if (this.driverId.length === 0) {
|
|
375
|
+
throw new PushDriverError("invalid_configuration", "driver_id must not be empty");
|
|
376
|
+
}
|
|
377
|
+
this.createWorkerSessionId = options.create_worker_session_id
|
|
378
|
+
?? (() => randomUUID());
|
|
379
|
+
this.executeAttempt = options.execute_attempt ?? executeWorkerAttempt;
|
|
380
|
+
this.workerOptions = options.worker_options;
|
|
381
|
+
this.sleep = options.sleep ?? defaultSleep;
|
|
382
|
+
this.capabilities = Object.freeze({
|
|
383
|
+
...DEFAULT_CAPABILITIES,
|
|
384
|
+
cancellation: this.executionAuthority !== null,
|
|
385
|
+
caller_attestation: this.callerAttestationAuthority !== null,
|
|
386
|
+
...options.capabilities,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
async emit(diagnostic) {
|
|
390
|
+
if (this.diagnosticSink === null)
|
|
391
|
+
return;
|
|
392
|
+
try {
|
|
393
|
+
await this.diagnosticSink(diagnostic);
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
// Diagnostics are observations and cannot become run authority.
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async attemptError(context, lease, error) {
|
|
400
|
+
await this.emit({
|
|
401
|
+
type: "attempt_failed",
|
|
402
|
+
run_id: lease.run_id,
|
|
403
|
+
attempt_id: lease.attempt_id,
|
|
404
|
+
error,
|
|
405
|
+
});
|
|
406
|
+
try {
|
|
407
|
+
await this.lifecycleHooks.on_attempt_error?.(context, error);
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
// Preserve the original driving failure.
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
assertHeartbeatLease(lease, heartbeat) {
|
|
414
|
+
const heartbeatAt = Date.parse(heartbeat.heartbeat_at);
|
|
415
|
+
const expiresAt = Date.parse(heartbeat.claim_expires_at);
|
|
416
|
+
if (heartbeat.run_id !== lease.run_id
|
|
417
|
+
|| heartbeat.step_id !== lease.step_id
|
|
418
|
+
|| heartbeat.activation_id !== lease.activation_id
|
|
419
|
+
|| heartbeat.attempt_id !== lease.attempt_id
|
|
420
|
+
|| heartbeat.attempt_number !== lease.attempt_number
|
|
421
|
+
|| heartbeat.claim_epoch !== lease.claim_epoch
|
|
422
|
+
|| heartbeat.owner_id !== lease.owner_id
|
|
423
|
+
|| heartbeat.execution_deadline_at !== lease.execution_deadline_at
|
|
424
|
+
|| !Number.isFinite(heartbeatAt)
|
|
425
|
+
|| !Number.isFinite(expiresAt)
|
|
426
|
+
|| expiresAt <= heartbeatAt) {
|
|
427
|
+
throw new PushDriverError("invalid_service_response", `heartbeat response does not match attempt ${lease.attempt_id}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async startHeartbeat(lease) {
|
|
431
|
+
let stopped = false;
|
|
432
|
+
let failureError = NO_ATTEMPT_ERROR;
|
|
433
|
+
let timer;
|
|
434
|
+
let inFlight = Promise.resolve();
|
|
435
|
+
let rejectFailure;
|
|
436
|
+
const failure = new Promise((_resolve, reject) => {
|
|
437
|
+
rejectFailure = reject;
|
|
438
|
+
});
|
|
439
|
+
void failure.catch(() => undefined);
|
|
440
|
+
const heartbeat = async () => {
|
|
441
|
+
const response = await this.service.heartbeat({
|
|
442
|
+
lease_token: lease.claim_token,
|
|
443
|
+
owner_id: lease.owner_id,
|
|
444
|
+
});
|
|
445
|
+
this.assertHeartbeatLease(lease, response);
|
|
446
|
+
return response;
|
|
447
|
+
};
|
|
448
|
+
const schedule = (response) => {
|
|
449
|
+
if (stopped)
|
|
450
|
+
return;
|
|
451
|
+
const claimExpiresAt = Date.parse(response.claim_expires_at);
|
|
452
|
+
const executionDeadline = Date.parse(response.execution_deadline_at);
|
|
453
|
+
if (claimExpiresAt === executionDeadline)
|
|
454
|
+
return;
|
|
455
|
+
const leaseWindow = claimExpiresAt - Date.parse(response.heartbeat_at);
|
|
456
|
+
const delay = Math.max(1, Math.min(this.heartbeatIntervalMs, Math.floor(leaseWindow / 2)));
|
|
457
|
+
timer = setTimeout(() => {
|
|
458
|
+
inFlight = heartbeat().then((next) => schedule(next), async (error) => {
|
|
459
|
+
failureError = error;
|
|
460
|
+
await this.emit({
|
|
461
|
+
type: "heartbeat_failed",
|
|
462
|
+
run_id: lease.run_id,
|
|
463
|
+
attempt_id: lease.attempt_id,
|
|
464
|
+
error,
|
|
465
|
+
});
|
|
466
|
+
stopped = true;
|
|
467
|
+
rejectFailure(error);
|
|
468
|
+
});
|
|
469
|
+
}, delay);
|
|
470
|
+
timer.unref?.();
|
|
471
|
+
};
|
|
472
|
+
schedule(await heartbeat());
|
|
473
|
+
return {
|
|
474
|
+
failure,
|
|
475
|
+
race: (operation) => (failureError === NO_ATTEMPT_ERROR
|
|
476
|
+
? Promise.race([failure, operation])
|
|
477
|
+
: Promise.reject(failureError)),
|
|
478
|
+
stop: async () => {
|
|
479
|
+
stopped = true;
|
|
480
|
+
if (timer !== undefined)
|
|
481
|
+
clearTimeout(timer);
|
|
482
|
+
await inFlight;
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
attachIsDefinitivelyFenced(view, lease, workerSessionId) {
|
|
487
|
+
const attempt = view.attempts.find((candidate) => candidate.attempt_id === lease.attempt_id);
|
|
488
|
+
const claims = view.claims
|
|
489
|
+
.filter((candidate) => candidate.attempt_id === lease.attempt_id)
|
|
490
|
+
.sort((left, right) => right.claim_epoch - left.claim_epoch);
|
|
491
|
+
const latestClaim = claims[0];
|
|
492
|
+
const attachedToOriginalWorker = attempt?.executor?.kind === "worker"
|
|
493
|
+
&& attempt.executor.worker_session_id === workerSessionId;
|
|
494
|
+
if (attempt?.executor !== null && attempt?.executor !== undefined && !attachedToOriginalWorker) {
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
if (latestClaim !== undefined
|
|
498
|
+
&& (latestClaim.claim_epoch !== lease.claim_epoch || latestClaim.owner_id !== lease.owner_id)
|
|
499
|
+
&& !attachedToOriginalWorker) {
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
return (view.run.terminal !== null || view.run.status === "cancelling")
|
|
503
|
+
&& !attachedToOriginalWorker;
|
|
504
|
+
}
|
|
505
|
+
async abandonPreparation(context, preparation, attachError) {
|
|
506
|
+
if (this.executionAuthority === null)
|
|
507
|
+
return;
|
|
508
|
+
try {
|
|
509
|
+
await this.executionAuthority.abandon(context, preparation);
|
|
510
|
+
}
|
|
511
|
+
catch (abandonError) {
|
|
512
|
+
throw new PushDriverError("authority_preparation_abandon_failed", `attempt ${context.lease.attempt_id} attach was fenced, but execution authority preparation cleanup failed: ${errorMessage(abandonError)}`, {
|
|
513
|
+
cause: new AggregateError([attachError, abandonError], "attempt attach fencing and authority preparation cleanup both failed"),
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
async reconcilePreparedAttach(request, preparationContext, preparation, originalError) {
|
|
518
|
+
const { lease, worker_session_id: workerSessionId } = preparationContext;
|
|
519
|
+
let inspected;
|
|
520
|
+
try {
|
|
521
|
+
inspected = await this.service.inspect(lease.run_id);
|
|
522
|
+
}
|
|
523
|
+
catch (inspectError) {
|
|
524
|
+
throw new PushDriverIndeterminateAttachError(lease.attempt_id, new AggregateError([originalError, inspectError], "attach and inspection failed"));
|
|
525
|
+
}
|
|
526
|
+
if (this.attachIsDefinitivelyFenced(inspected, lease, workerSessionId)) {
|
|
527
|
+
await this.abandonPreparation(preparationContext, preparation, originalError);
|
|
528
|
+
return { kind: "fenced", run: inspected };
|
|
529
|
+
}
|
|
530
|
+
try {
|
|
531
|
+
return { kind: "attached", value: await this.service.attachAttempt(request) };
|
|
532
|
+
}
|
|
533
|
+
catch (replayError) {
|
|
534
|
+
let replayInspection;
|
|
535
|
+
try {
|
|
536
|
+
replayInspection = await this.service.inspect(lease.run_id);
|
|
537
|
+
}
|
|
538
|
+
catch (inspectError) {
|
|
539
|
+
throw new PushDriverIndeterminateAttachError(lease.attempt_id, new AggregateError([originalError, replayError, inspectError], "attach replay and inspection failed"));
|
|
540
|
+
}
|
|
541
|
+
if (this.attachIsDefinitivelyFenced(replayInspection, lease, workerSessionId)) {
|
|
542
|
+
await this.abandonPreparation(preparationContext, preparation, replayError);
|
|
543
|
+
return { kind: "fenced", run: replayInspection };
|
|
544
|
+
}
|
|
545
|
+
throw new PushDriverIndeterminateAttachError(lease.attempt_id, new AggregateError([originalError, replayError], "attach disposition remains ambiguous"));
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
async driveAttempt(lease, run, workerSessionId, executionClaimEpoch, recoveryMode) {
|
|
549
|
+
let context = null;
|
|
550
|
+
let normalizedExecution = null;
|
|
551
|
+
let executionInvoked = false;
|
|
552
|
+
let processLaunchObserved = false;
|
|
553
|
+
let executionAuthority;
|
|
554
|
+
let launchAuthority = !recoveryMode;
|
|
555
|
+
let heartbeat = null;
|
|
556
|
+
try {
|
|
557
|
+
heartbeat = await this.startHeartbeat(lease);
|
|
558
|
+
const node = requireWorkerNode(run, lease.step_id);
|
|
559
|
+
await this.emit({
|
|
560
|
+
type: "attempt_acquired",
|
|
561
|
+
run_id: lease.run_id,
|
|
562
|
+
attempt_id: lease.attempt_id,
|
|
563
|
+
step_id: lease.step_id,
|
|
564
|
+
});
|
|
565
|
+
if (recoveryMode && this.executionAuthority === null) {
|
|
566
|
+
throw new PushDriverError("invalid_configuration", `attempt ${lease.attempt_id} cannot recover without execution authority`);
|
|
567
|
+
}
|
|
568
|
+
const authorityPreparationContext = {
|
|
569
|
+
run,
|
|
570
|
+
binding: run.run.run_binding.binding,
|
|
571
|
+
node,
|
|
572
|
+
lease,
|
|
573
|
+
worker_session_id: workerSessionId,
|
|
574
|
+
execution_claim_epoch: executionClaimEpoch,
|
|
575
|
+
};
|
|
576
|
+
const authorityPreparation = recoveryMode || this.executionAuthority === null
|
|
577
|
+
? undefined
|
|
578
|
+
: await heartbeat.race(Promise.resolve(this.executionAuthority.prepare(authorityPreparationContext)));
|
|
579
|
+
const attachRequest = {
|
|
580
|
+
lease_token: lease.claim_token,
|
|
581
|
+
owner_id: lease.owner_id,
|
|
582
|
+
worker_identity: workerSessionId,
|
|
583
|
+
capabilities: this.capabilities,
|
|
584
|
+
};
|
|
585
|
+
const rawAttach = this.service.attachAttempt(attachRequest).then((value) => ({ kind: "attached", value }), (error) => ({ kind: "rejected", error }));
|
|
586
|
+
let attachOutcome;
|
|
587
|
+
try {
|
|
588
|
+
attachOutcome = await heartbeat.race(rawAttach);
|
|
589
|
+
}
|
|
590
|
+
catch (heartbeatError) {
|
|
591
|
+
await heartbeat.stop();
|
|
592
|
+
heartbeat = null;
|
|
593
|
+
const rawOutcome = await rawAttach;
|
|
594
|
+
if (recoveryMode || this.executionAuthority === null)
|
|
595
|
+
throw heartbeatError;
|
|
596
|
+
const reconciliation = await this.reconcilePreparedAttach(attachRequest, authorityPreparationContext, authorityPreparation, rawOutcome.kind === "attached"
|
|
597
|
+
? heartbeatError
|
|
598
|
+
: new AggregateError([heartbeatError, rawOutcome.error], "heartbeat and attach failed"));
|
|
599
|
+
if (reconciliation.kind === "fenced")
|
|
600
|
+
return reconciliation.run;
|
|
601
|
+
assertAttemptView(lease, workerSessionId, reconciliation.value);
|
|
602
|
+
heartbeat = await this.startHeartbeat(lease);
|
|
603
|
+
attachOutcome = reconciliation;
|
|
604
|
+
}
|
|
605
|
+
if (attachOutcome.kind === "rejected") {
|
|
606
|
+
if (recoveryMode || this.executionAuthority === null)
|
|
607
|
+
throw attachOutcome.error;
|
|
608
|
+
const reconciliation = await this.reconcilePreparedAttach(attachRequest, authorityPreparationContext, authorityPreparation, attachOutcome.error);
|
|
609
|
+
if (reconciliation.kind === "fenced")
|
|
610
|
+
return reconciliation.run;
|
|
611
|
+
attachOutcome = reconciliation;
|
|
612
|
+
}
|
|
613
|
+
const attached = attachOutcome.value;
|
|
614
|
+
const packet = assertAttemptView(lease, workerSessionId, attached);
|
|
615
|
+
context = {
|
|
616
|
+
run,
|
|
617
|
+
binding: run.run.run_binding.binding,
|
|
618
|
+
node,
|
|
619
|
+
packet,
|
|
620
|
+
lease,
|
|
621
|
+
attached,
|
|
622
|
+
worker_session_id: workerSessionId,
|
|
623
|
+
};
|
|
624
|
+
await this.emit({
|
|
625
|
+
type: "attempt_attached",
|
|
626
|
+
run_id: lease.run_id,
|
|
627
|
+
attempt_id: lease.attempt_id,
|
|
628
|
+
worker_session_id: workerSessionId,
|
|
629
|
+
});
|
|
630
|
+
const startedAt = attached.attempt.started_at;
|
|
631
|
+
if (startedAt === null) {
|
|
632
|
+
throw new PushDriverError("invalid_service_response", `attempt ${lease.attempt_id} has no durable start time`);
|
|
633
|
+
}
|
|
634
|
+
if (this.executionAuthority !== null) {
|
|
635
|
+
const authorityContext = {
|
|
636
|
+
...context,
|
|
637
|
+
started_at: startedAt,
|
|
638
|
+
execution_claim_epoch: executionClaimEpoch,
|
|
639
|
+
};
|
|
640
|
+
if (recoveryMode) {
|
|
641
|
+
const resolution = await heartbeat.race(Promise.resolve(this.executionAuthority.recover(authorityContext)));
|
|
642
|
+
launchAuthority = resolution.kind === "launch";
|
|
643
|
+
executionAuthority = resolution.binding;
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
executionAuthority = await heartbeat.race(Promise.resolve(this.executionAuthority.bind(authorityContext, authorityPreparation)));
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
await heartbeat.race(Promise.resolve(this.lifecycleHooks.before_execute?.(context)));
|
|
650
|
+
const provider = await heartbeat.race(Promise.resolve(this.providerFactory(context)));
|
|
651
|
+
if (!provider.name) {
|
|
652
|
+
throw new PushDriverError("invalid_configuration", "provider factory returned an unnamed provider");
|
|
653
|
+
}
|
|
654
|
+
const observedExecutionAuthority = executionAuthority === undefined
|
|
655
|
+
? undefined
|
|
656
|
+
: {
|
|
657
|
+
...executionAuthority,
|
|
658
|
+
processStarted: async (processId) => {
|
|
659
|
+
processLaunchObserved = true;
|
|
660
|
+
await executionAuthority.processStarted(processId);
|
|
661
|
+
},
|
|
662
|
+
};
|
|
663
|
+
let execution;
|
|
664
|
+
let executionError = null;
|
|
665
|
+
const executionPromise = this.executeAttempt(packet, provider, {
|
|
666
|
+
...this.workerOptions,
|
|
667
|
+
cancellationReason: null,
|
|
668
|
+
execution_authority: observedExecutionAuthority,
|
|
669
|
+
startedAt,
|
|
670
|
+
});
|
|
671
|
+
try {
|
|
672
|
+
executionInvoked = true;
|
|
673
|
+
execution = await heartbeat.race(executionPromise);
|
|
674
|
+
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
executionError = error;
|
|
677
|
+
}
|
|
678
|
+
if (executionError !== null) {
|
|
679
|
+
void executionPromise.catch(() => undefined);
|
|
680
|
+
throw executionError;
|
|
681
|
+
}
|
|
682
|
+
if (execution === undefined) {
|
|
683
|
+
throw new PushDriverError("invalid_execution_result", `worker execution returned no result for attempt ${lease.attempt_id}`);
|
|
684
|
+
}
|
|
685
|
+
normalizedExecution = execution;
|
|
686
|
+
await this.lifecycleHooks.after_execute?.(context, execution);
|
|
687
|
+
const result = assertExecutionIdentity(context, provider, execution);
|
|
688
|
+
let outcome = terminalOutcomeToCanonicalAttemptOutcome(result.outcome);
|
|
689
|
+
let outputs;
|
|
690
|
+
try {
|
|
691
|
+
outputs = mapProviderOutputsToSubmission(node, result.outputs, outcome.kind === "worker_process" && outcome.status === "succeeded");
|
|
692
|
+
}
|
|
693
|
+
catch (error) {
|
|
694
|
+
outcome = {
|
|
695
|
+
kind: "worker_process",
|
|
696
|
+
status: "failed",
|
|
697
|
+
reason_code: error instanceof PushDriverError
|
|
698
|
+
? error.code
|
|
699
|
+
: "driver_postprocess_failed",
|
|
700
|
+
message: errorMessage(error),
|
|
701
|
+
process: result.outcome,
|
|
702
|
+
};
|
|
703
|
+
outputs = [];
|
|
704
|
+
}
|
|
705
|
+
const submissionKey = stableSubmissionKey(lease, workerSessionId);
|
|
706
|
+
const submissionExecution = {
|
|
707
|
+
executor: { kind: "worker", worker_session_id: workerSessionId },
|
|
708
|
+
started_at: result.started_at,
|
|
709
|
+
completed_at: result.completed_at,
|
|
710
|
+
outcome,
|
|
711
|
+
outputs,
|
|
712
|
+
workspace: {
|
|
713
|
+
root: packet.workspace.working_directory,
|
|
714
|
+
workspace_id: lease.attempt_id,
|
|
715
|
+
},
|
|
716
|
+
stdout_reference: null,
|
|
717
|
+
stderr_reference: null,
|
|
718
|
+
};
|
|
719
|
+
let evidenceRefs;
|
|
720
|
+
if (run.plan.plan.required_capabilities.caller_attestation) {
|
|
721
|
+
const authority = this.callerAttestationAuthority;
|
|
722
|
+
if (authority === null) {
|
|
723
|
+
throw new PushDriverError("unsupported", "trusted caller attestation authority is unavailable");
|
|
724
|
+
}
|
|
725
|
+
const attestationRequest = {
|
|
726
|
+
run_id: lease.run_id,
|
|
727
|
+
step_id: lease.step_id,
|
|
728
|
+
activation_id: lease.activation_id,
|
|
729
|
+
attempt_id: lease.attempt_id,
|
|
730
|
+
claim_epoch: lease.claim_epoch,
|
|
731
|
+
worker_session_id: workerSessionId,
|
|
732
|
+
provider_session_id: result.provider_session_id,
|
|
733
|
+
submission_key_digest: digest({ submission_key: submissionKey }),
|
|
734
|
+
execution_digest: digest(submissionExecution),
|
|
735
|
+
};
|
|
736
|
+
const attestation = await heartbeat.race(Promise.resolve(authority.attest(attestationRequest)));
|
|
737
|
+
evidenceRefs = [assertCallerAttestation(authority, attestationRequest, attestation)];
|
|
738
|
+
}
|
|
739
|
+
const response = await heartbeat.race(this.service.submitAttempt({
|
|
740
|
+
lease_token: lease.claim_token,
|
|
741
|
+
owner_id: lease.owner_id,
|
|
742
|
+
submission_key: submissionKey,
|
|
743
|
+
execution: submissionExecution,
|
|
744
|
+
...(evidenceRefs === undefined ? {} : { evidence_refs: evidenceRefs }),
|
|
745
|
+
}));
|
|
746
|
+
if (response.replayed) {
|
|
747
|
+
await this.emit({
|
|
748
|
+
type: "submission_replayed",
|
|
749
|
+
run_id: lease.run_id,
|
|
750
|
+
attempt_id: lease.attempt_id,
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
try {
|
|
754
|
+
await this.lifecycleHooks.after_submit?.(context, execution, response);
|
|
755
|
+
}
|
|
756
|
+
catch {
|
|
757
|
+
// Post-submit hooks are observations and cannot override a committed result.
|
|
758
|
+
}
|
|
759
|
+
return response.run;
|
|
760
|
+
}
|
|
761
|
+
catch (error) {
|
|
762
|
+
await this.attemptError(context, lease, error);
|
|
763
|
+
const canProvePrelaunch = launchAuthority && (!executionInvoked
|
|
764
|
+
|| (executionAuthority !== undefined && !processLaunchObserved));
|
|
765
|
+
if (context !== null && normalizedExecution === null && canProvePrelaunch) {
|
|
766
|
+
try {
|
|
767
|
+
return await this.service.settleDriverFailure({
|
|
768
|
+
lease_token: lease.claim_token,
|
|
769
|
+
owner_id: lease.owner_id,
|
|
770
|
+
failure_key: driverFailureKey(lease, workerSessionId),
|
|
771
|
+
reason_code: "driver_prelaunch_failed",
|
|
772
|
+
message: errorMessage(error),
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
catch (settlementError) {
|
|
776
|
+
throw new PushDriverError("driver_failure_settlement_failed", `driver failed before launch, but canonical settlement was rejected: ${errorMessage(settlementError)}`, {
|
|
777
|
+
cause: new AggregateError([error, settlementError], "driver failure and canonical settlement both failed"),
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
throw error;
|
|
782
|
+
}
|
|
783
|
+
finally {
|
|
784
|
+
await heartbeat?.stop();
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
trackAttempt(lease, run, workerSessionId, executionClaimEpoch, recoveryMode) {
|
|
788
|
+
return this.driveAttempt(lease, run, workerSessionId, executionClaimEpoch, recoveryMode).then((nextRun) => ({
|
|
789
|
+
attempt_id: lease.attempt_id,
|
|
790
|
+
run: nextRun,
|
|
791
|
+
error: null,
|
|
792
|
+
}), (error) => ({
|
|
793
|
+
attempt_id: lease.attempt_id,
|
|
794
|
+
run: null,
|
|
795
|
+
error,
|
|
796
|
+
}));
|
|
797
|
+
}
|
|
798
|
+
async run(runId, options = {}) {
|
|
799
|
+
if (!runId) {
|
|
800
|
+
throw new PushDriverError("invalid_configuration", "run_id must not be empty");
|
|
801
|
+
}
|
|
802
|
+
if (options.initial_view !== undefined && options.resume === true) {
|
|
803
|
+
throw new PushDriverError("invalid_configuration", "initial_view and resume cannot be used together");
|
|
804
|
+
}
|
|
805
|
+
let current = options.initial_view
|
|
806
|
+
?? (options.resume === true
|
|
807
|
+
? await this.service.resume(runId)
|
|
808
|
+
: await this.service.inspect(runId));
|
|
809
|
+
if (current.run.run_id !== runId) {
|
|
810
|
+
throw new PushDriverError("invalid_service_response", "service returned a different run_id");
|
|
811
|
+
}
|
|
812
|
+
const pinned = pinRun(current);
|
|
813
|
+
if (pinned.max_concurrency !== this.maxConcurrency) {
|
|
814
|
+
throw new PushDriverError("invalid_configuration", `driver max_concurrency ${this.maxConcurrency} does not match immutable run binding ${pinned.max_concurrency}`);
|
|
815
|
+
}
|
|
816
|
+
current = assertPinnedRun(pinned, current);
|
|
817
|
+
assertRequiredCapabilities(current.plan.plan.required_capabilities, this.capabilities);
|
|
818
|
+
const active = new Map();
|
|
819
|
+
const activeDeadlines = new Map();
|
|
820
|
+
const launchedAttempts = new Set();
|
|
821
|
+
const workerSessions = new Set();
|
|
822
|
+
let firstAttemptError = NO_ATTEMPT_ERROR;
|
|
823
|
+
let cancellationRequested = false;
|
|
824
|
+
const abandonActiveWaits = () => {
|
|
825
|
+
for (const promise of active.values()) {
|
|
826
|
+
void promise.then(() => undefined, () => undefined);
|
|
827
|
+
}
|
|
828
|
+
active.clear();
|
|
829
|
+
activeDeadlines.clear();
|
|
830
|
+
};
|
|
831
|
+
const cancelAndDrainAfterDriverFailure = async (error) => {
|
|
832
|
+
if (active.size === 0)
|
|
833
|
+
throw error;
|
|
834
|
+
if (!cancellationRequested) {
|
|
835
|
+
cancellationRequested = true;
|
|
836
|
+
current = assertPinnedRun(pinned, await this.service.cancel(runId, `push driver failed: ${errorMessage(error)}`));
|
|
837
|
+
}
|
|
838
|
+
const latestDeadline = Math.max(Date.now(), ...activeDeadlines.values());
|
|
839
|
+
abandonActiveWaits();
|
|
840
|
+
while (current.run.terminal === null) {
|
|
841
|
+
current = assertPinnedRun(pinned, await this.service.inspect(runId));
|
|
842
|
+
if (current.run.terminal !== null)
|
|
843
|
+
break;
|
|
844
|
+
if (Date.now() >= latestDeadline)
|
|
845
|
+
throw error;
|
|
846
|
+
const decision = await this.waitStrategy({
|
|
847
|
+
reason_code: "run_cancelling",
|
|
848
|
+
run: current,
|
|
849
|
+
active_attempts: 0,
|
|
850
|
+
});
|
|
851
|
+
if (decision.action === "stop")
|
|
852
|
+
throw error;
|
|
853
|
+
const remaining = latestDeadline - Date.now();
|
|
854
|
+
await this.sleep(Math.min(decision.delay_ms, remaining));
|
|
855
|
+
}
|
|
856
|
+
throw error;
|
|
857
|
+
};
|
|
858
|
+
for (;;) {
|
|
859
|
+
if (current.run.terminal !== null) {
|
|
860
|
+
if (active.size > 0) {
|
|
861
|
+
const siblings = [...active.values()];
|
|
862
|
+
active.clear();
|
|
863
|
+
activeDeadlines.clear();
|
|
864
|
+
await Promise.allSettled(siblings);
|
|
865
|
+
}
|
|
866
|
+
return current;
|
|
867
|
+
}
|
|
868
|
+
let waiting = null;
|
|
869
|
+
while (active.size < this.maxConcurrency
|
|
870
|
+
&& firstAttemptError === NO_ATTEMPT_ERROR) {
|
|
871
|
+
try {
|
|
872
|
+
const acquired = await this.service.acquireAttempt({
|
|
873
|
+
run_id: runId,
|
|
874
|
+
driver_id: this.driverId,
|
|
875
|
+
recover_running: options.resume === true,
|
|
876
|
+
});
|
|
877
|
+
current = assertPinnedRun(pinned, current);
|
|
878
|
+
if (acquired.kind === "terminal") {
|
|
879
|
+
current = assertPinnedRun(pinned, acquired.run);
|
|
880
|
+
break;
|
|
881
|
+
}
|
|
882
|
+
if (acquired.kind === "wait") {
|
|
883
|
+
current = assertPinnedRun(pinned, acquired.run);
|
|
884
|
+
waiting = acquired;
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
const lease = acquired.lease;
|
|
888
|
+
if (lease.run_id !== runId || lease.owner_id !== this.driverId) {
|
|
889
|
+
throw new PushDriverError("invalid_service_response", "acquired lease does not belong to this driver and run");
|
|
890
|
+
}
|
|
891
|
+
requireNonEmpty("attempt_id", lease.attempt_id);
|
|
892
|
+
if (launchedAttempts.has(lease.attempt_id)) {
|
|
893
|
+
throw new PushDriverError("attempt_relaunch", `service attempted to relaunch ${lease.attempt_id}`);
|
|
894
|
+
}
|
|
895
|
+
const executionDeadline = Date.parse(lease.execution_deadline_at);
|
|
896
|
+
if (!Number.isFinite(executionDeadline)) {
|
|
897
|
+
throw new PushDriverError("invalid_service_response", `attempt ${lease.attempt_id} has an invalid execution deadline`);
|
|
898
|
+
}
|
|
899
|
+
const workerSessionId = acquired.recovery?.worker_session_id
|
|
900
|
+
?? this.createWorkerSessionId();
|
|
901
|
+
requireNonEmpty("worker_session_id", workerSessionId);
|
|
902
|
+
if (workerSessions.has(workerSessionId)) {
|
|
903
|
+
throw new PushDriverError("invalid_configuration", `worker session factory reused ${workerSessionId}`);
|
|
904
|
+
}
|
|
905
|
+
launchedAttempts.add(lease.attempt_id);
|
|
906
|
+
workerSessions.add(workerSessionId);
|
|
907
|
+
const tracked = this.trackAttempt(lease, current, workerSessionId, acquired.recovery?.execution_claim_epoch ?? lease.claim_epoch, acquired.recovery !== null).then((completion) => {
|
|
908
|
+
if (completion.error !== null
|
|
909
|
+
&& firstAttemptError === NO_ATTEMPT_ERROR) {
|
|
910
|
+
firstAttemptError = completion.error;
|
|
911
|
+
}
|
|
912
|
+
return completion;
|
|
913
|
+
});
|
|
914
|
+
active.set(lease.attempt_id, tracked);
|
|
915
|
+
activeDeadlines.set(lease.attempt_id, executionDeadline);
|
|
916
|
+
}
|
|
917
|
+
catch (error) {
|
|
918
|
+
await cancelAndDrainAfterDriverFailure(error);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if (current.run.terminal !== null)
|
|
922
|
+
continue;
|
|
923
|
+
if (active.size > 0) {
|
|
924
|
+
const completion = await Promise.race(active.values());
|
|
925
|
+
const completedDeadline = activeDeadlines.get(completion.attempt_id);
|
|
926
|
+
active.delete(completion.attempt_id);
|
|
927
|
+
activeDeadlines.delete(completion.attempt_id);
|
|
928
|
+
if (completion.error !== null) {
|
|
929
|
+
const attemptError = firstAttemptError === NO_ATTEMPT_ERROR
|
|
930
|
+
? completion.error
|
|
931
|
+
: firstAttemptError;
|
|
932
|
+
if (attemptError instanceof PushDriverIndeterminateAttachError) {
|
|
933
|
+
const siblings = [...active.values()];
|
|
934
|
+
active.clear();
|
|
935
|
+
activeDeadlines.clear();
|
|
936
|
+
await Promise.allSettled(siblings);
|
|
937
|
+
throw attemptError;
|
|
938
|
+
}
|
|
939
|
+
if (!cancellationRequested) {
|
|
940
|
+
cancellationRequested = true;
|
|
941
|
+
current = assertPinnedRun(pinned, await this.service.cancel(runId, `push driver attempt failed: ${errorMessage(attemptError)}`));
|
|
942
|
+
if (current.run.terminal !== null) {
|
|
943
|
+
abandonActiveWaits();
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
const latestDeadline = Math.max(Date.now(), completedDeadline ?? Number.NEGATIVE_INFINITY, ...activeDeadlines.values());
|
|
948
|
+
abandonActiveWaits();
|
|
949
|
+
for (;;) {
|
|
950
|
+
current = assertPinnedRun(pinned, await this.service.inspect(runId));
|
|
951
|
+
if (current.run.terminal !== null)
|
|
952
|
+
break;
|
|
953
|
+
if (Date.now() >= latestDeadline)
|
|
954
|
+
throw attemptError;
|
|
955
|
+
const decision = await this.waitStrategy({
|
|
956
|
+
reason_code: "run_cancelling",
|
|
957
|
+
run: current,
|
|
958
|
+
active_attempts: 0,
|
|
959
|
+
});
|
|
960
|
+
if (decision.action === "stop")
|
|
961
|
+
throw attemptError;
|
|
962
|
+
const remaining = latestDeadline - Date.now();
|
|
963
|
+
await this.sleep(Math.min(decision.delay_ms, remaining));
|
|
964
|
+
}
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
if (completion.run === null) {
|
|
968
|
+
throw new PushDriverError("invalid_service_response", `attempt ${completion.attempt_id} completed without a run view`);
|
|
969
|
+
}
|
|
970
|
+
current = assertPinnedRun(pinned, completion.run);
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
if (waiting === null) {
|
|
974
|
+
throw new PushDriverError("invalid_service_response", "driver has neither an active attempt nor a typed wait response");
|
|
975
|
+
}
|
|
976
|
+
const decision = await this.waitStrategy({
|
|
977
|
+
reason_code: waiting.reason_code,
|
|
978
|
+
run: current,
|
|
979
|
+
active_attempts: 0,
|
|
980
|
+
});
|
|
981
|
+
if (decision.action === "stop") {
|
|
982
|
+
await this.emit({
|
|
983
|
+
type: "wait",
|
|
984
|
+
run_id: runId,
|
|
985
|
+
reason_code: waiting.reason_code,
|
|
986
|
+
delay_ms: null,
|
|
987
|
+
});
|
|
988
|
+
throw new PushDriverError("wait_stopped", decision.reason ?? `wait strategy stopped on ${waiting.reason_code}`);
|
|
989
|
+
}
|
|
990
|
+
if (!Number.isSafeInteger(decision.delay_ms) || decision.delay_ms < 0) {
|
|
991
|
+
throw new PushDriverError("invalid_configuration", "wait strategy delay_ms must be a non-negative safe integer");
|
|
992
|
+
}
|
|
993
|
+
await this.emit({
|
|
994
|
+
type: "wait",
|
|
995
|
+
run_id: runId,
|
|
996
|
+
reason_code: waiting.reason_code,
|
|
997
|
+
delay_ms: decision.delay_ms,
|
|
998
|
+
});
|
|
999
|
+
await this.sleep(decision.delay_ms);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|