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,788 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Dependency-free Windows supervisor. The native keeper owns the Job Object;
|
|
4
|
+
* this process owns durable CAS transitions and the authenticated named pipe.
|
|
5
|
+
*/
|
|
6
|
+
export const LOCAL_WINDOWS_EXECUTION_SUPERVISOR_SOURCE = String.raw `"use strict";
|
|
7
|
+
const { spawn } = require("node:child_process");
|
|
8
|
+
const { createHash, randomUUID } = require("node:crypto");
|
|
9
|
+
const fs = require("node:fs");
|
|
10
|
+
const net = require("node:net");
|
|
11
|
+
const path = require("node:path");
|
|
12
|
+
|
|
13
|
+
const RECORD_SCHEMA = "intentdna.local_execution_record.v1";
|
|
14
|
+
const CONTROL_SCHEMA = "intentdna.local_execution_control.v1";
|
|
15
|
+
const FACT_SCHEMA = "intentdna.windows_job_keeper_fact.v1";
|
|
16
|
+
const KEEPER_MAGIC = "IDNA_WINDOWS_JOB_KEEPER_V1";
|
|
17
|
+
const recordPath = process.argv[2];
|
|
18
|
+
if (!recordPath) process.exit(64);
|
|
19
|
+
|
|
20
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
+
const isErrno = (error, code) => error && error.code === code;
|
|
22
|
+
const renameWait = new Int32Array(new SharedArrayBuffer(4));
|
|
23
|
+
const canonical = (value) => {
|
|
24
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
25
|
+
if (Array.isArray(value)) return "[" + value.map(canonical).join(",") + "]";
|
|
26
|
+
return "{" + Object.keys(value).sort().map((key) => JSON.stringify(key) + ":" + canonical(value[key])).join(",") + "}";
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function replaceFile(temporary, target) {
|
|
30
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
31
|
+
try {
|
|
32
|
+
fs.renameSync(temporary, target);
|
|
33
|
+
return;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
const retryable = ["EACCES", "EBUSY", "EPERM"].some((code) => isErrno(error, code));
|
|
36
|
+
if (!retryable || attempt >= 9) {
|
|
37
|
+
try { fs.unlinkSync(temporary); } catch {}
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
Atomics.wait(renameWait, 0, 0, 25 * (attempt + 1));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function atomicWrite(target, value) {
|
|
46
|
+
const directory = path.dirname(target);
|
|
47
|
+
const temporary = path.join(directory, "." + path.basename(target) + ".tmp." + process.pid + "." + randomUUID());
|
|
48
|
+
const fd = fs.openSync(temporary, "wx", 0o600);
|
|
49
|
+
try {
|
|
50
|
+
fs.writeFileSync(fd, canonical(value) + "\n", "utf8");
|
|
51
|
+
fs.fsyncSync(fd);
|
|
52
|
+
} finally {
|
|
53
|
+
fs.closeSync(fd);
|
|
54
|
+
}
|
|
55
|
+
replaceFile(temporary, target);
|
|
56
|
+
try {
|
|
57
|
+
const directoryFd = fs.openSync(directory, "r");
|
|
58
|
+
try { fs.fsyncSync(directoryFd); } finally { fs.closeSync(directoryFd); }
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (!isErrno(error, "EPERM") && !isErrno(error, "EACCES") && !isErrno(error, "EISDIR") && !isErrno(error, "EINVAL") && !isErrno(error, "ENOTSUP")) throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readRecord() {
|
|
65
|
+
return JSON.parse(fs.readFileSync(recordPath, "utf8"));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function syncDirectory(target) {
|
|
69
|
+
try {
|
|
70
|
+
const descriptor = fs.openSync(target, "r");
|
|
71
|
+
try { fs.fsyncSync(descriptor); } finally { fs.closeSync(descriptor); }
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (!isErrno(error, "EPERM") && !isErrno(error, "EACCES") && !isErrno(error, "EISDIR")
|
|
74
|
+
&& !isErrno(error, "EINVAL") && !isErrno(error, "ENOTSUP")) throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readLockOwner(target) {
|
|
79
|
+
try {
|
|
80
|
+
const value = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
81
|
+
return exactKeys(value, ["schema_version", "pid", "process_birth_id", "generation", "acquired_at"])
|
|
82
|
+
&& value.schema_version === "intentdna.execution_lock_owner.v1"
|
|
83
|
+
&& positivePid(value.pid)
|
|
84
|
+
&& typeof value.process_birth_id === "string" && value.process_birth_id.length > 0
|
|
85
|
+
&& typeof value.generation === "string" && value.generation.length > 0
|
|
86
|
+
&& Number.isSafeInteger(value.acquired_at) && value.acquired_at >= 0
|
|
87
|
+
? value
|
|
88
|
+
: null;
|
|
89
|
+
} catch { return null; }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function lockOwnersEqual(left, right) {
|
|
93
|
+
return left !== null && canonical(left) === canonical(right);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function lockOwnerDigest(owner) {
|
|
97
|
+
return createHash("sha256").update(canonical(owner), "utf8").digest("hex");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function withRecordLock(operation) {
|
|
101
|
+
const lockPath = recordPath + ".lock";
|
|
102
|
+
const ownerPath = path.join(lockPath, "owner.json");
|
|
103
|
+
const owner = {
|
|
104
|
+
schema_version: "intentdna.execution_lock_owner.v1",
|
|
105
|
+
pid: process.pid,
|
|
106
|
+
process_birth_id: "node-time-origin-ms:" + String(Math.floor(performance.timeOrigin)),
|
|
107
|
+
generation: randomUUID(),
|
|
108
|
+
acquired_at: Date.now(),
|
|
109
|
+
};
|
|
110
|
+
const deadline = Date.now() + 5000;
|
|
111
|
+
for (;;) {
|
|
112
|
+
const candidatePath = lockPath + ".candidate." + process.pid + "." + randomUUID();
|
|
113
|
+
try {
|
|
114
|
+
fs.mkdirSync(candidatePath, { mode: 0o700 });
|
|
115
|
+
const descriptor = fs.openSync(path.join(candidatePath, "owner.json"), "wx", 0o600);
|
|
116
|
+
try {
|
|
117
|
+
fs.writeFileSync(descriptor, canonical(owner) + "\n", "utf8");
|
|
118
|
+
fs.fsyncSync(descriptor);
|
|
119
|
+
} finally { fs.closeSync(descriptor); }
|
|
120
|
+
try {
|
|
121
|
+
const candidateDescriptor = fs.openSync(candidatePath, "r");
|
|
122
|
+
try { fs.fsyncSync(candidateDescriptor); } finally { fs.closeSync(candidateDescriptor); }
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (!isErrno(error, "EPERM") && !isErrno(error, "EACCES") && !isErrno(error, "EISDIR")
|
|
125
|
+
&& !isErrno(error, "EINVAL") && !isErrno(error, "ENOTSUP")) throw error;
|
|
126
|
+
}
|
|
127
|
+
fs.renameSync(candidatePath, lockPath);
|
|
128
|
+
syncDirectory(path.dirname(lockPath));
|
|
129
|
+
break;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (!isErrno(error, "EEXIST") && !isErrno(error, "ENOTEMPTY")
|
|
132
|
+
&& !(isErrno(error, "EPERM") && fs.existsSync(lockPath))) throw error;
|
|
133
|
+
if (Date.now() >= deadline) throw new Error("execution record lock is unavailable");
|
|
134
|
+
await sleep(10);
|
|
135
|
+
} finally {
|
|
136
|
+
fs.rmSync(candidatePath, { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
return await operation();
|
|
141
|
+
} finally {
|
|
142
|
+
try {
|
|
143
|
+
const current = readLockOwner(ownerPath);
|
|
144
|
+
if (lockOwnersEqual(current, owner)) {
|
|
145
|
+
const released = lockPath + ".released." + lockOwnerDigest(owner) + "." + randomUUID();
|
|
146
|
+
let retired = true;
|
|
147
|
+
try { fs.renameSync(lockPath, released); }
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (isErrno(error, "ENOENT")) retired = false;
|
|
150
|
+
else throw error;
|
|
151
|
+
}
|
|
152
|
+
if (retired) {
|
|
153
|
+
syncDirectory(path.dirname(lockPath));
|
|
154
|
+
if (!lockOwnersEqual(readLockOwner(path.join(released, "owner.json")), owner)) {
|
|
155
|
+
throw new Error("execution record lock ownership changed during release");
|
|
156
|
+
}
|
|
157
|
+
fs.rmSync(released, { recursive: true, force: true });
|
|
158
|
+
syncDirectory(path.dirname(lockPath));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (!isErrno(error, "ENOENT")) throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function exactKeys(value, keys) {
|
|
168
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
169
|
+
const actual = Object.keys(value).sort();
|
|
170
|
+
const expected = [...keys].sort();
|
|
171
|
+
return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function positivePid(value) {
|
|
175
|
+
return Number.isSafeInteger(value) && value > 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function filetime(value) {
|
|
179
|
+
return typeof value === "string" && /^[1-9][0-9]*$/.test(value);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function digest(value) {
|
|
183
|
+
return typeof value === "string" && /^sha256:[0-9a-f]{64}$/.test(value);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function streamEvidence(target, maxOutputBytes) {
|
|
187
|
+
const metadata = fs.lstatSync(target, { bigint: true });
|
|
188
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > BigInt(maxOutputBytes)) {
|
|
189
|
+
throw new Error("provider stream does not satisfy the durable output boundary");
|
|
190
|
+
}
|
|
191
|
+
const bytes = fs.readFileSync(target);
|
|
192
|
+
if (BigInt(bytes.byteLength) !== metadata.size) {
|
|
193
|
+
throw new Error("provider stream changed while completion evidence was captured");
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
path: target,
|
|
197
|
+
byte_count: bytes.byteLength,
|
|
198
|
+
sha256: "sha256:" + createHash("sha256").update(bytes).digest("hex"),
|
|
199
|
+
device: metadata.dev.toString(),
|
|
200
|
+
inode: metadata.ino.toString(),
|
|
201
|
+
birthtime_ns: metadata.birthtimeNs.toString(),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function parseFact(line) {
|
|
206
|
+
const value = JSON.parse(line);
|
|
207
|
+
if (!value || value.schema_version !== FACT_SCHEMA || typeof value.kind !== "string") {
|
|
208
|
+
throw new Error("keeper emitted a fact with an invalid schema");
|
|
209
|
+
}
|
|
210
|
+
if (value.kind === "contained_ready") {
|
|
211
|
+
if (!exactKeys(value, ["schema_version", "kind", "job_identifier", "keeper_pid", "keeper_creation_filetime", "root_pid", "root_creation_filetime"])
|
|
212
|
+
|| !digest(value.job_identifier) || !positivePid(value.keeper_pid)
|
|
213
|
+
|| !filetime(value.keeper_creation_filetime) || !positivePid(value.root_pid)
|
|
214
|
+
|| !filetime(value.root_creation_filetime)) {
|
|
215
|
+
throw new Error("keeper emitted invalid containment identity");
|
|
216
|
+
}
|
|
217
|
+
return value;
|
|
218
|
+
}
|
|
219
|
+
if (value.kind === "released") {
|
|
220
|
+
if (!exactKeys(value, ["schema_version", "kind", "root_pid"]) || !positivePid(value.root_pid)) {
|
|
221
|
+
throw new Error("keeper emitted invalid release evidence");
|
|
222
|
+
}
|
|
223
|
+
return value;
|
|
224
|
+
}
|
|
225
|
+
if (value.kind === "completed") {
|
|
226
|
+
if (!exactKeys(value, ["schema_version", "kind", "reason", "root_exit_observed", "root_exit_code", "windows_signal", "active_processes", "process_tree_drained"])
|
|
227
|
+
|| !["root_exit", "terminate", "parent_eof", "protocol_error", "drain_timeout", "output_limit"].includes(value.reason)
|
|
228
|
+
|| typeof value.root_exit_observed !== "boolean"
|
|
229
|
+
|| !Number.isSafeInteger(value.root_exit_code) || value.root_exit_code < 0 || value.root_exit_code > 0xffffffff
|
|
230
|
+
|| value.windows_signal !== null || value.active_processes !== 0
|
|
231
|
+
|| value.process_tree_drained !== true) {
|
|
232
|
+
throw new Error("keeper emitted invalid completion evidence");
|
|
233
|
+
}
|
|
234
|
+
return value;
|
|
235
|
+
}
|
|
236
|
+
throw new Error("keeper emitted an unknown fact kind");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function hex(value) {
|
|
240
|
+
if (typeof value !== "string" || value.includes("\0")) throw new Error("keeper configuration contains NUL");
|
|
241
|
+
return Buffer.from(value, "utf8").toString("hex");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function encodeConfig(config) {
|
|
245
|
+
const lines = [
|
|
246
|
+
KEEPER_MAGIC,
|
|
247
|
+
"exe:" + hex(config.executable),
|
|
248
|
+
"cwd:" + hex(config.cwd),
|
|
249
|
+
"stdin:" + hex(config.stdin_path || ""),
|
|
250
|
+
"stdout:" + hex(config.stdout_path),
|
|
251
|
+
"stderr:" + hex(config.stderr_path),
|
|
252
|
+
"max_output_bytes:" + String(config.max_output_bytes),
|
|
253
|
+
];
|
|
254
|
+
if (config.command_line !== null) {
|
|
255
|
+
lines.push("mode:verbatim", "command:" + hex(config.command_line));
|
|
256
|
+
} else {
|
|
257
|
+
lines.push("mode:argv", "argc:" + String(config.args.length));
|
|
258
|
+
for (const argument of config.args) lines.push("arg:" + hex(argument));
|
|
259
|
+
}
|
|
260
|
+
const environment = Object.entries(config.env).sort(([left], [right]) => {
|
|
261
|
+
const folded = left.toLowerCase().localeCompare(right.toLowerCase(), "en");
|
|
262
|
+
return folded === 0 ? left.localeCompare(right, "en") : folded;
|
|
263
|
+
});
|
|
264
|
+
const seen = new Set();
|
|
265
|
+
lines.push("envc:" + String(environment.length));
|
|
266
|
+
for (const [key, value] of environment) {
|
|
267
|
+
if (key.length === 0 || key.includes("=") || key.includes("\0") || value.includes("\0")) {
|
|
268
|
+
throw new Error("keeper environment contains an invalid entry");
|
|
269
|
+
}
|
|
270
|
+
const folded = key.toLowerCase();
|
|
271
|
+
if (seen.has(folded)) throw new Error("keeper environment contains duplicate case-insensitive keys");
|
|
272
|
+
seen.add(folded);
|
|
273
|
+
lines.push("env:" + hex(key + "=" + value));
|
|
274
|
+
}
|
|
275
|
+
lines.push(
|
|
276
|
+
"natural_drain_grace_ms:" + String(config.natural_drain_grace_ms),
|
|
277
|
+
"forced_drain_grace_ms:" + String(config.forced_drain_grace_ms),
|
|
278
|
+
"end",
|
|
279
|
+
);
|
|
280
|
+
const encoded = lines.join("\n") + "\n";
|
|
281
|
+
if (Buffer.byteLength(encoded, "utf8") > 8 * 1024 * 1024) throw new Error("keeper configuration exceeds the protocol limit");
|
|
282
|
+
return encoded;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function main() {
|
|
286
|
+
let configurationText = "";
|
|
287
|
+
for await (const chunk of process.stdin) configurationText += chunk;
|
|
288
|
+
const config = JSON.parse(configurationText);
|
|
289
|
+
const prepared = readRecord();
|
|
290
|
+
if (prepared.schema_version !== RECORD_SCHEMA || prepared.phase !== "launching"
|
|
291
|
+
|| prepared.execution_instance_id !== config.execution_instance_id
|
|
292
|
+
|| prepared.challenge !== config.challenge
|
|
293
|
+
|| prepared.backend.kind !== "windows_job"
|
|
294
|
+
|| prepared.backend.containment_nonce !== config.containment_nonce
|
|
295
|
+
|| prepared.backend.helper_path !== config.helper_path
|
|
296
|
+
|| prepared.backend.helper_digest !== config.helper_digest
|
|
297
|
+
|| prepared.supervisor.socket_path !== config.control_endpoint) {
|
|
298
|
+
throw new Error("prepared execution record does not match Windows supervisor configuration");
|
|
299
|
+
}
|
|
300
|
+
if (!path.win32.isAbsolute(config.helper_path) || !path.win32.isAbsolute(config.executable)
|
|
301
|
+
|| !path.win32.isAbsolute(config.cwd) || !path.win32.isAbsolute(config.stdout_path)
|
|
302
|
+
|| !path.win32.isAbsolute(config.stderr_path)
|
|
303
|
+
|| (config.stdin_path !== null && !path.win32.isAbsolute(config.stdin_path))
|
|
304
|
+
|| !Array.isArray(config.args) || config.args.length > 4096
|
|
305
|
+
|| config.args.some((value) => typeof value !== "string" || value.includes("\0"))
|
|
306
|
+
|| config.env === null || typeof config.env !== "object" || Array.isArray(config.env)
|
|
307
|
+
|| !Number.isSafeInteger(config.natural_drain_grace_ms)
|
|
308
|
+
|| config.natural_drain_grace_ms < 0 || config.natural_drain_grace_ms > 300000
|
|
309
|
+
|| !Number.isSafeInteger(config.forced_drain_grace_ms)
|
|
310
|
+
|| config.forced_drain_grace_ms < 0 || config.forced_drain_grace_ms > 300000
|
|
311
|
+
|| !Number.isSafeInteger(config.max_output_bytes) || config.max_output_bytes <= 0
|
|
312
|
+
|| (config.timeout_ms !== null && (!Number.isSafeInteger(config.timeout_ms) || config.timeout_ms <= 0))) {
|
|
313
|
+
throw new Error("Windows supervisor configuration is invalid");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function validateHelper() {
|
|
317
|
+
const helperMetadata = fs.lstatSync(config.helper_path);
|
|
318
|
+
if (!helperMetadata.isFile() || helperMetadata.isSymbolicLink()) throw new Error("Windows Job keeper is not a trusted regular file");
|
|
319
|
+
const helperDigest = "sha256:" + createHash("sha256").update(fs.readFileSync(config.helper_path)).digest("hex");
|
|
320
|
+
if (helperDigest !== config.helper_digest) throw new Error("Windows Job keeper changed after preflight");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
let helper = null;
|
|
324
|
+
let helperFacts = "";
|
|
325
|
+
let helperErrors = "";
|
|
326
|
+
let stopFact = null;
|
|
327
|
+
let rawCompletion = null;
|
|
328
|
+
let pendingCompletionFact = null;
|
|
329
|
+
let provider = null;
|
|
330
|
+
let released = false;
|
|
331
|
+
let releaseSent = false;
|
|
332
|
+
let terminateSent = false;
|
|
333
|
+
let supervisorReadyAt = null;
|
|
334
|
+
let uncertaintyObserved = null;
|
|
335
|
+
let timeout = null;
|
|
336
|
+
let completionSettled = false;
|
|
337
|
+
let completionResolve;
|
|
338
|
+
const completion = new Promise((resolve) => { completionResolve = resolve; });
|
|
339
|
+
|
|
340
|
+
function settleCompletion(value) {
|
|
341
|
+
if (completionSettled) return;
|
|
342
|
+
completionSettled = true;
|
|
343
|
+
completionResolve(value);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function persist(patch) {
|
|
347
|
+
return withRecordLock(async () => {
|
|
348
|
+
const current = readRecord();
|
|
349
|
+
if (current.execution_instance_id !== config.execution_instance_id
|
|
350
|
+
|| current.challenge !== config.challenge
|
|
351
|
+
|| current.backend.kind !== "windows_job"
|
|
352
|
+
|| current.backend.containment_nonce !== config.containment_nonce) {
|
|
353
|
+
throw new Error("execution record identity changed");
|
|
354
|
+
}
|
|
355
|
+
if (current.phase === "never_launched") throw new Error("execution was durably attested as never launched");
|
|
356
|
+
if (current.phase === "unsupported") return current;
|
|
357
|
+
if (["raw_completed", "normalized_completed", "receipted"].includes(current.phase)) {
|
|
358
|
+
return current;
|
|
359
|
+
}
|
|
360
|
+
if (patch.phase === "running" && current.phase !== "launching") throw new Error("execution no longer owns the durable launch claim");
|
|
361
|
+
if (patch.phase === "raw_completed" && current.phase !== "running") throw new Error("raw completion requires running launch ownership");
|
|
362
|
+
const next = { ...current, ...patch, revision: current.revision + 1, updated_at: new Date().toISOString() };
|
|
363
|
+
atomicWrite(recordPath, next);
|
|
364
|
+
return next;
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function stopHelperAfterUncertainty() {
|
|
369
|
+
try {
|
|
370
|
+
if (!helper || !helper.stdin || helper.stdin.destroyed) return;
|
|
371
|
+
if (released) {
|
|
372
|
+
if (!terminateSent) {
|
|
373
|
+
terminateSent = true;
|
|
374
|
+
helper.stdin.end("terminate\n");
|
|
375
|
+
}
|
|
376
|
+
} else {
|
|
377
|
+
helper.stdin.end();
|
|
378
|
+
}
|
|
379
|
+
} catch {}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function commitUnsupported(error) {
|
|
383
|
+
stopHelperAfterUncertainty();
|
|
384
|
+
await withRecordLock(async () => {
|
|
385
|
+
const current = readRecord();
|
|
386
|
+
if (["never_launched", "raw_completed", "normalized_completed", "receipted", "unsupported"].includes(current.phase)) return;
|
|
387
|
+
atomicWrite(recordPath, {
|
|
388
|
+
...current,
|
|
389
|
+
phase: "unsupported",
|
|
390
|
+
supervisor_error: error instanceof Error ? error.message : String(error),
|
|
391
|
+
revision: current.revision + 1,
|
|
392
|
+
updated_at: new Date().toISOString(),
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
settleCompletion(null);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
let eventQueue = Promise.resolve();
|
|
399
|
+
function enqueue(operation) {
|
|
400
|
+
const task = eventQueue.then(async () => {
|
|
401
|
+
if (uncertaintyObserved !== null) throw uncertaintyObserved;
|
|
402
|
+
return operation();
|
|
403
|
+
});
|
|
404
|
+
eventQueue = task.then(
|
|
405
|
+
() => undefined,
|
|
406
|
+
async (error) => {
|
|
407
|
+
if (error && error.control_conflict === true) return;
|
|
408
|
+
if (uncertaintyObserved === null) {
|
|
409
|
+
uncertaintyObserved = error instanceof Error ? error : new Error(String(error));
|
|
410
|
+
}
|
|
411
|
+
try { await commitUnsupported(uncertaintyObserved); }
|
|
412
|
+
catch { stopHelperAfterUncertainty(); settleCompletion(null); }
|
|
413
|
+
},
|
|
414
|
+
);
|
|
415
|
+
return task;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function enqueueObserved(operation) {
|
|
419
|
+
void enqueue(operation).catch(() => undefined);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function controlConflict(message) {
|
|
423
|
+
const error = new Error(message);
|
|
424
|
+
error.control_conflict = true;
|
|
425
|
+
return error;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function sendTerminateIfReleased() {
|
|
429
|
+
if (stopFact === null || !released || terminateSent) return;
|
|
430
|
+
if (!helper || !helper.stdin || helper.stdin.destroyed) {
|
|
431
|
+
throw new Error("Windows Job keeper control channel is unavailable after release");
|
|
432
|
+
}
|
|
433
|
+
terminateSent = true;
|
|
434
|
+
helper.stdin.write("terminate\n");
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function canonicalStopMode(outcome) {
|
|
438
|
+
if (!outcome || outcome.kind !== "worker_process" || !outcome.process) return null;
|
|
439
|
+
if (outcome.status === "failed" && outcome.process.kind === "timeout"
|
|
440
|
+
&& exactKeys(outcome.process, ["kind", "exit_code", "signal", "timeout_ms"])
|
|
441
|
+
&& outcome.process.exit_code === null && outcome.process.signal === null
|
|
442
|
+
&& Number.isSafeInteger(outcome.process.timeout_ms) && outcome.process.timeout_ms > 0) {
|
|
443
|
+
return { kind: "timeout", reason: null };
|
|
444
|
+
}
|
|
445
|
+
if (outcome.status === "cancelled" && outcome.process.kind === "cancelled"
|
|
446
|
+
&& exactKeys(outcome.process, ["kind", "exit_code", "signal", "reason"])
|
|
447
|
+
&& outcome.process.exit_code === null && outcome.process.signal === null
|
|
448
|
+
&& (outcome.process.reason === null || typeof outcome.process.reason === "string")) {
|
|
449
|
+
return { kind: "cancellation", reason: outcome.process.reason };
|
|
450
|
+
}
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function completionResult(fact) {
|
|
455
|
+
const canonicalMode = stopFact && stopFact.kind === "canonical_stop"
|
|
456
|
+
? canonicalStopMode(stopFact.requested_outcome)
|
|
457
|
+
: null;
|
|
458
|
+
const timeoutStop = stopFact && (stopFact.kind === "timeout" || canonicalMode && canonicalMode.kind === "timeout");
|
|
459
|
+
const cancelledStop = stopFact && (stopFact.kind === "local_cancel" || canonicalMode && canonicalMode.kind === "cancellation");
|
|
460
|
+
const outputStop = stopFact && stopFact.kind === "output_limit";
|
|
461
|
+
return {
|
|
462
|
+
stdout: streamEvidence(config.stdout_path, config.max_output_bytes),
|
|
463
|
+
stderr: streamEvidence(config.stderr_path, config.max_output_bytes),
|
|
464
|
+
max_output_bytes: config.max_output_bytes,
|
|
465
|
+
timeout_ms: config.timeout_ms,
|
|
466
|
+
status: fact.root_exit_observed && !timeoutStop && !cancelledStop && !outputStop ? fact.root_exit_code : null,
|
|
467
|
+
signal: null,
|
|
468
|
+
timed_out: Boolean(timeoutStop),
|
|
469
|
+
cancelled: Boolean(cancelledStop),
|
|
470
|
+
cancellation_reason: cancelledStop
|
|
471
|
+
? (canonicalMode ? canonicalMode.reason : stopFact.reason || null)
|
|
472
|
+
: null,
|
|
473
|
+
process_limit_exceeded: false,
|
|
474
|
+
output_limit_exceeded: Boolean(outputStop),
|
|
475
|
+
termination_reason: timeoutStop ? "timeout" : cancelledStop ? "cancellation" : outputStop ? "output_limit" : null,
|
|
476
|
+
process_id: provider.root_pid,
|
|
477
|
+
process_tree_drained: true,
|
|
478
|
+
completed_at: new Date().toISOString(),
|
|
479
|
+
error: null,
|
|
480
|
+
windows_job_completion: fact,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function finish(fact) {
|
|
485
|
+
if (rawCompletion !== null) throw new Error("keeper emitted duplicate completion evidence");
|
|
486
|
+
if (provider === null || !released) throw new Error("keeper completed before durable release evidence");
|
|
487
|
+
if (fact.reason === "parent_eof" || fact.reason === "protocol_error") {
|
|
488
|
+
throw new Error("keeper completion reported control-channel uncertainty: " + fact.reason);
|
|
489
|
+
}
|
|
490
|
+
if (fact.reason === "output_limit" && stopFact === null) {
|
|
491
|
+
stopFact = { kind: "output_limit", reason: "output_limit" };
|
|
492
|
+
}
|
|
493
|
+
if (stopFact === null) {
|
|
494
|
+
if (!["root_exit", "drain_timeout"].includes(fact.reason) || !fact.root_exit_observed) {
|
|
495
|
+
throw new Error("keeper completion does not prove an unstopped root exit");
|
|
496
|
+
}
|
|
497
|
+
} else if (stopFact.kind === "output_limit" ? fact.reason !== "output_limit" : fact.reason !== "terminate") {
|
|
498
|
+
throw new Error("keeper completion does not match the durable stop fact");
|
|
499
|
+
}
|
|
500
|
+
const candidate = completionResult(fact);
|
|
501
|
+
const next = await persist({ phase: "raw_completed", raw_completion: candidate, stop_fact: stopFact });
|
|
502
|
+
if (next.phase === "unsupported") throw new Error("execution became unsupported before completion commit");
|
|
503
|
+
if (next.phase !== "raw_completed" || canonical(next.raw_completion) !== canonical(candidate)) {
|
|
504
|
+
throw new Error("raw completion lost durable transition ownership");
|
|
505
|
+
}
|
|
506
|
+
rawCompletion = candidate;
|
|
507
|
+
settleCompletion(rawCompletion);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function registerStop(kind, detail) {
|
|
511
|
+
if (rawCompletion !== null || pendingCompletionFact !== null) {
|
|
512
|
+
throw controlConflict("execution already completed before the stop request");
|
|
513
|
+
}
|
|
514
|
+
if (stopFact !== null) {
|
|
515
|
+
const same = stopFact.kind === kind && (kind !== "canonical_stop" || (
|
|
516
|
+
stopFact.stop_idempotency_key === detail.stop_idempotency_key
|
|
517
|
+
&& canonical(stopFact.requested_outcome) === canonical(detail.requested_outcome)
|
|
518
|
+
));
|
|
519
|
+
if (!same) throw controlConflict("execution already has a different terminal fact");
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (kind === "canonical_stop" && canonicalStopMode(detail.requested_outcome) === null) {
|
|
523
|
+
throw controlConflict("canonical stop outcome is unsupported by Windows execution");
|
|
524
|
+
}
|
|
525
|
+
stopFact = kind === "canonical_stop"
|
|
526
|
+
? {
|
|
527
|
+
kind,
|
|
528
|
+
stop_idempotency_key: detail.stop_idempotency_key,
|
|
529
|
+
requested_at: detail.requested_at,
|
|
530
|
+
requested_outcome: detail.requested_outcome,
|
|
531
|
+
}
|
|
532
|
+
: { kind, reason: detail.reason || null };
|
|
533
|
+
const next = await persist({ stop_fact: stopFact });
|
|
534
|
+
if (next.phase !== "running" || canonical(next.stop_fact) !== canonical(stopFact)) {
|
|
535
|
+
stopFact = null;
|
|
536
|
+
throw controlConflict("execution completion won before the stop request");
|
|
537
|
+
}
|
|
538
|
+
sendTerminateIfReleased();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async function handleFact(line) {
|
|
542
|
+
const fact = parseFact(line);
|
|
543
|
+
if (fact.kind === "contained_ready") {
|
|
544
|
+
if (provider !== null || helper === null || fact.keeper_pid !== helper.pid) {
|
|
545
|
+
throw new Error("keeper containment identity conflicts with launched helper");
|
|
546
|
+
}
|
|
547
|
+
provider = {
|
|
548
|
+
pid: fact.root_pid,
|
|
549
|
+
keeper_pid: fact.keeper_pid,
|
|
550
|
+
keeper_creation_filetime: fact.keeper_creation_filetime,
|
|
551
|
+
root_pid: fact.root_pid,
|
|
552
|
+
root_creation_filetime: fact.root_creation_filetime,
|
|
553
|
+
job_identifier: fact.job_identifier,
|
|
554
|
+
containment_nonce: config.containment_nonce,
|
|
555
|
+
contained_at: new Date().toISOString(),
|
|
556
|
+
released_at: null,
|
|
557
|
+
};
|
|
558
|
+
const next = await persist({ provider });
|
|
559
|
+
if (canonical(next.provider) !== canonical(provider)) throw new Error("containment identity was not durably committed");
|
|
560
|
+
if (!helper.stdin || helper.stdin.destroyed) throw new Error("Windows Job keeper release channel is unavailable");
|
|
561
|
+
releaseSent = true;
|
|
562
|
+
helper.stdin.write("release\n");
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (fact.kind === "released") {
|
|
566
|
+
if (provider === null || !releaseSent || released || fact.root_pid !== provider.root_pid) {
|
|
567
|
+
throw new Error("keeper release identity conflicts with containment evidence");
|
|
568
|
+
}
|
|
569
|
+
provider = { ...provider, released_at: new Date().toISOString() };
|
|
570
|
+
const next = await persist({ provider });
|
|
571
|
+
if (canonical(next.provider) !== canonical(provider)) throw new Error("release identity was not durably committed");
|
|
572
|
+
released = true;
|
|
573
|
+
sendTerminateIfReleased();
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (pendingCompletionFact !== null) throw new Error("keeper emitted duplicate completion evidence");
|
|
577
|
+
if (provider === null || !released) throw new Error("keeper completed before durable release evidence");
|
|
578
|
+
pendingCompletionFact = fact;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
let startupReadyResolve;
|
|
582
|
+
const startupReady = new Promise((resolve) => { startupReadyResolve = resolve; });
|
|
583
|
+
const activeControls = new Set();
|
|
584
|
+
function respond(socket, value) {
|
|
585
|
+
return new Promise((resolve) => {
|
|
586
|
+
if (socket.destroyed) {
|
|
587
|
+
resolve();
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
socket.once("close", resolve);
|
|
591
|
+
socket.end(JSON.stringify(value) + "\n");
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
const server = net.createServer({ allowHalfOpen: true }, (socket) => {
|
|
595
|
+
let resolveControl;
|
|
596
|
+
const control = new Promise((resolve) => { resolveControl = resolve; });
|
|
597
|
+
socket.once("close", resolveControl);
|
|
598
|
+
socket.on("error", () => undefined);
|
|
599
|
+
socket.setEncoding("utf8");
|
|
600
|
+
let text = "";
|
|
601
|
+
socket.on("data", (chunk) => {
|
|
602
|
+
if (handled) {
|
|
603
|
+
socket.destroy(new Error("execution control request has trailing bytes"));
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
text += chunk;
|
|
607
|
+
if (Buffer.byteLength(text, "utf8") > 1024 * 1024) {
|
|
608
|
+
socket.destroy(new Error("control request too large"));
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
handleRequest();
|
|
612
|
+
});
|
|
613
|
+
let handled = false;
|
|
614
|
+
function handleRequest() {
|
|
615
|
+
if (handled) return;
|
|
616
|
+
const newline = text.indexOf("\n");
|
|
617
|
+
if (newline < 0) return;
|
|
618
|
+
handled = true;
|
|
619
|
+
const requestText = text.slice(0, newline);
|
|
620
|
+
if (text.slice(newline + 1).length !== 0) {
|
|
621
|
+
socket.destroy(new Error("execution control request has trailing bytes"));
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
void (async () => {
|
|
625
|
+
const request = JSON.parse(requestText);
|
|
626
|
+
const unauthenticated = request.schema_version !== CONTROL_SCHEMA
|
|
627
|
+
|| request.execution_instance_id !== config.execution_instance_id
|
|
628
|
+
|| request.challenge !== config.challenge
|
|
629
|
+
|| request.containment_nonce !== config.containment_nonce;
|
|
630
|
+
if (unauthenticated) {
|
|
631
|
+
throw new Error("unauthenticated execution control request");
|
|
632
|
+
}
|
|
633
|
+
const expectedKeys = request.action === "probe"
|
|
634
|
+
? ["schema_version", "execution_instance_id", "challenge", "containment_nonce", "action"]
|
|
635
|
+
: request.action === "stop" && request.requested_outcome
|
|
636
|
+
? ["schema_version", "execution_instance_id", "challenge", "containment_nonce", "action", "stop_idempotency_key", "requested_at", "requested_outcome"]
|
|
637
|
+
: request.action === "stop"
|
|
638
|
+
? ["schema_version", "execution_instance_id", "challenge", "containment_nonce", "action", "reason"]
|
|
639
|
+
: [];
|
|
640
|
+
if (expectedKeys.length === 0 || !exactKeys(request, expectedKeys)) {
|
|
641
|
+
throw new Error("execution control request has an invalid schema");
|
|
642
|
+
}
|
|
643
|
+
if (request.action === "stop" && request.requested_outcome
|
|
644
|
+
&& (!digest(request.stop_idempotency_key)
|
|
645
|
+
|| typeof request.requested_at !== "string"
|
|
646
|
+
|| !Number.isFinite(Date.parse(request.requested_at))
|
|
647
|
+
|| new Date(Date.parse(request.requested_at)).toISOString() !== request.requested_at
|
|
648
|
+
|| canonicalStopMode(request.requested_outcome) === null)) {
|
|
649
|
+
throw new Error("canonical stop request is invalid");
|
|
650
|
+
}
|
|
651
|
+
if (request.action === "stop" && !request.requested_outcome
|
|
652
|
+
&& request.reason !== null && typeof request.reason !== "string") {
|
|
653
|
+
throw new Error("local cancellation request is invalid");
|
|
654
|
+
}
|
|
655
|
+
await startupReady;
|
|
656
|
+
if (request.action === "probe") {
|
|
657
|
+
const identity = await enqueue(async () => {
|
|
658
|
+
const current = readRecord();
|
|
659
|
+
return {
|
|
660
|
+
record_revision: current.revision,
|
|
661
|
+
execution_instance_id: config.execution_instance_id,
|
|
662
|
+
supervisor_pid: process.pid,
|
|
663
|
+
supervisor_ready_at: supervisorReadyAt,
|
|
664
|
+
containment_nonce: config.containment_nonce,
|
|
665
|
+
backend: prepared.backend,
|
|
666
|
+
provider,
|
|
667
|
+
};
|
|
668
|
+
});
|
|
669
|
+
await respond(socket, { ok: true, identity });
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
await enqueue(() => request.requested_outcome
|
|
673
|
+
? registerStop("canonical_stop", request)
|
|
674
|
+
: registerStop("local_cancel", request));
|
|
675
|
+
const result = await completion;
|
|
676
|
+
if (result === null) throw new Error("Windows execution became unsupported while stopping");
|
|
677
|
+
await respond(socket, { ok: true, raw_completion: result });
|
|
678
|
+
})()
|
|
679
|
+
.catch((error) => respond(socket, { ok: false, error: error.message }));
|
|
680
|
+
}
|
|
681
|
+
socket.on("end", () => {
|
|
682
|
+
if (!handled) void respond(socket, { ok: false, error: "execution control request was incomplete" });
|
|
683
|
+
});
|
|
684
|
+
activeControls.add(control);
|
|
685
|
+
void control.finally(() => activeControls.delete(control));
|
|
686
|
+
});
|
|
687
|
+
await new Promise((resolve, reject) => {
|
|
688
|
+
server.once("error", reject);
|
|
689
|
+
server.listen({ path: config.control_endpoint, readableAll: false, writableAll: false }, () => {
|
|
690
|
+
server.off("error", reject);
|
|
691
|
+
resolve();
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
supervisorReadyAt = new Date().toISOString();
|
|
695
|
+
await persist({
|
|
696
|
+
phase: "running",
|
|
697
|
+
supervisor: { ...prepared.supervisor, pid: process.pid, ready_at: supervisorReadyAt },
|
|
698
|
+
});
|
|
699
|
+
startupReadyResolve();
|
|
700
|
+
|
|
701
|
+
try {
|
|
702
|
+
if (config.stdin_path !== null) {
|
|
703
|
+
fs.writeFileSync(config.stdin_path, config.stdin, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
704
|
+
}
|
|
705
|
+
const encodedConfig = encodeConfig(config);
|
|
706
|
+
validateHelper();
|
|
707
|
+
helper = spawn(config.helper_path, [], {
|
|
708
|
+
detached: false,
|
|
709
|
+
windowsHide: true,
|
|
710
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
711
|
+
});
|
|
712
|
+
helper.stdout.setEncoding("utf8");
|
|
713
|
+
helper.stderr.setEncoding("utf8");
|
|
714
|
+
helper.stderr.on("data", (chunk) => {
|
|
715
|
+
helperErrors += chunk;
|
|
716
|
+
if (Buffer.byteLength(helperErrors, "utf8") > 1024 * 1024) helperErrors = helperErrors.slice(-1024 * 1024);
|
|
717
|
+
});
|
|
718
|
+
helper.stdout.on("data", (chunk) => {
|
|
719
|
+
helperFacts += chunk;
|
|
720
|
+
if (Buffer.byteLength(helperFacts, "utf8") > 1024 * 1024) {
|
|
721
|
+
enqueueObserved(() => { throw new Error("keeper fact stream exceeded the protocol limit"); });
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
for (;;) {
|
|
725
|
+
const newline = helperFacts.indexOf("\n");
|
|
726
|
+
if (newline < 0) break;
|
|
727
|
+
const line = helperFacts.slice(0, newline);
|
|
728
|
+
helperFacts = helperFacts.slice(newline + 1);
|
|
729
|
+
if (line.length === 0) continue;
|
|
730
|
+
enqueueObserved(() => handleFact(line));
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
helper.once("error", (error) => { enqueueObserved(() => { throw error; }); });
|
|
734
|
+
helper.once("close", (status) => {
|
|
735
|
+
enqueueObserved(() => {
|
|
736
|
+
if (helperFacts.length !== 0) throw new Error("Windows Job keeper ended with a partial fact");
|
|
737
|
+
if (helperErrors.trim().length !== 0) {
|
|
738
|
+
throw new Error("Windows Job keeper reported an error: " + helperErrors.trim());
|
|
739
|
+
}
|
|
740
|
+
if (status !== 0) {
|
|
741
|
+
throw new Error("Windows Job keeper exited without success" + (status === null ? "" : " (exit " + status + ")"));
|
|
742
|
+
}
|
|
743
|
+
if (pendingCompletionFact === null) throw new Error("Windows Job keeper exited without trusted completion evidence");
|
|
744
|
+
return finish(pendingCompletionFact);
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
helper.stdin.on("error", (error) => {
|
|
748
|
+
enqueueObserved(() => {
|
|
749
|
+
if (rawCompletion === null) throw error;
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
helper.stdin.write(encodedConfig);
|
|
753
|
+
if (config.timeout_ms !== null) {
|
|
754
|
+
timeout = setTimeout(() => {
|
|
755
|
+
enqueueObserved(() => registerStop("timeout", { reason: "timeout" }));
|
|
756
|
+
}, config.timeout_ms);
|
|
757
|
+
}
|
|
758
|
+
await completion;
|
|
759
|
+
} catch (error) {
|
|
760
|
+
await enqueue(() => { throw error; }).catch(() => undefined);
|
|
761
|
+
await completion;
|
|
762
|
+
} finally {
|
|
763
|
+
if (timeout !== null) clearTimeout(timeout);
|
|
764
|
+
await new Promise((resolve) => server.close(resolve));
|
|
765
|
+
await Promise.allSettled([...activeControls]);
|
|
766
|
+
if (config.stdin_path !== null) {
|
|
767
|
+
try { fs.unlinkSync(config.stdin_path); } catch {}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
main().catch((error) => {
|
|
773
|
+
void withRecordLock(async () => {
|
|
774
|
+
const current = readRecord();
|
|
775
|
+
if (["never_launched", "raw_completed", "normalized_completed", "receipted", "unsupported"].includes(current.phase)) return;
|
|
776
|
+
atomicWrite(recordPath, {
|
|
777
|
+
...current,
|
|
778
|
+
phase: "unsupported",
|
|
779
|
+
supervisor_error: error.message,
|
|
780
|
+
revision: current.revision + 1,
|
|
781
|
+
updated_at: new Date().toISOString(),
|
|
782
|
+
});
|
|
783
|
+
}).catch(() => {}).finally(() => process.exit(1));
|
|
784
|
+
});
|
|
785
|
+
`;
|
|
786
|
+
export const LOCAL_WINDOWS_EXECUTION_SUPERVISOR_DIGEST = `sha256:${createHash("sha256")
|
|
787
|
+
.update(LOCAL_WINDOWS_EXECUTION_SUPERVISOR_SOURCE, "utf8")
|
|
788
|
+
.digest("hex")}`;
|