@ouro.bot/cli 0.1.0-alpha.815 → 0.1.0-alpha.817

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.
Files changed (63) hide show
  1. package/assets/sanctuary-host-launcher.sh +16 -0
  2. package/changelog.json +18 -0
  3. package/deploy/unraid/Dockerfile +6 -0
  4. package/deploy/unraid/README.txt +305 -365
  5. package/deploy/unraid/docker-man-template-transaction.mjs +192 -7
  6. package/deploy/unraid/sanctuary-acceptance-adapter.sh +1 -1
  7. package/deploy/unraid/sanctuary-acceptance-contract.json +7 -7
  8. package/deploy/unraid/sanctuary-authority-installation.json +36 -0
  9. package/deploy/unraid/sanctuary-authority-service.sh +30 -0
  10. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +62 -9
  11. package/deploy/unraid/sanctuary-unit16-run.sh +15 -15
  12. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
  13. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +2 -2
  14. package/deploy/unraid/sanctuary.xml +2 -1
  15. package/dist/heart/approval-store.js +11 -1
  16. package/dist/heart/core.js +2 -1
  17. package/dist/heart/daemon/container-spec-auditor-main.js +9 -8
  18. package/dist/heart/daemon/container-spec-auditor.js +17 -24
  19. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +95 -91
  20. package/dist/heart/daemon/sanctuary-acceptance-harness.js +95 -169
  21. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +4 -5
  22. package/dist/heart/daemon/sanctuary-authority-codec.js +86 -0
  23. package/dist/heart/daemon/sanctuary-authority-epoch.js +256 -0
  24. package/dist/heart/daemon/sanctuary-authority-installation.js +140 -0
  25. package/dist/heart/daemon/sanctuary-authority-ledger.js +241 -0
  26. package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +780 -0
  27. package/dist/heart/daemon/sanctuary-authority-vault-migration.js +79 -0
  28. package/dist/heart/daemon/sanctuary-host-authority.js +1008 -0
  29. package/dist/heart/daemon/sanctuary-host-detached-supervisor.js +494 -0
  30. package/dist/heart/daemon/sanctuary-host-executor.js +670 -0
  31. package/dist/heart/daemon/sanctuary-host-linux-kernel.js +334 -0
  32. package/dist/heart/daemon/sanctuary-host-supervisor-entry.js +207 -0
  33. package/dist/heart/daemon/sanctuary-host-supervisor.js +95 -0
  34. package/dist/heart/daemon/sanctuary-telegram-authority-entry.js +445 -0
  35. package/dist/heart/daemon/sanctuary-telegram-authority-gateway.js +585 -0
  36. package/dist/heart/daemon/sanctuary-telegram-authority-service.js +615 -0
  37. package/dist/heart/daemon/sense-manager.js +20 -10
  38. package/dist/heart/external-events/router.js +31 -15
  39. package/dist/heart/steward-policy.js +374 -58
  40. package/dist/heart/tool-approval.js +8 -1
  41. package/dist/repertoire/relationship-authorization.js +128 -0
  42. package/dist/repertoire/tools-base.js +9 -13
  43. package/dist/repertoire/tools-sanctuary-host.js +202 -0
  44. package/dist/repertoire/tools-steward-policy.js +34 -10
  45. package/dist/repertoire/tools-unraid.js +79 -32
  46. package/dist/repertoire/tools.js +37 -25
  47. package/dist/repertoire/unraid-restart.js +179 -52
  48. package/dist/senses/private-runtime.js +27 -12
  49. package/dist/senses/root-host-approval-port.js +345 -0
  50. package/dist/senses/root-host-approval-runtime.js +393 -0
  51. package/dist/senses/sanctuary-authority-resident.js +102 -0
  52. package/dist/senses/sanctuary-health-runner.js +0 -1
  53. package/dist/senses/sanctuary-media-catalog-contract.js +4 -1
  54. package/dist/senses/sanctuary-runtime.js +2 -0
  55. package/dist/senses/telegram-admission.js +7 -0
  56. package/dist/senses/telegram-approval-runtime.js +130 -12
  57. package/dist/senses/telegram-attachments.js +5 -1
  58. package/dist/senses/telegram-authority-transport.js +231 -0
  59. package/dist/senses/telegram-client.js +31 -8
  60. package/dist/senses/telegram-entry.js +1 -1
  61. package/dist/senses/telegram.js +174 -28
  62. package/npm-shrinkwrap.json +2 -2
  63. package/package.json +1 -1
@@ -0,0 +1,494 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DetachedSanctuaryHostSupervisor = void 0;
37
+ const node_crypto_1 = require("node:crypto");
38
+ const node_child_process_1 = require("node:child_process");
39
+ const fs = __importStar(require("node:fs"));
40
+ const path = __importStar(require("node:path"));
41
+ const runtime_1 = require("../../nerves/runtime");
42
+ const SCHEMA_VERSION = 1;
43
+ const DIGEST = /^sha256:[a-f0-9]{64}$/u;
44
+ const PERMIT_ID = /^permit-[A-Za-z0-9_-]{43}$/u;
45
+ function digest(bytes) {
46
+ return `sha256:${(0, node_crypto_1.createHash)("sha256").update(bytes).digest("hex")}`;
47
+ }
48
+ function isObject(value) {
49
+ return typeof value === "object" && value !== null && !Array.isArray(value);
50
+ }
51
+ function assertAbsolute(value, label) {
52
+ if (!path.isAbsolute(value))
53
+ throw new Error(`Sanctuary host ${label} path must be absolute`);
54
+ }
55
+ function assertPinnedProgram(filePath, expectedDigest, expectedUid, kind) {
56
+ if (!DIGEST.test(expectedDigest))
57
+ throw new Error("Sanctuary host program digest is invalid");
58
+ const actual = kind === "system" ? fs.realpathSync(filePath) : filePath;
59
+ const stat = fs.lstatSync(actual);
60
+ if (!stat.isFile()
61
+ || stat.isSymbolicLink()
62
+ || stat.uid !== expectedUid
63
+ || (kind !== "source" && (stat.mode & 0o111) === 0)
64
+ || (stat.mode & 0o022) !== 0) {
65
+ throw new Error(`Sanctuary host program metadata is invalid: ${filePath}`);
66
+ }
67
+ const descriptor = fs.openSync(actual, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
68
+ try {
69
+ if (digest(fs.readFileSync(descriptor)) !== expectedDigest) {
70
+ throw new Error(`Sanctuary host program digest changed: ${filePath}`);
71
+ }
72
+ }
73
+ finally {
74
+ fs.closeSync(descriptor);
75
+ }
76
+ }
77
+ function writePrivate(filePath, bytes) {
78
+ fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
79
+ fs.chmodSync(path.dirname(filePath), 0o700);
80
+ const temporaryPath = `${filePath}.${process.pid}.tmp`;
81
+ const descriptor = fs.openSync(temporaryPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW, 0o600);
82
+ try {
83
+ fs.writeFileSync(descriptor, bytes, "utf8");
84
+ fs.fsyncSync(descriptor);
85
+ }
86
+ finally {
87
+ fs.closeSync(descriptor);
88
+ }
89
+ fs.renameSync(temporaryPath, filePath);
90
+ fs.chmodSync(filePath, 0o600);
91
+ }
92
+ function readPrivateJson(filePath, expectedUid) {
93
+ const stat = fs.lstatSync(filePath);
94
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== expectedUid || (stat.mode & 0o777) !== 0o600) {
95
+ throw new Error(`Sanctuary host supervisor record metadata is invalid: ${filePath}`);
96
+ }
97
+ const descriptor = fs.openSync(filePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
98
+ try {
99
+ return JSON.parse(fs.readFileSync(descriptor, "utf8"));
100
+ }
101
+ finally {
102
+ fs.closeSync(descriptor);
103
+ }
104
+ }
105
+ function binding(value, permitId, specDigest) {
106
+ if (!isObject(value)
107
+ || value.schemaVersion !== SCHEMA_VERSION
108
+ || value.permitId !== permitId
109
+ || value.specDigest !== specDigest
110
+ || !Number.isSafeInteger(value.supervisorPid)
111
+ || value.supervisorPid < 1
112
+ || typeof value.bootId !== "string"
113
+ || value.bootId.length === 0
114
+ || typeof value.processStartTime !== "string"
115
+ || value.processStartTime.length === 0) {
116
+ throw new Error("Sanctuary host supervisor binding is invalid");
117
+ }
118
+ return value;
119
+ }
120
+ function defaultBootId() {
121
+ return fs.readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
122
+ }
123
+ function defaultProcessStartTime(pid) {
124
+ const fields = fs.readFileSync(`/proc/${pid}/stat`, "utf8").trim().split(" ");
125
+ if (fields.length < 22 || !fields[21])
126
+ throw new Error("Sanctuary host supervisor process start time is unavailable");
127
+ return fields[21];
128
+ }
129
+ function defaultProcessAlive(pid) {
130
+ try {
131
+ process.kill(pid, 0);
132
+ return true;
133
+ }
134
+ catch (error) {
135
+ return error.code === "EPERM";
136
+ }
137
+ }
138
+ function cgroupEmpty(cgroupPath) {
139
+ const events = fs.readFileSync(path.join(cgroupPath, "cgroup.events"), "utf8");
140
+ const populated = events.split("\n").find((line) => line.startsWith("populated "));
141
+ if (!populated)
142
+ throw new Error("Sanctuary host cgroup populated state is unavailable");
143
+ return populated === "populated 0";
144
+ }
145
+ class DetachedSanctuaryHostSupervisor {
146
+ #options;
147
+ #active = false;
148
+ constructor(options) {
149
+ for (const [label, value] of [
150
+ ["state root", options.stateRoot],
151
+ ["cgroup root", options.cgroupRoot],
152
+ ["supervisor", options.programPath],
153
+ ["launcher", options.launcherPath],
154
+ ["prlimit", options.prlimitPath],
155
+ ["setsid", options.setsidPath],
156
+ ["shell", options.shellPath],
157
+ ])
158
+ assertAbsolute(value, label);
159
+ this.#options = options;
160
+ }
161
+ verifyInstallation() {
162
+ this.#assertPins();
163
+ }
164
+ async execute(input) {
165
+ (0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.sanctuary_host_supervision_requested", message: "Independent Sanctuary host supervision requested" });
166
+ return this.#run(input, false);
167
+ }
168
+ async resume(input) {
169
+ return this.#run(input, true);
170
+ }
171
+ async reconcileOrphans(knownPermitIds) {
172
+ const known = new Set(knownPermitIds);
173
+ let stateEntries = [];
174
+ try {
175
+ stateEntries = fs.readdirSync(this.#options.stateRoot);
176
+ }
177
+ catch (error) {
178
+ if (error.code !== "ENOENT")
179
+ throw error;
180
+ }
181
+ for (const permitId of stateEntries) {
182
+ if (!PERMIT_ID.test(permitId) || !known.has(permitId)) {
183
+ throw new Error("Sanctuary host supervisor state is unmatched");
184
+ }
185
+ }
186
+ for (const permitId of fs.readdirSync(this.#options.cgroupRoot)) {
187
+ const cgroupPath = path.join(this.#options.cgroupRoot, permitId);
188
+ if (!fs.lstatSync(cgroupPath).isDirectory())
189
+ continue;
190
+ if (!PERMIT_ID.test(permitId))
191
+ throw new Error("Sanctuary host cgroup entry is invalid");
192
+ if (!known.has(permitId) && !await this.#reconcileCgroup(cgroupPath)) {
193
+ throw new Error("Sanctuary orphan cgroup cleanup is unproven");
194
+ }
195
+ }
196
+ }
197
+ acknowledge(permitId) {
198
+ if (!PERMIT_ID.test(permitId))
199
+ throw new Error("Sanctuary host supervisor permit id is invalid");
200
+ const permitRoot = path.join(this.#options.stateRoot, permitId);
201
+ if (!fs.existsSync(permitRoot)) {
202
+ if (fs.existsSync(path.join(this.#options.cgroupRoot, permitId))) {
203
+ throw new Error("Sanctuary host supervisor cgroup is still present");
204
+ }
205
+ return;
206
+ }
207
+ const specPath = path.join(permitRoot, "spec.json");
208
+ const terminalPath = path.join(permitRoot, "terminal.json");
209
+ const lockPath = path.join(permitRoot, "supervisor.lock");
210
+ const acknowledgementPath = path.join(permitRoot, "acknowledgement.json");
211
+ const recoveryPath = path.join(permitRoot, "recovery.json");
212
+ if (fs.existsSync(lockPath) || fs.existsSync(path.join(this.#options.cgroupRoot, permitId))) {
213
+ throw new Error("Sanctuary host supervisor is not safely terminal");
214
+ }
215
+ if (fs.existsSync(acknowledgementPath)) {
216
+ const acknowledgement = readPrivateJson(acknowledgementPath, this.#options.expectedUid);
217
+ if (!isObject(acknowledgement) || acknowledgement.schemaVersion !== SCHEMA_VERSION || acknowledgement.permitId !== permitId) {
218
+ throw new Error("Sanctuary host supervisor acknowledgement changed");
219
+ }
220
+ }
221
+ else if (fs.existsSync(specPath) || fs.existsSync(terminalPath) || fs.existsSync(recoveryPath)) {
222
+ const specBytes = fs.readFileSync(specPath, "utf8");
223
+ if (fs.existsSync(terminalPath)) {
224
+ binding(readPrivateJson(terminalPath, this.#options.expectedUid), permitId, digest(specBytes));
225
+ }
226
+ else {
227
+ const recovery = readPrivateJson(recoveryPath, this.#options.expectedUid);
228
+ if (!isObject(recovery)
229
+ || recovery.schemaVersion !== SCHEMA_VERSION
230
+ || recovery.permitId !== permitId
231
+ || recovery.specDigest !== digest(specBytes)) {
232
+ throw new Error("Sanctuary host supervisor recovery acknowledgement changed");
233
+ }
234
+ }
235
+ writePrivate(acknowledgementPath, `${JSON.stringify({ schemaVersion: SCHEMA_VERSION, permitId })}\n`);
236
+ }
237
+ for (const name of ["start.json", "spawn.json", "ready.json", "terminal.json", "recovery.json", "spec.json", "acknowledgement.json"]) {
238
+ const filePath = path.join(permitRoot, name);
239
+ if (fs.existsSync(filePath))
240
+ fs.unlinkSync(filePath);
241
+ }
242
+ fs.rmdirSync(permitRoot);
243
+ }
244
+ async #run(input, resume) {
245
+ if (this.#active)
246
+ throw new Error("Sanctuary detached host supervisor is already active");
247
+ if (!PERMIT_ID.test(input.permit.permitId))
248
+ throw new Error("Sanctuary detached host permit id is invalid");
249
+ this.#active = true;
250
+ try {
251
+ if (!resume)
252
+ this.verifyInstallation();
253
+ const permitRoot = path.join(this.#options.stateRoot, input.permit.permitId);
254
+ const specPath = path.join(permitRoot, "spec.json");
255
+ const readyPath = path.join(permitRoot, "ready.json");
256
+ const terminalPath = path.join(permitRoot, "terminal.json");
257
+ const lockPath = path.join(permitRoot, "supervisor.lock");
258
+ const spawnPath = path.join(permitRoot, "spawn.json");
259
+ const startPath = path.join(permitRoot, "start.json");
260
+ const spec = {
261
+ schemaVersion: SCHEMA_VERSION,
262
+ permitId: input.permit.permitId,
263
+ permitArtifact: input.permitArtifact,
264
+ keyId: this.#options.keyId,
265
+ publicKeyPem: this.#options.publicKeyPem,
266
+ executable: input.executable,
267
+ arguments: input.arguments,
268
+ cwd: input.cwd,
269
+ environment: input.environment,
270
+ timeoutMs: input.timeoutMs,
271
+ cgroupRoot: this.#options.cgroupRoot,
272
+ launcherPath: this.#options.launcherPath,
273
+ launcherDigest: this.#options.launcherDigest,
274
+ prlimitPath: this.#options.prlimitPath,
275
+ prlimitDigest: this.#options.prlimitDigest,
276
+ setsidPath: this.#options.setsidPath,
277
+ setsidDigest: this.#options.setsidDigest,
278
+ shellPath: this.#options.shellPath,
279
+ shellDigest: this.#options.shellDigest,
280
+ supervisorProgramPath: this.#options.programPath,
281
+ supervisorProgramDigest: this.#options.programDigest,
282
+ readyPath,
283
+ terminalPath,
284
+ lockPath,
285
+ spawnPath,
286
+ startPath,
287
+ };
288
+ const specBytes = `${JSON.stringify(spec)}\n`;
289
+ const specDigest = digest(specBytes);
290
+ let ready;
291
+ if (resume && !fs.existsSync(specPath)) {
292
+ return this.#recoveryAttempt(input.permit.permitId);
293
+ }
294
+ if (fs.existsSync(specPath)) {
295
+ const existing = fs.readFileSync(specPath, "utf8");
296
+ if (existing !== specBytes)
297
+ throw new Error("Sanctuary host supervisor specification changed");
298
+ if (fs.existsSync(terminalPath))
299
+ return this.#terminal(terminalPath, input.permit.permitId, specDigest);
300
+ try {
301
+ if (fs.existsSync(readyPath)) {
302
+ ready = binding(readPrivateJson(readyPath, this.#options.expectedUid), input.permit.permitId, specDigest);
303
+ }
304
+ else if (fs.existsSync(spawnPath)) {
305
+ ready = binding(readPrivateJson(spawnPath, this.#options.expectedUid), input.permit.permitId, specDigest);
306
+ this.#assertProcessBinding(ready);
307
+ if (!fs.existsSync(startPath))
308
+ writePrivate(startPath, `${JSON.stringify({ schemaVersion: SCHEMA_VERSION, permitId: input.permit.permitId, specDigest })}\n`);
309
+ ready = await this.#waitForReady(readyPath, input.permit.permitId, specDigest);
310
+ }
311
+ else {
312
+ return this.#recoveryAttempt(input.permit.permitId, undefined, specDigest);
313
+ }
314
+ this.#assertLiveBinding(ready, lockPath);
315
+ }
316
+ catch {
317
+ return this.#recoveryAttempt(input.permit.permitId, ready, specDigest);
318
+ }
319
+ }
320
+ else {
321
+ writePrivate(specPath, specBytes);
322
+ const child = (this.#options.spawn ?? ((value) => {
323
+ const spawned = (0, node_child_process_1.spawn)(process.execPath, [value.programPath, "--spec", value.specPath], {
324
+ detached: true,
325
+ stdio: "ignore",
326
+ });
327
+ return spawned;
328
+ }))({ programPath: this.#options.programPath, specPath, readyPath, terminalPath, detached: true });
329
+ if (!child.pid)
330
+ throw new Error("Sanctuary host supervisor failed to start");
331
+ child.unref();
332
+ ready = {
333
+ schemaVersion: SCHEMA_VERSION,
334
+ permitId: input.permit.permitId,
335
+ specDigest,
336
+ supervisorPid: child.pid,
337
+ bootId: (this.#options.bootId ?? defaultBootId)(),
338
+ processStartTime: (this.#options.processStartTime ?? defaultProcessStartTime)(child.pid),
339
+ };
340
+ writePrivate(spawnPath, `${JSON.stringify(ready)}\n`);
341
+ writePrivate(startPath, `${JSON.stringify({ schemaVersion: SCHEMA_VERSION, permitId: input.permit.permitId, specDigest })}\n`);
342
+ ready = await this.#waitForReady(readyPath, input.permit.permitId, specDigest);
343
+ if (ready.supervisorPid !== child.pid)
344
+ throw new Error("Sanctuary host supervisor binding changed");
345
+ this.#assertLiveBinding(ready, lockPath);
346
+ }
347
+ try {
348
+ return await this.#waitForTerminal(terminalPath, lockPath, ready, input.permit.permitId, specDigest, input.timeoutMs + 20_000);
349
+ }
350
+ catch {
351
+ return this.#recoveryAttempt(input.permit.permitId, ready, specDigest);
352
+ }
353
+ }
354
+ finally {
355
+ this.#active = false;
356
+ }
357
+ }
358
+ #assertPins() {
359
+ for (const [filePath, expectedDigest, kind] of [
360
+ [this.#options.programPath, this.#options.programDigest, "source"],
361
+ [this.#options.launcherPath, this.#options.launcherDigest, "executable"],
362
+ [this.#options.prlimitPath, this.#options.prlimitDigest, "system"],
363
+ [this.#options.setsidPath, this.#options.setsidDigest, "system"],
364
+ [this.#options.shellPath, this.#options.shellDigest, "system"],
365
+ ])
366
+ assertPinnedProgram(filePath, expectedDigest, this.#options.expectedUid, kind);
367
+ }
368
+ #assertLiveBinding(value, lockPath) {
369
+ this.#assertProcessBinding(value);
370
+ const lock = readPrivateJson(lockPath, this.#options.expectedUid);
371
+ if (!isObject(lock)
372
+ || lock.schemaVersion !== SCHEMA_VERSION
373
+ || lock.permitId !== value.permitId
374
+ || lock.supervisorPid !== value.supervisorPid) {
375
+ throw new Error("Sanctuary host supervisor live binding changed");
376
+ }
377
+ }
378
+ #assertProcessBinding(value) {
379
+ const processAlive = this.#options.processAlive ?? defaultProcessAlive;
380
+ const bootId = this.#options.bootId ?? defaultBootId;
381
+ const processStartTime = this.#options.processStartTime ?? defaultProcessStartTime;
382
+ if (!processAlive(value.supervisorPid)
383
+ || bootId() !== value.bootId
384
+ || processStartTime(value.supervisorPid) !== value.processStartTime) {
385
+ throw new Error("Sanctuary host supervisor live binding changed");
386
+ }
387
+ }
388
+ async #waitForReady(filePath, permitId, specDigest) {
389
+ const attempts = Math.max(1, Math.ceil((this.#options.readyTimeoutMs ?? 5_000) / 25));
390
+ for (let index = 0; index < attempts; index += 1) {
391
+ if (fs.existsSync(filePath))
392
+ return binding(readPrivateJson(filePath, this.#options.expectedUid), permitId, specDigest);
393
+ await (this.#options.sleep ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))))(25);
394
+ }
395
+ if (fs.existsSync(filePath))
396
+ return binding(readPrivateJson(filePath, this.#options.expectedUid), permitId, specDigest);
397
+ throw new Error("Sanctuary host supervisor readiness timed out");
398
+ }
399
+ async #waitForTerminal(filePath, lockPath, ready, permitId, specDigest, timeoutMs) {
400
+ const attempts = Math.max(1, Math.ceil(timeoutMs / 25));
401
+ for (let index = 0; index < attempts; index += 1) {
402
+ if (fs.existsSync(filePath) && !fs.existsSync(lockPath))
403
+ return this.#terminal(filePath, permitId, specDigest);
404
+ this.#assertProcessBinding(ready);
405
+ await (this.#options.sleep ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))))(25);
406
+ }
407
+ if (fs.existsSync(filePath) && !fs.existsSync(lockPath))
408
+ return this.#terminal(filePath, permitId, specDigest);
409
+ throw new Error("Sanctuary host supervisor terminal record timed out");
410
+ }
411
+ #terminal(filePath, permitId, specDigest) {
412
+ const value = readPrivateJson(filePath, this.#options.expectedUid);
413
+ const record = binding(value, permitId, specDigest);
414
+ if (!isObject(value) || !("attempt" in value))
415
+ throw new Error("Sanctuary host supervisor terminal record is invalid");
416
+ const terminal = { ...record, attempt: value.attempt };
417
+ return terminal.attempt;
418
+ }
419
+ async #recoveryAttempt(permitId, supervisor, specDigest) {
420
+ const startedAt = this.#options.now?.() ?? new Date().toISOString();
421
+ if (supervisor && (this.#options.processAlive ?? defaultProcessAlive)(supervisor.supervisorPid)) {
422
+ try {
423
+ (this.#options.signalProcess ?? process.kill)(supervisor.supervisorPid, "SIGTERM");
424
+ }
425
+ catch (error) {
426
+ if (!isObject(error) || error.code !== "ESRCH")
427
+ throw error;
428
+ }
429
+ }
430
+ const cgroupPath = path.join(this.#options.cgroupRoot, permitId);
431
+ let cleanup = !fs.existsSync(cgroupPath) || await this.#reconcileCgroup(cgroupPath)
432
+ ? "cgroup_empty"
433
+ : "cleanup_unproven";
434
+ if (!supervisor && fs.existsSync(path.join(this.#options.stateRoot, permitId, "supervisor.lock"))) {
435
+ cleanup = "cleanup_unproven";
436
+ }
437
+ if (cleanup === "cgroup_empty" && supervisor) {
438
+ const processAlive = this.#options.processAlive ?? defaultProcessAlive;
439
+ for (let attempt = 0; attempt < 400 && processAlive(supervisor.supervisorPid); attempt += 1) {
440
+ await (this.#options.sleep ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))))(25);
441
+ }
442
+ if (processAlive(supervisor.supervisorPid)) {
443
+ cleanup = "cleanup_unproven";
444
+ }
445
+ else {
446
+ const lockPath = path.join(this.#options.stateRoot, permitId, "supervisor.lock");
447
+ if (fs.existsSync(lockPath)) {
448
+ const lock = readPrivateJson(lockPath, this.#options.expectedUid);
449
+ if (!isObject(lock) || lock.permitId !== permitId || lock.supervisorPid !== supervisor.supervisorPid) {
450
+ throw new Error("Sanctuary host supervisor recovery lock changed");
451
+ }
452
+ fs.unlinkSync(lockPath);
453
+ }
454
+ }
455
+ }
456
+ if (cleanup === "cgroup_empty" && specDigest) {
457
+ writePrivate(path.join(this.#options.stateRoot, permitId, "recovery.json"), `${JSON.stringify({ schemaVersion: SCHEMA_VERSION, permitId, specDigest })}\n`);
458
+ }
459
+ return {
460
+ startedAt,
461
+ completedAt: this.#options.now?.() ?? new Date().toISOString(),
462
+ exitCode: null,
463
+ signal: null,
464
+ timedOut: false,
465
+ cancelled: false,
466
+ outputOverflow: false,
467
+ stdout: "",
468
+ stderr: "",
469
+ stdoutDigest: digest(""),
470
+ stderrDigest: digest(""),
471
+ stdoutBytes: 0,
472
+ stderrBytes: 0,
473
+ cleanup,
474
+ containment: "unprovable_after_approved_root_migration",
475
+ verificationBefore: null,
476
+ verificationAfter: null,
477
+ };
478
+ }
479
+ async #reconcileCgroup(cgroupPath) {
480
+ if (this.#options.killCgroup)
481
+ this.#options.killCgroup(cgroupPath);
482
+ else
483
+ fs.writeFileSync(path.join(cgroupPath, "cgroup.kill"), "1", "utf8");
484
+ for (let attempt = 0; attempt < 400; attempt += 1) {
485
+ if ((this.#options.cgroupEmpty ?? cgroupEmpty)(cgroupPath)) {
486
+ (this.#options.removeCgroup ?? fs.rmdirSync)(cgroupPath);
487
+ return true;
488
+ }
489
+ await (this.#options.sleep ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))))(25);
490
+ }
491
+ return false;
492
+ }
493
+ }
494
+ exports.DetachedSanctuaryHostSupervisor = DetachedSanctuaryHostSupervisor;