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,793 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Self-contained CommonJS supervisor written into the private authority root.
|
|
4
|
+
* Keeping the child runtime dependency-free lets source tests and compiled CLI
|
|
5
|
+
* builds use the exact same protocol.
|
|
6
|
+
*/
|
|
7
|
+
export const LOCAL_EXECUTION_SUPERVISOR_SOURCE = String.raw `"use strict";
|
|
8
|
+
const { spawn, spawnSync } = require("node:child_process");
|
|
9
|
+
const { createHash, randomUUID } = require("node:crypto");
|
|
10
|
+
const fs = require("node:fs");
|
|
11
|
+
const net = require("node:net");
|
|
12
|
+
const path = require("node:path");
|
|
13
|
+
|
|
14
|
+
const recordPath = process.argv[2];
|
|
15
|
+
if (!recordPath) process.exit(64);
|
|
16
|
+
let ownedSocketPath = null;
|
|
17
|
+
|
|
18
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
|
+
const isErrno = (error, code) => error && error.code === code;
|
|
20
|
+
|
|
21
|
+
function atomicWrite(target, value) {
|
|
22
|
+
const directory = path.dirname(target);
|
|
23
|
+
const temporary = path.join(directory, "." + path.basename(target) + ".tmp." + process.pid + "." + randomUUID());
|
|
24
|
+
const fd = fs.openSync(temporary, "wx", 0o600);
|
|
25
|
+
try {
|
|
26
|
+
fs.writeFileSync(fd, JSON.stringify(value) + "\n", "utf8");
|
|
27
|
+
fs.fsyncSync(fd);
|
|
28
|
+
} finally {
|
|
29
|
+
fs.closeSync(fd);
|
|
30
|
+
}
|
|
31
|
+
fs.renameSync(temporary, target);
|
|
32
|
+
const directoryFd = fs.openSync(directory, "r");
|
|
33
|
+
try { fs.fsyncSync(directoryFd); } finally { fs.closeSync(directoryFd); }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readRecord() {
|
|
37
|
+
return JSON.parse(fs.readFileSync(recordPath, "utf8"));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function syncDirectory(target) {
|
|
41
|
+
const descriptor = fs.openSync(target, "r");
|
|
42
|
+
try { fs.fsyncSync(descriptor); } finally { fs.closeSync(descriptor); }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readLockOwner(target) {
|
|
46
|
+
try {
|
|
47
|
+
const value = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
48
|
+
return value && value.schema_version === "intentdna.execution_lock_owner.v1"
|
|
49
|
+
&& Number.isSafeInteger(value.pid) && value.pid > 0
|
|
50
|
+
&& typeof value.process_birth_id === "string" && value.process_birth_id.length > 0
|
|
51
|
+
&& typeof value.generation === "string" && value.generation.length > 0
|
|
52
|
+
&& Number.isSafeInteger(value.acquired_at) && value.acquired_at >= 0
|
|
53
|
+
&& Object.keys(value).sort().join(",") === "acquired_at,generation,pid,process_birth_id,schema_version"
|
|
54
|
+
? value
|
|
55
|
+
: null;
|
|
56
|
+
} catch { return null; }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function lockOwnersEqual(left, right) {
|
|
60
|
+
return left !== null
|
|
61
|
+
&& left.schema_version === right.schema_version
|
|
62
|
+
&& left.pid === right.pid
|
|
63
|
+
&& left.process_birth_id === right.process_birth_id
|
|
64
|
+
&& left.generation === right.generation
|
|
65
|
+
&& left.acquired_at === right.acquired_at;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function lockOwnerDigest(owner) {
|
|
69
|
+
return createHash("sha256").update(JSON.stringify({
|
|
70
|
+
acquired_at: owner.acquired_at,
|
|
71
|
+
generation: owner.generation,
|
|
72
|
+
pid: owner.pid,
|
|
73
|
+
process_birth_id: owner.process_birth_id,
|
|
74
|
+
schema_version: owner.schema_version,
|
|
75
|
+
}), "utf8").digest("hex");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function guardianMain() {
|
|
79
|
+
const { spawn } = require("node:child_process");
|
|
80
|
+
const fs = require("node:fs");
|
|
81
|
+
const config = JSON.parse(Buffer.from(process.argv[process.argv.length - 1], "base64").toString("utf8"));
|
|
82
|
+
if (config.kill_signal !== "SIGKILL" && config.kill_signal !== "SIGSTOP") {
|
|
83
|
+
process.on(config.kill_signal, () => {});
|
|
84
|
+
}
|
|
85
|
+
const control = require("node:fs").createWriteStream(null, { fd: 3 });
|
|
86
|
+
const actual = spawn("/bin/sh", [
|
|
87
|
+
"-c",
|
|
88
|
+
"gate=$1; shift; while [ ! -f \"$gate\" ]; do sleep 0.01; done; exec \"$@\"",
|
|
89
|
+
"intentdna-provider-gate",
|
|
90
|
+
config.gate_path,
|
|
91
|
+
config.command,
|
|
92
|
+
...config.args,
|
|
93
|
+
], {
|
|
94
|
+
cwd: config.cwd,
|
|
95
|
+
env: config.env,
|
|
96
|
+
detached: false,
|
|
97
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
98
|
+
windowsHide: true,
|
|
99
|
+
});
|
|
100
|
+
const publish = (value) => control.write(JSON.stringify(value) + "\n");
|
|
101
|
+
let outputLimited = false;
|
|
102
|
+
const forward = (stream, descriptor) => {
|
|
103
|
+
let written = 0;
|
|
104
|
+
stream.on("data", (chunk) => {
|
|
105
|
+
if (outputLimited) return;
|
|
106
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
107
|
+
const remaining = Math.max(0, config.max_output_bytes - written);
|
|
108
|
+
if (bytes.byteLength > remaining) {
|
|
109
|
+
if (remaining > 0) fs.writeSync(descriptor, bytes.subarray(0, remaining));
|
|
110
|
+
written += remaining;
|
|
111
|
+
outputLimited = true;
|
|
112
|
+
publish({ type: "output_limit" });
|
|
113
|
+
actual.stdout.pause();
|
|
114
|
+
actual.stderr.pause();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (bytes.byteLength > 0) fs.writeSync(descriptor, bytes);
|
|
118
|
+
written += bytes.byteLength;
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
forward(actual.stdout, 1);
|
|
122
|
+
forward(actual.stderr, 2);
|
|
123
|
+
actual.stdin.on("error", () => {});
|
|
124
|
+
actual.stdin.end(config.stdin === null ? undefined : config.stdin);
|
|
125
|
+
publish({ type: "ready", pid: actual.pid || null });
|
|
126
|
+
actual.once("error", (error) => publish({
|
|
127
|
+
type: "completed",
|
|
128
|
+
status: null,
|
|
129
|
+
signal: null,
|
|
130
|
+
error: { code: error.code || "UNKNOWN", message: error.message },
|
|
131
|
+
}));
|
|
132
|
+
actual.once("exit", (status, signal) => publish({ type: "completed", status, signal, error: null }));
|
|
133
|
+
process.stdin.resume();
|
|
134
|
+
process.stdin.setEncoding("utf8");
|
|
135
|
+
let command = "";
|
|
136
|
+
let released = false;
|
|
137
|
+
const killOrphanedGroup = () => {
|
|
138
|
+
if (released) return;
|
|
139
|
+
try { process.kill(-process.pid, "SIGKILL"); } catch (error) {
|
|
140
|
+
if (!error || error.code !== "ESRCH") process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
process.stdin.on("data", (chunk) => {
|
|
144
|
+
command += chunk;
|
|
145
|
+
if (command.includes("release\n")) {
|
|
146
|
+
released = true;
|
|
147
|
+
process.exit(0);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
process.stdin.once("end", killOrphanedGroup);
|
|
151
|
+
process.stdin.once("close", killOrphanedGroup);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const guardianSource = "(" + guardianMain.toString() + ")()";
|
|
155
|
+
|
|
156
|
+
async function withRecordLock(operation) {
|
|
157
|
+
const lockPath = recordPath + ".lock";
|
|
158
|
+
const ownerPath = path.join(lockPath, "owner.json");
|
|
159
|
+
const processBirthIdentity = (pid) => {
|
|
160
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return null;
|
|
161
|
+
if (process.platform === "linux") {
|
|
162
|
+
try {
|
|
163
|
+
const stat = fs.readFileSync("/proc/" + pid + "/stat", "utf8");
|
|
164
|
+
const fields = stat.slice(stat.lastIndexOf(")") + 2).trim().split(/\s+/);
|
|
165
|
+
return fields[19] && /^[0-9]+$/.test(fields[19]) ? "linux-start-ticks:" + fields[19] : null;
|
|
166
|
+
} catch { return null; }
|
|
167
|
+
}
|
|
168
|
+
const result = spawnSync("ps", ["-p", String(pid), "-o", "lstart="], { encoding: "utf8", timeout: 1000 });
|
|
169
|
+
const started = result.status === 0 ? result.stdout.trim().replace(/\s+/g, " ") : "";
|
|
170
|
+
return started === "" ? null : process.platform + "-lstart:" + started;
|
|
171
|
+
};
|
|
172
|
+
const owner = {
|
|
173
|
+
schema_version: "intentdna.execution_lock_owner.v1",
|
|
174
|
+
pid: process.pid,
|
|
175
|
+
process_birth_id: processBirthIdentity(process.pid),
|
|
176
|
+
generation: randomUUID(),
|
|
177
|
+
acquired_at: Date.now(),
|
|
178
|
+
};
|
|
179
|
+
if (owner.process_birth_id === null) throw new Error("cannot establish execution lock owner identity");
|
|
180
|
+
const deadline = Date.now() + 5000;
|
|
181
|
+
for (;;) {
|
|
182
|
+
const candidatePath = lockPath + ".candidate." + process.pid + "." + randomUUID();
|
|
183
|
+
try {
|
|
184
|
+
fs.mkdirSync(candidatePath, { mode: 0o700 });
|
|
185
|
+
const descriptor = fs.openSync(path.join(candidatePath, "owner.json"), "wx", 0o600);
|
|
186
|
+
try {
|
|
187
|
+
fs.writeFileSync(descriptor, JSON.stringify(owner) + "\n", "utf8");
|
|
188
|
+
fs.fsyncSync(descriptor);
|
|
189
|
+
} finally { fs.closeSync(descriptor); }
|
|
190
|
+
const candidateDescriptor = fs.openSync(candidatePath, "r");
|
|
191
|
+
try { fs.fsyncSync(candidateDescriptor); } finally { fs.closeSync(candidateDescriptor); }
|
|
192
|
+
fs.renameSync(candidatePath, lockPath);
|
|
193
|
+
syncDirectory(path.dirname(lockPath));
|
|
194
|
+
break;
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (!isErrno(error, "EEXIST") && !isErrno(error, "ENOTEMPTY")
|
|
197
|
+
&& !(isErrno(error, "EPERM") && fs.existsSync(lockPath))) throw error;
|
|
198
|
+
const current = readLockOwner(ownerPath);
|
|
199
|
+
let stale = false;
|
|
200
|
+
if (current) {
|
|
201
|
+
let alive = true;
|
|
202
|
+
try { process.kill(current.pid, 0); } catch (probeError) { alive = probeError.code === "EPERM"; }
|
|
203
|
+
const actualBirthIdentity = alive ? processBirthIdentity(current.pid) : null;
|
|
204
|
+
stale = !alive || (actualBirthIdentity !== null && actualBirthIdentity !== current.process_birth_id);
|
|
205
|
+
}
|
|
206
|
+
if (stale) {
|
|
207
|
+
try {
|
|
208
|
+
const quarantine = lockPath + ".stale." + lockOwnerDigest(current);
|
|
209
|
+
fs.renameSync(lockPath, quarantine);
|
|
210
|
+
syncDirectory(path.dirname(lockPath));
|
|
211
|
+
if (!lockOwnersEqual(readLockOwner(path.join(quarantine, "owner.json")), current)) {
|
|
212
|
+
throw new Error("execution record lock ownership changed during stale takeover");
|
|
213
|
+
}
|
|
214
|
+
continue;
|
|
215
|
+
} catch (takeoverError) {
|
|
216
|
+
const quarantine = lockPath + ".stale." + lockOwnerDigest(current);
|
|
217
|
+
if (!isErrno(takeoverError, "ENOENT")
|
|
218
|
+
&& !isErrno(takeoverError, "EEXIST")
|
|
219
|
+
&& !isErrno(takeoverError, "ENOTEMPTY")
|
|
220
|
+
&& !((isErrno(takeoverError, "EPERM") || isErrno(takeoverError, "EACCES")) && fs.existsSync(quarantine))) {
|
|
221
|
+
throw takeoverError;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (Date.now() >= deadline) throw new Error("execution record lock is unavailable");
|
|
226
|
+
await sleep(10);
|
|
227
|
+
} finally {
|
|
228
|
+
fs.rmSync(candidatePath, { recursive: true, force: true });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
return await operation();
|
|
233
|
+
} finally {
|
|
234
|
+
try {
|
|
235
|
+
const current = readLockOwner(ownerPath);
|
|
236
|
+
if (lockOwnersEqual(current, owner)) {
|
|
237
|
+
const released = lockPath + ".released." + lockOwnerDigest(owner) + "." + randomUUID();
|
|
238
|
+
let retired = true;
|
|
239
|
+
try { fs.renameSync(lockPath, released); }
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (isErrno(error, "ENOENT")) retired = false;
|
|
242
|
+
else throw error;
|
|
243
|
+
}
|
|
244
|
+
if (retired) {
|
|
245
|
+
syncDirectory(path.dirname(lockPath));
|
|
246
|
+
if (!lockOwnersEqual(readLockOwner(path.join(released, "owner.json")), owner)) {
|
|
247
|
+
throw new Error("execution record lock ownership changed during release");
|
|
248
|
+
}
|
|
249
|
+
fs.rmSync(released, { recursive: true, force: true });
|
|
250
|
+
syncDirectory(path.dirname(lockPath));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
254
|
+
if (!isErrno(error, "ENOENT")) throw error;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function processGroupMembers(processId) {
|
|
260
|
+
const result = spawnSync("ps", ["-axo", "pid=,pgid=,state="], { encoding: "utf8" });
|
|
261
|
+
if (result.status !== 0) return null;
|
|
262
|
+
return result.stdout.split("\n").flatMap((line) => {
|
|
263
|
+
const [pidText, pgidText, state] = line.trim().split(/\s+/);
|
|
264
|
+
const pid = Number(pidText);
|
|
265
|
+
const pgid = Number(pgidText);
|
|
266
|
+
return Number.isSafeInteger(pid) && pgid === processId && state !== "Z" ? [pid] : [];
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function signalProcessGroup(processId, signal) {
|
|
271
|
+
try { process.kill(-processId, signal); return true; }
|
|
272
|
+
catch (error) {
|
|
273
|
+
if (error.code === "ESRCH") return false;
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function waitForGuardianExit(guardian, timeoutMs) {
|
|
279
|
+
if (guardian.exitCode !== null || guardian.signalCode !== null) return true;
|
|
280
|
+
return new Promise((resolve) => {
|
|
281
|
+
const exited = () => { clearTimeout(timer); resolve(true); };
|
|
282
|
+
const timer = setTimeout(() => { guardian.off("exit", exited); resolve(false); }, timeoutMs);
|
|
283
|
+
guardian.once("exit", exited);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function waitForEmptyProcessGroup(processId, timeoutMs) {
|
|
288
|
+
const deadline = Date.now() + timeoutMs;
|
|
289
|
+
let consecutiveEmptyObservations = 0;
|
|
290
|
+
for (;;) {
|
|
291
|
+
const members = processGroupMembers(processId);
|
|
292
|
+
if (members !== null && members.length === 0) {
|
|
293
|
+
consecutiveEmptyObservations += 1;
|
|
294
|
+
if (consecutiveEmptyObservations >= 3) return true;
|
|
295
|
+
} else {
|
|
296
|
+
consecutiveEmptyObservations = 0;
|
|
297
|
+
}
|
|
298
|
+
if (members === null || Date.now() >= deadline) return false;
|
|
299
|
+
await sleep(10);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function forceDrainProcessGroup(processId, timeoutMs) {
|
|
304
|
+
const deadline = Date.now() + timeoutMs;
|
|
305
|
+
let consecutiveEmptyObservations = 0;
|
|
306
|
+
for (;;) {
|
|
307
|
+
const members = processGroupMembers(processId);
|
|
308
|
+
if (members === null) return false;
|
|
309
|
+
if (members.length === 0) {
|
|
310
|
+
consecutiveEmptyObservations += 1;
|
|
311
|
+
if (consecutiveEmptyObservations >= 3) return true;
|
|
312
|
+
} else {
|
|
313
|
+
consecutiveEmptyObservations = 0;
|
|
314
|
+
for (const pid of members) {
|
|
315
|
+
try { process.kill(pid, "SIGKILL"); }
|
|
316
|
+
catch (error) { if (error.code !== "ESRCH") throw error; }
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (Date.now() >= deadline) return false;
|
|
320
|
+
await sleep(10);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function drainProcessGroup(guardian, initialSignal, graceMs) {
|
|
325
|
+
const processId = guardian && guardian.pid ? guardian.pid : null;
|
|
326
|
+
if (processId === null) return true;
|
|
327
|
+
signalProcessGroup(processId, initialSignal);
|
|
328
|
+
let deadline = Date.now() + graceMs;
|
|
329
|
+
for (;;) {
|
|
330
|
+
const members = processGroupMembers(processId);
|
|
331
|
+
if (members !== null && members.every((pid) => pid === processId)) {
|
|
332
|
+
guardian.stdin.end("release\n");
|
|
333
|
+
if (!await waitForGuardianExit(guardian, Math.max(1_000, graceMs))) return false;
|
|
334
|
+
return waitForEmptyProcessGroup(processId, Math.max(1_000, graceMs));
|
|
335
|
+
}
|
|
336
|
+
if (members === null || Date.now() >= deadline) break;
|
|
337
|
+
await sleep(10);
|
|
338
|
+
}
|
|
339
|
+
signalProcessGroup(processId, "SIGKILL");
|
|
340
|
+
if (!await waitForGuardianExit(guardian, Math.max(1_000, graceMs))) return false;
|
|
341
|
+
return forceDrainProcessGroup(processId, Math.max(1_000, graceMs));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function signalKnownGroupMembers(processId, signal) {
|
|
345
|
+
const members = processGroupMembers(processId);
|
|
346
|
+
if (members === null) return false;
|
|
347
|
+
for (const pid of members) {
|
|
348
|
+
try { process.kill(pid, signal); }
|
|
349
|
+
catch (error) { if (error.code !== "ESRCH") throw error; }
|
|
350
|
+
}
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function drainExitedProcessGroup(processId, initialSignal, graceMs) {
|
|
355
|
+
if (processId === null) return true;
|
|
356
|
+
if (!signalKnownGroupMembers(processId, initialSignal)) return false;
|
|
357
|
+
let deadline = Date.now() + graceMs;
|
|
358
|
+
for (;;) {
|
|
359
|
+
const members = processGroupMembers(processId);
|
|
360
|
+
if (members !== null && members.length === 0) return true;
|
|
361
|
+
if (members === null || Date.now() >= deadline) break;
|
|
362
|
+
await sleep(10);
|
|
363
|
+
}
|
|
364
|
+
if (!signalKnownGroupMembers(processId, "SIGKILL")) return false;
|
|
365
|
+
return forceDrainProcessGroup(processId, Math.max(1_000, graceMs));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function readConfiguration() {
|
|
369
|
+
let text = "";
|
|
370
|
+
for await (const chunk of process.stdin) text += chunk;
|
|
371
|
+
return JSON.parse(text);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function streamEvidence(target, maxOutputBytes) {
|
|
375
|
+
const metadata = fs.lstatSync(target, { bigint: true });
|
|
376
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > BigInt(maxOutputBytes)) {
|
|
377
|
+
throw new Error("provider stream does not satisfy the durable output boundary");
|
|
378
|
+
}
|
|
379
|
+
const bytes = fs.readFileSync(target);
|
|
380
|
+
if (BigInt(bytes.byteLength) !== metadata.size) {
|
|
381
|
+
throw new Error("provider stream changed while completion evidence was captured");
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
path: target,
|
|
385
|
+
byte_count: bytes.byteLength,
|
|
386
|
+
sha256: "sha256:" + createHash("sha256").update(bytes).digest("hex"),
|
|
387
|
+
device: metadata.dev.toString(),
|
|
388
|
+
inode: metadata.ino.toString(),
|
|
389
|
+
birthtime_ns: metadata.birthtimeNs.toString(),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function main() {
|
|
394
|
+
const config = await readConfiguration();
|
|
395
|
+
const prepared = readRecord();
|
|
396
|
+
if (
|
|
397
|
+
prepared.schema_version !== "intentdna.local_execution_record.v1"
|
|
398
|
+
|| prepared.phase !== "launching"
|
|
399
|
+
|| prepared.execution_instance_id !== config.execution_instance_id
|
|
400
|
+
|| prepared.challenge !== config.challenge
|
|
401
|
+
|| prepared.supervisor.socket_path !== config.socket_path
|
|
402
|
+
) throw new Error("prepared execution record does not match supervisor configuration");
|
|
403
|
+
if (!Number.isSafeInteger(config.max_output_bytes) || config.max_output_bytes <= 0
|
|
404
|
+
|| (config.timeout_ms !== null && (!Number.isSafeInteger(config.timeout_ms) || config.timeout_ms <= 0))) {
|
|
405
|
+
throw new Error("execution supervisor configuration is invalid");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
try { fs.unlinkSync(config.socket_path); } catch (error) { if (!isErrno(error, "ENOENT")) throw error; }
|
|
409
|
+
let provider = null;
|
|
410
|
+
let providerCompletion = null;
|
|
411
|
+
let stopFact = null;
|
|
412
|
+
let rawCompletion = null;
|
|
413
|
+
let completeRaw;
|
|
414
|
+
let timeout = null;
|
|
415
|
+
let setupComplete = false;
|
|
416
|
+
let providerChildPid = null;
|
|
417
|
+
let stdoutFd = null;
|
|
418
|
+
let stderrFd = null;
|
|
419
|
+
const rawCompleted = new Promise((resolve) => { completeRaw = resolve; });
|
|
420
|
+
|
|
421
|
+
async function persist(patch) {
|
|
422
|
+
return withRecordLock(async () => {
|
|
423
|
+
const current = readRecord();
|
|
424
|
+
if (current.execution_instance_id !== config.execution_instance_id || current.challenge !== config.challenge) {
|
|
425
|
+
throw new Error("execution record identity changed");
|
|
426
|
+
}
|
|
427
|
+
if (current.phase === "never_launched") throw new Error("execution was durably attested as never launched");
|
|
428
|
+
const terminal = ["raw_completed", "normalized_completed", "receipted"];
|
|
429
|
+
if (terminal.includes(current.phase) && patch.phase && patch.phase !== current.phase) {
|
|
430
|
+
if (patch.phase === "unsupported") return current;
|
|
431
|
+
throw new Error("terminal execution record cannot transition to " + patch.phase);
|
|
432
|
+
}
|
|
433
|
+
if (current.phase === "unsupported" && patch.phase && patch.phase !== "unsupported") {
|
|
434
|
+
throw new Error("unsupported execution record is terminal");
|
|
435
|
+
}
|
|
436
|
+
if (patch.phase === "running" && current.phase !== "launching") {
|
|
437
|
+
throw new Error("execution no longer owns the durable launch claim");
|
|
438
|
+
}
|
|
439
|
+
if (patch.phase === "raw_completed" && current.phase !== "running") {
|
|
440
|
+
throw new Error("raw completion requires running launch ownership");
|
|
441
|
+
}
|
|
442
|
+
const next = { ...current, ...patch, revision: current.revision + 1, updated_at: new Date().toISOString() };
|
|
443
|
+
atomicWrite(recordPath, next);
|
|
444
|
+
return next;
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function finishRaw(value, release = true) {
|
|
449
|
+
if (rawCompletion !== null) return rawCompletion;
|
|
450
|
+
if (stdoutFd !== null) { fs.closeSync(stdoutFd); stdoutFd = null; }
|
|
451
|
+
if (stderrFd !== null) { fs.closeSync(stderrFd); stderrFd = null; }
|
|
452
|
+
rawCompletion = {
|
|
453
|
+
stdout: streamEvidence(config.stdout_path, config.max_output_bytes),
|
|
454
|
+
stderr: streamEvidence(config.stderr_path, config.max_output_bytes),
|
|
455
|
+
max_output_bytes: config.max_output_bytes,
|
|
456
|
+
timeout_ms: config.timeout_ms,
|
|
457
|
+
...value,
|
|
458
|
+
};
|
|
459
|
+
await persist({ phase: "raw_completed", raw_completion: rawCompletion, stop_fact: stopFact });
|
|
460
|
+
if (release) completeRaw(rawCompletion);
|
|
461
|
+
return rawCompletion;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function failAfterGuardian(error) {
|
|
465
|
+
if (stopFact !== null) return rawCompletion ?? rawCompleted;
|
|
466
|
+
const processId = provider && provider.pid ? provider.pid : null;
|
|
467
|
+
if (config.hold_before_failure_drain_path) {
|
|
468
|
+
fs.writeFileSync(
|
|
469
|
+
config.hold_before_failure_drain_path,
|
|
470
|
+
JSON.stringify({ guardian_pid: processId }) + "\n",
|
|
471
|
+
{ encoding: "utf8", flag: "wx", mode: 0o600 },
|
|
472
|
+
);
|
|
473
|
+
while (fs.existsSync(config.hold_before_failure_drain_path)) await sleep(10);
|
|
474
|
+
}
|
|
475
|
+
const drained = provider === null
|
|
476
|
+
? true
|
|
477
|
+
: provider.exitCode === null && provider.signalCode === null
|
|
478
|
+
? await drainProcessGroup(provider, config.kill_signal, config.cancellation_grace_ms)
|
|
479
|
+
: await drainExitedProcessGroup(processId, config.kill_signal, config.cancellation_grace_ms);
|
|
480
|
+
if (!drained) {
|
|
481
|
+
await persist({ phase: "unsupported", supervisor_error: error.message });
|
|
482
|
+
completeRaw(null);
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
return finishRaw({
|
|
486
|
+
status: null,
|
|
487
|
+
signal: provider ? provider.signalCode : null,
|
|
488
|
+
timed_out: false,
|
|
489
|
+
cancelled: false,
|
|
490
|
+
cancellation_reason: null,
|
|
491
|
+
process_limit_exceeded: false,
|
|
492
|
+
output_limit_exceeded: false,
|
|
493
|
+
termination_reason: null,
|
|
494
|
+
process_id: processId,
|
|
495
|
+
process_tree_drained: true,
|
|
496
|
+
completed_at: new Date().toISOString(),
|
|
497
|
+
error: { code: error.code || "UNKNOWN", message: error.message },
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async function publishUnsupported(error) {
|
|
502
|
+
await persist({ phase: "unsupported", supervisor_error: error.message });
|
|
503
|
+
completeRaw(null);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function attachProviderObservers() {
|
|
507
|
+
let guardianEvents = "";
|
|
508
|
+
let readySettled = false;
|
|
509
|
+
let resolveReady;
|
|
510
|
+
let rejectReady;
|
|
511
|
+
const ready = new Promise((resolve, reject) => {
|
|
512
|
+
resolveReady = resolve;
|
|
513
|
+
rejectReady = reject;
|
|
514
|
+
});
|
|
515
|
+
const rejectUnready = (error) => {
|
|
516
|
+
if (readySettled) return;
|
|
517
|
+
readySettled = true;
|
|
518
|
+
rejectReady(error);
|
|
519
|
+
};
|
|
520
|
+
const handleCompletion = (event) => {
|
|
521
|
+
if (event.type === "ready") {
|
|
522
|
+
if (readySettled) throw new Error("provider guardian published duplicate ready evidence");
|
|
523
|
+
if (!Number.isSafeInteger(event.pid) || event.pid <= 0) {
|
|
524
|
+
throw new Error("provider guardian published an invalid child identity");
|
|
525
|
+
}
|
|
526
|
+
readySettled = true;
|
|
527
|
+
providerChildPid = event.pid;
|
|
528
|
+
resolveReady(event.pid);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (event.type === "output_limit") {
|
|
532
|
+
void terminate("output_limit", { reason: "output_limit" }).catch(publishUnsupported);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (event.type !== "completed" || providerCompletion !== null) return;
|
|
536
|
+
providerCompletion = event;
|
|
537
|
+
void (async () => {
|
|
538
|
+
while (!setupComplete && rawCompletion === null && stopFact === null) await sleep(1);
|
|
539
|
+
if (rawCompletion !== null || stopFact !== null) return;
|
|
540
|
+
const processId = provider.pid || null;
|
|
541
|
+
const drained = await drainProcessGroup(provider, config.kill_signal, config.timeout_grace_ms);
|
|
542
|
+
if (!drained) throw new Error("provider process group drain was not proven");
|
|
543
|
+
await finishRaw({
|
|
544
|
+
status: event.status,
|
|
545
|
+
signal: event.signal,
|
|
546
|
+
timed_out: false,
|
|
547
|
+
cancelled: false,
|
|
548
|
+
cancellation_reason: null,
|
|
549
|
+
process_limit_exceeded: false,
|
|
550
|
+
output_limit_exceeded: false,
|
|
551
|
+
termination_reason: null,
|
|
552
|
+
process_id: processId,
|
|
553
|
+
process_tree_drained: true,
|
|
554
|
+
completed_at: new Date().toISOString(),
|
|
555
|
+
error: event.error,
|
|
556
|
+
});
|
|
557
|
+
})().catch(publishUnsupported);
|
|
558
|
+
};
|
|
559
|
+
provider.stdio[3].setEncoding("utf8");
|
|
560
|
+
provider.stdio[3].on("data", (chunk) => {
|
|
561
|
+
guardianEvents += chunk;
|
|
562
|
+
for (;;) {
|
|
563
|
+
const newline = guardianEvents.indexOf("\n");
|
|
564
|
+
if (newline < 0) break;
|
|
565
|
+
const line = guardianEvents.slice(0, newline);
|
|
566
|
+
guardianEvents = guardianEvents.slice(newline + 1);
|
|
567
|
+
try {
|
|
568
|
+
handleCompletion(JSON.parse(line));
|
|
569
|
+
} catch (error) {
|
|
570
|
+
rejectUnready(error);
|
|
571
|
+
void (async () => {
|
|
572
|
+
while (!setupComplete && rawCompletion === null && stopFact === null) await sleep(1);
|
|
573
|
+
if (rawCompletion === null && stopFact === null) await failAfterGuardian(error);
|
|
574
|
+
})().catch(publishUnsupported);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
provider.once("error", (error) => {
|
|
579
|
+
rejectUnready(error);
|
|
580
|
+
void (async () => {
|
|
581
|
+
while (!setupComplete && rawCompletion === null && stopFact === null) await sleep(1);
|
|
582
|
+
if (rawCompletion === null && stopFact === null) await failAfterGuardian(error);
|
|
583
|
+
})().catch(publishUnsupported);
|
|
584
|
+
});
|
|
585
|
+
provider.once("exit", (status, signal) => {
|
|
586
|
+
rejectUnready(new Error("provider guardian exited before publishing ready evidence"));
|
|
587
|
+
void (async () => {
|
|
588
|
+
while (!setupComplete && rawCompletion === null && stopFact === null) await sleep(1);
|
|
589
|
+
if (rawCompletion !== null || stopFact !== null || providerCompletion !== null) return;
|
|
590
|
+
const processId = provider.pid || null;
|
|
591
|
+
const drained = await drainExitedProcessGroup(processId, "SIGKILL", config.timeout_grace_ms);
|
|
592
|
+
if (!drained) throw new Error("provider guardian exited and residual group drain was not proven");
|
|
593
|
+
await finishRaw({
|
|
594
|
+
status,
|
|
595
|
+
signal,
|
|
596
|
+
timed_out: false,
|
|
597
|
+
cancelled: false,
|
|
598
|
+
cancellation_reason: null,
|
|
599
|
+
process_limit_exceeded: false,
|
|
600
|
+
output_limit_exceeded: false,
|
|
601
|
+
termination_reason: null,
|
|
602
|
+
process_id: processId,
|
|
603
|
+
process_tree_drained: true,
|
|
604
|
+
completed_at: new Date().toISOString(),
|
|
605
|
+
error: { code: "GUARDIAN_EXIT", message: "provider guardian exited before publishing child completion" },
|
|
606
|
+
});
|
|
607
|
+
})().catch(publishUnsupported);
|
|
608
|
+
});
|
|
609
|
+
return ready;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async function terminate(kind, detail, deferRelease = false) {
|
|
613
|
+
if (stopFact !== null) {
|
|
614
|
+
const same = stopFact.kind === kind
|
|
615
|
+
&& (kind !== "canonical_stop" || (
|
|
616
|
+
stopFact.stop_idempotency_key === detail.stop_idempotency_key
|
|
617
|
+
&& JSON.stringify(stopFact.requested_outcome) === JSON.stringify(detail.requested_outcome)
|
|
618
|
+
));
|
|
619
|
+
if (!same) throw new Error("execution already has a different terminal fact");
|
|
620
|
+
return rawCompletion ?? rawCompleted;
|
|
621
|
+
}
|
|
622
|
+
stopFact = { kind, ...detail };
|
|
623
|
+
const processId = provider && provider.pid ? provider.pid : null;
|
|
624
|
+
const drained = provider === null
|
|
625
|
+
? true
|
|
626
|
+
: await drainProcessGroup(provider, config.kill_signal, config.cancellation_grace_ms);
|
|
627
|
+
if (!drained) throw new Error("provider process group drain was not proven");
|
|
628
|
+
const canonicalProcess = kind === "canonical_stop" ? detail.requested_outcome.process : null;
|
|
629
|
+
const canonicalTimeout = canonicalProcess !== null && canonicalProcess.kind === "timeout";
|
|
630
|
+
const canonicalCancellation = canonicalProcess !== null && canonicalProcess.kind === "cancelled";
|
|
631
|
+
if (kind === "canonical_stop" && !canonicalTimeout && !canonicalCancellation) {
|
|
632
|
+
throw new Error("canonical stop outcome is unsupported by the execution supervisor");
|
|
633
|
+
}
|
|
634
|
+
const timedOut = kind === "timeout" || canonicalTimeout;
|
|
635
|
+
const cancelled = kind === "local_cancel" || canonicalCancellation;
|
|
636
|
+
return finishRaw({
|
|
637
|
+
status: null,
|
|
638
|
+
signal: config.kill_signal,
|
|
639
|
+
timed_out: timedOut,
|
|
640
|
+
cancelled,
|
|
641
|
+
cancellation_reason: cancelled
|
|
642
|
+
? (canonicalCancellation ? canonicalProcess.reason : detail.reason || null)
|
|
643
|
+
: null,
|
|
644
|
+
process_limit_exceeded: kind === "process_limit",
|
|
645
|
+
output_limit_exceeded: kind === "output_limit",
|
|
646
|
+
termination_reason: timedOut ? "timeout"
|
|
647
|
+
: kind === "process_limit" ? "process_limit"
|
|
648
|
+
: kind === "output_limit" ? "output_limit"
|
|
649
|
+
: "cancellation",
|
|
650
|
+
process_id: processId,
|
|
651
|
+
process_tree_drained: true,
|
|
652
|
+
completed_at: new Date().toISOString(),
|
|
653
|
+
error: null,
|
|
654
|
+
}, !deferRelease);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const server = net.createServer({ allowHalfOpen: true }, (socket) => {
|
|
658
|
+
socket.setEncoding("utf8");
|
|
659
|
+
let text = "";
|
|
660
|
+
socket.on("data", (chunk) => {
|
|
661
|
+
text += chunk;
|
|
662
|
+
if (text.length > 1024 * 1024) socket.destroy(new Error("control request too large"));
|
|
663
|
+
});
|
|
664
|
+
socket.on("end", () => {
|
|
665
|
+
void (async () => {
|
|
666
|
+
const request = JSON.parse(text);
|
|
667
|
+
if (
|
|
668
|
+
request.schema_version !== "intentdna.local_execution_control.v1"
|
|
669
|
+
|| request.execution_instance_id !== config.execution_instance_id
|
|
670
|
+
|| request.challenge !== config.challenge
|
|
671
|
+
|| request.action !== "stop"
|
|
672
|
+
) throw new Error("unauthenticated execution control request");
|
|
673
|
+
const result = request.requested_outcome
|
|
674
|
+
? await terminate("canonical_stop", {
|
|
675
|
+
stop_idempotency_key: request.stop_idempotency_key,
|
|
676
|
+
requested_at: request.requested_at,
|
|
677
|
+
requested_outcome: request.requested_outcome,
|
|
678
|
+
}, true)
|
|
679
|
+
: await terminate("local_cancel", { reason: request.reason || null }, true);
|
|
680
|
+
await new Promise((resolve) => {
|
|
681
|
+
socket.end(JSON.stringify({ ok: true, raw_completion: result }) + "\n", resolve);
|
|
682
|
+
});
|
|
683
|
+
completeRaw(result);
|
|
684
|
+
})().catch((error) => socket.end(JSON.stringify({ ok: false, error: error.message }) + "\n"));
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
stdoutFd = fs.openSync(config.stdout_path, "wx", 0o600);
|
|
688
|
+
stderrFd = fs.openSync(config.stderr_path, "wx", 0o600);
|
|
689
|
+
await new Promise((resolve, reject) => {
|
|
690
|
+
server.once("error", reject);
|
|
691
|
+
server.listen(config.socket_path, () => {
|
|
692
|
+
server.off("error", reject);
|
|
693
|
+
ownedSocketPath = config.socket_path;
|
|
694
|
+
resolve();
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
fs.chmodSync(config.socket_path, 0o600);
|
|
698
|
+
if (config.hold_before_running_path) {
|
|
699
|
+
fs.writeFileSync(
|
|
700
|
+
config.hold_before_running_path,
|
|
701
|
+
JSON.stringify({ supervisor_pid: process.pid }) + "\n",
|
|
702
|
+
{ encoding: "utf8", flag: "wx", mode: 0o600 },
|
|
703
|
+
);
|
|
704
|
+
while (fs.existsSync(config.hold_before_running_path)) await sleep(10);
|
|
705
|
+
}
|
|
706
|
+
await persist({
|
|
707
|
+
phase: "running",
|
|
708
|
+
supervisor: { ...prepared.supervisor, pid: process.pid, ready_at: new Date().toISOString() },
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
if (stopFact === null) try {
|
|
712
|
+
const guardianConfig = Buffer.from(JSON.stringify({
|
|
713
|
+
gate_path: config.gate_path,
|
|
714
|
+
command: config.command,
|
|
715
|
+
args: config.args,
|
|
716
|
+
cwd: config.cwd,
|
|
717
|
+
env: config.env,
|
|
718
|
+
stdin: config.stdin,
|
|
719
|
+
kill_signal: config.kill_signal,
|
|
720
|
+
max_output_bytes: config.max_output_bytes,
|
|
721
|
+
}), "utf8").toString("base64");
|
|
722
|
+
if (config.inject_guardian_spawn_failure === true) throw new Error("injected guardian spawn failure");
|
|
723
|
+
provider = spawn(process.execPath, ["-e", guardianSource, guardianConfig], {
|
|
724
|
+
cwd: config.cwd,
|
|
725
|
+
env: config.env,
|
|
726
|
+
detached: true,
|
|
727
|
+
stdio: ["pipe", stdoutFd, stderrFd, "pipe"],
|
|
728
|
+
windowsHide: true,
|
|
729
|
+
});
|
|
730
|
+
fs.closeSync(stdoutFd);
|
|
731
|
+
fs.closeSync(stderrFd);
|
|
732
|
+
stdoutFd = null;
|
|
733
|
+
stderrFd = null;
|
|
734
|
+
provider.stdin.on("error", () => {});
|
|
735
|
+
const providerReady = attachProviderObservers();
|
|
736
|
+
if (config.timeout_ms !== null) {
|
|
737
|
+
timeout = setTimeout(() => {
|
|
738
|
+
void terminate("timeout", { reason: "timeout" }).catch(publishUnsupported);
|
|
739
|
+
}, config.timeout_ms);
|
|
740
|
+
}
|
|
741
|
+
await providerReady;
|
|
742
|
+
if (config.hold_after_guardian_spawn_path) {
|
|
743
|
+
fs.writeFileSync(
|
|
744
|
+
config.hold_after_guardian_spawn_path,
|
|
745
|
+
JSON.stringify({ guardian_pid: provider.pid || null }) + "\n",
|
|
746
|
+
{ encoding: "utf8", flag: "wx", mode: 0o600 },
|
|
747
|
+
);
|
|
748
|
+
while (fs.existsSync(config.hold_after_guardian_spawn_path)) await sleep(10);
|
|
749
|
+
}
|
|
750
|
+
if (config.inject_provider_persist_failure === true) throw new Error("injected provider persist failure");
|
|
751
|
+
await persist({
|
|
752
|
+
provider: {
|
|
753
|
+
pid: provider.pid || null,
|
|
754
|
+
process_group_id: provider.pid || null,
|
|
755
|
+
child_pid: providerChildPid,
|
|
756
|
+
launched_at: new Date().toISOString(),
|
|
757
|
+
},
|
|
758
|
+
});
|
|
759
|
+
if (config.auto_release_gate === true && rawCompletion === null && stopFact === null) {
|
|
760
|
+
if (config.inject_gate_release_failure === true) throw new Error("injected gate release failure");
|
|
761
|
+
fs.writeFileSync(config.gate_path, "release\n", { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
762
|
+
}
|
|
763
|
+
} catch (error) {
|
|
764
|
+
await failAfterGuardian(error);
|
|
765
|
+
} finally {
|
|
766
|
+
setupComplete = true;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (provider !== null) {
|
|
770
|
+
await rawCompleted;
|
|
771
|
+
if (timeout !== null) clearTimeout(timeout);
|
|
772
|
+
}
|
|
773
|
+
server.close();
|
|
774
|
+
try { fs.unlinkSync(config.socket_path); } catch {}
|
|
775
|
+
ownedSocketPath = null;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
main().catch((error) => {
|
|
779
|
+
void withRecordLock(async () => {
|
|
780
|
+
const current = readRecord();
|
|
781
|
+
if (["never_launched", "raw_completed", "normalized_completed", "receipted"].includes(current.phase)) return;
|
|
782
|
+
atomicWrite(recordPath, { ...current, phase: "unsupported", supervisor_error: error.message, revision: current.revision + 1, updated_at: new Date().toISOString() });
|
|
783
|
+
}).catch(() => {}).finally(() => {
|
|
784
|
+
if (ownedSocketPath !== null) {
|
|
785
|
+
try { fs.unlinkSync(ownedSocketPath); } catch {}
|
|
786
|
+
}
|
|
787
|
+
process.exit(1);
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
`;
|
|
791
|
+
export const LOCAL_EXECUTION_SUPERVISOR_DIGEST = `sha256:${createHash("sha256")
|
|
792
|
+
.update(LOCAL_EXECUTION_SUPERVISOR_SOURCE, "utf8")
|
|
793
|
+
.digest("hex")}`;
|