@ouro.bot/cli 0.1.0-alpha.816 → 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 (49) hide show
  1. package/assets/sanctuary-host-launcher.sh +16 -0
  2. package/changelog.json +10 -0
  3. package/deploy/unraid/Dockerfile +6 -0
  4. package/deploy/unraid/README.txt +280 -60
  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/core.js +2 -1
  16. package/dist/heart/daemon/container-spec-auditor-main.js +8 -7
  17. package/dist/heart/daemon/container-spec-auditor.js +16 -9
  18. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +95 -91
  19. package/dist/heart/daemon/sanctuary-acceptance-harness.js +95 -169
  20. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +4 -5
  21. package/dist/heart/daemon/sanctuary-authority-codec.js +86 -0
  22. package/dist/heart/daemon/sanctuary-authority-epoch.js +256 -0
  23. package/dist/heart/daemon/sanctuary-authority-installation.js +140 -0
  24. package/dist/heart/daemon/sanctuary-authority-ledger.js +241 -0
  25. package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +780 -0
  26. package/dist/heart/daemon/sanctuary-authority-vault-migration.js +79 -0
  27. package/dist/heart/daemon/sanctuary-host-authority.js +1008 -0
  28. package/dist/heart/daemon/sanctuary-host-detached-supervisor.js +494 -0
  29. package/dist/heart/daemon/sanctuary-host-executor.js +670 -0
  30. package/dist/heart/daemon/sanctuary-host-linux-kernel.js +334 -0
  31. package/dist/heart/daemon/sanctuary-host-supervisor-entry.js +207 -0
  32. package/dist/heart/daemon/sanctuary-host-supervisor.js +95 -0
  33. package/dist/heart/daemon/sanctuary-telegram-authority-entry.js +445 -0
  34. package/dist/heart/daemon/sanctuary-telegram-authority-gateway.js +585 -0
  35. package/dist/heart/daemon/sanctuary-telegram-authority-service.js +615 -0
  36. package/dist/heart/daemon/sense-manager.js +20 -10
  37. package/dist/repertoire/tools-sanctuary-host.js +202 -0
  38. package/dist/repertoire/tools.js +15 -6
  39. package/dist/senses/root-host-approval-port.js +345 -0
  40. package/dist/senses/root-host-approval-runtime.js +393 -0
  41. package/dist/senses/sanctuary-authority-resident.js +102 -0
  42. package/dist/senses/telegram-admission.js +7 -0
  43. package/dist/senses/telegram-attachments.js +5 -1
  44. package/dist/senses/telegram-authority-transport.js +231 -0
  45. package/dist/senses/telegram-client.js +31 -8
  46. package/dist/senses/telegram-entry.js +1 -1
  47. package/dist/senses/telegram.js +168 -28
  48. package/npm-shrinkwrap.json +2 -2
  49. package/package.json +1 -1
@@ -0,0 +1,334 @@
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.LinuxSanctuaryHostSupervisorKernel = void 0;
37
+ const node_child_process_1 = require("node:child_process");
38
+ const node_crypto_1 = require("node:crypto");
39
+ const fs = __importStar(require("node:fs"));
40
+ const path = __importStar(require("node:path"));
41
+ const runtime_1 = require("../../nerves/runtime");
42
+ const OUTPUT_LIMIT = 64 * 1024;
43
+ function requireRootOwnedProgram(filePath, links = new Set()) {
44
+ const parts = path.resolve(filePath).split("/").filter(Boolean);
45
+ let current = "/";
46
+ let stat = fs.lstatSync(current);
47
+ for (let index = 0; index < parts.length; index += 1) {
48
+ current = path.join(current, parts[index]);
49
+ stat = fs.lstatSync(current);
50
+ if (stat.uid !== 0)
51
+ throw new Error("Sanctuary host program path must be root-owned");
52
+ if (stat.isSymbolicLink()) {
53
+ if (links.has(current))
54
+ throw new Error("Sanctuary host program path has a symlink cycle");
55
+ links.add(current);
56
+ return requireRootOwnedProgram(path.resolve(path.dirname(current), fs.readlinkSync(current), ...parts.slice(index + 1)), links);
57
+ }
58
+ if ((stat.mode & 0o022) !== 0 || (index < parts.length - 1 && !stat.isDirectory())) {
59
+ throw new Error("Sanctuary host program path must be root-owned and non-writable by other users");
60
+ }
61
+ }
62
+ if (!stat.isFile() || (stat.mode & 0o111) === 0)
63
+ throw new Error("Sanctuary host program must be an executable regular file");
64
+ }
65
+ function sleep(milliseconds) {
66
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
67
+ }
68
+ function cgroupEmpty(cgroupPath) {
69
+ const events = fs.readFileSync(path.join(cgroupPath, "cgroup.events"), "utf8");
70
+ const populated = events.split("\n").find((line) => line.startsWith("populated "));
71
+ if (!populated)
72
+ throw new Error("Sanctuary host cgroup populated state is unavailable");
73
+ return populated === "populated 0";
74
+ }
75
+ function defaultProcessCgroup(pid) {
76
+ try {
77
+ return fs.readFileSync(`/proc/${pid}/cgroup`, "utf8");
78
+ }
79
+ catch (error) {
80
+ if (error.code === "ENOENT")
81
+ return null;
82
+ throw error;
83
+ }
84
+ }
85
+ function processInCgroup(value, cgroupPath) {
86
+ const relative = cgroupPath.startsWith("/sys/fs/cgroup")
87
+ ? cgroupPath.slice("/sys/fs/cgroup".length)
88
+ : cgroupPath;
89
+ return value.split("\n").some((line) => line.endsWith(`:${relative}`));
90
+ }
91
+ function fileDigest(filePath) {
92
+ const stat = fs.lstatSync(filePath);
93
+ if (!stat.isFile() || stat.isSymbolicLink())
94
+ throw new Error("Sanctuary host verification target is not a regular file");
95
+ const descriptor = fs.openSync(filePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
96
+ const hash = (0, node_crypto_1.createHash)("sha256");
97
+ const buffer = Buffer.allocUnsafe(64 * 1024);
98
+ try {
99
+ let count;
100
+ do {
101
+ count = fs.readSync(descriptor, buffer, 0, buffer.length, null);
102
+ if (count > 0)
103
+ hash.update(buffer.subarray(0, count));
104
+ } while (count > 0);
105
+ }
106
+ finally {
107
+ fs.closeSync(descriptor);
108
+ }
109
+ return `sha256:${hash.digest("hex")}`;
110
+ }
111
+ function outputCollector(child, timeoutMs) {
112
+ let stdout = Buffer.alloc(0);
113
+ let stderr = Buffer.alloc(0);
114
+ let stdoutBytes = 0;
115
+ let stderrBytes = 0;
116
+ const stdoutHash = (0, node_crypto_1.createHash)("sha256");
117
+ const stderrHash = (0, node_crypto_1.createHash)("sha256");
118
+ let overflowed = false;
119
+ let settle;
120
+ let reject;
121
+ let settled = false;
122
+ const completion = new Promise((resolve, rejectPromise) => {
123
+ settle = resolve;
124
+ reject = rejectPromise;
125
+ });
126
+ let settleDrained;
127
+ let rejectDrained;
128
+ const drained = new Promise((resolve, rejectPromise) => {
129
+ settleDrained = resolve;
130
+ rejectDrained = rejectPromise;
131
+ });
132
+ const finish = (value) => {
133
+ if (settled)
134
+ return;
135
+ settled = true;
136
+ clearTimeout(deadline);
137
+ settle(value);
138
+ };
139
+ const append = (current, value) => {
140
+ const remaining = Math.max(0, OUTPUT_LIMIT - current.length);
141
+ const next = remaining > 0 ? Buffer.concat([current, value.subarray(0, remaining)]) : current;
142
+ return { bytes: next, overflow: value.length > remaining };
143
+ };
144
+ const finishOverflow = () => {
145
+ if (overflowed)
146
+ return;
147
+ overflowed = true;
148
+ finish({
149
+ exitCode: null,
150
+ signal: null,
151
+ stdout: stdout.toString("utf8"),
152
+ stderr: stderr.toString("utf8"),
153
+ outputOverflow: true,
154
+ migrationSuspected: false,
155
+ deadlineExpired: false,
156
+ });
157
+ };
158
+ child.stdout?.on("data", (value) => {
159
+ stdoutHash.update(value);
160
+ stdoutBytes += value.length;
161
+ const next = append(stdout, value);
162
+ stdout = next.bytes;
163
+ if (next.overflow)
164
+ finishOverflow();
165
+ });
166
+ child.stderr?.on("data", (value) => {
167
+ stderrHash.update(value);
168
+ stderrBytes += value.length;
169
+ const next = append(stderr, value);
170
+ stderr = next.bytes;
171
+ if (next.overflow)
172
+ finishOverflow();
173
+ });
174
+ const deadline = setTimeout(() => finish({
175
+ exitCode: null,
176
+ signal: null,
177
+ stdout: stdout.toString("utf8"),
178
+ stderr: stderr.toString("utf8"),
179
+ outputOverflow: overflowed,
180
+ migrationSuspected: false,
181
+ deadlineExpired: true,
182
+ }), timeoutMs);
183
+ deadline.unref();
184
+ child.once("error", (error) => {
185
+ if (settled)
186
+ return;
187
+ settled = true;
188
+ clearTimeout(deadline);
189
+ reject(error);
190
+ rejectDrained(error);
191
+ });
192
+ child.once("close", (exitCode, signal) => {
193
+ finish({
194
+ exitCode,
195
+ signal,
196
+ stdout: stdout.toString("utf8"),
197
+ stderr: stderr.toString("utf8"),
198
+ outputOverflow: overflowed,
199
+ migrationSuspected: false,
200
+ deadlineExpired: false,
201
+ });
202
+ settleDrained({
203
+ stdout: stdout.toString("utf8"),
204
+ stderr: stderr.toString("utf8"),
205
+ stdoutDigest: `sha256:${stdoutHash.digest("hex")}`,
206
+ stderrDigest: `sha256:${stderrHash.digest("hex")}`,
207
+ stdoutBytes,
208
+ stderrBytes,
209
+ });
210
+ });
211
+ return { completion, drained };
212
+ }
213
+ class LinuxSanctuaryHostSupervisorKernel {
214
+ #options;
215
+ constructor(options) {
216
+ for (const value of [options.cgroupRoot, options.shellPath, options.launcherPath, options.prlimitPath, options.setsidPath]) {
217
+ if (!path.isAbsolute(value))
218
+ throw new Error("Sanctuary host Linux kernel paths must be absolute");
219
+ }
220
+ this.#options = options;
221
+ }
222
+ async prepareCgroup(permitId, limits) {
223
+ const cgroupPath = path.join(this.#options.cgroupRoot, permitId);
224
+ fs.mkdirSync(cgroupPath, { recursive: false, mode: 0o700 });
225
+ fs.writeFileSync(path.join(cgroupPath, "cpu.max"), limits.cpuMax, "utf8");
226
+ fs.writeFileSync(path.join(cgroupPath, "memory.max"), limits.memoryMax, "utf8");
227
+ fs.writeFileSync(path.join(cgroupPath, "pids.max"), limits.pidsMax, "utf8");
228
+ return cgroupPath;
229
+ }
230
+ async launch(input) {
231
+ requireRootOwnedProgram(input.executable);
232
+ const spawnImpl = this.#options.spawn ?? node_child_process_1.spawn;
233
+ const child = spawnImpl(this.#options.shellPath, [
234
+ this.#options.launcherPath,
235
+ path.join(input.cgroupPath, "cgroup.procs"),
236
+ this.#options.prlimitPath,
237
+ this.#options.setsidPath,
238
+ input.executable,
239
+ ...input.arguments,
240
+ ], {
241
+ cwd: input.cwd,
242
+ env: input.environment,
243
+ detached: false,
244
+ stdio: ["ignore", "pipe", "pipe", "pipe"],
245
+ });
246
+ if (!child.pid || !child.stdio[3])
247
+ throw new Error("Sanctuary host launcher failed to start");
248
+ (0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.sanctuary_host_launcher_spawned", message: "Sanctuary host launcher spawned; handshake pending" });
249
+ const handshake = child.stdio[3];
250
+ const ready = new Promise((resolve, reject) => {
251
+ let bytes = "";
252
+ handshake.setEncoding("utf8");
253
+ handshake.on("data", (value) => { bytes += value; });
254
+ handshake.once("error", reject);
255
+ handshake.once("close", () => {
256
+ if (bytes === "ready\n")
257
+ resolve();
258
+ else
259
+ reject(new Error("Sanctuary host launcher handshake is invalid"));
260
+ });
261
+ });
262
+ const { completion, drained } = outputCollector(child, input.timeoutMs);
263
+ const processCgroup = this.#options.processCgroup ?? defaultProcessCgroup;
264
+ let migrationSuspected = false;
265
+ const inspectCgroup = () => {
266
+ try {
267
+ const cgroup = processCgroup(child.pid);
268
+ if (cgroup !== null && !processInCgroup(cgroup, input.cgroupPath))
269
+ migrationSuspected = true;
270
+ }
271
+ catch {
272
+ migrationSuspected = true;
273
+ }
274
+ };
275
+ inspectCgroup();
276
+ const migrationMonitor = setInterval(inspectCgroup, this.#options.pollIntervalMs ?? 25);
277
+ migrationMonitor.unref();
278
+ const observedCompletion = completion.then((value) => {
279
+ inspectCgroup();
280
+ return {
281
+ ...value,
282
+ migrationSuspected,
283
+ };
284
+ }).finally(() => clearInterval(migrationMonitor));
285
+ return {
286
+ pid: child.pid,
287
+ bootId: this.#options.bootId?.() ?? fs.readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim(),
288
+ processStartTime: this.#options.processStartTime?.(child.pid)
289
+ ?? fs.readFileSync(`/proc/${child.pid}/stat`, "utf8").trim().split(" ")[21]
290
+ ?? "",
291
+ ready,
292
+ completion: observedCompletion,
293
+ drained,
294
+ signalGroup: (signal) => {
295
+ try {
296
+ (this.#options.kill ?? process.kill)(-child.pid, signal);
297
+ }
298
+ catch (error) {
299
+ if (error.code !== "ESRCH")
300
+ throw error;
301
+ }
302
+ },
303
+ };
304
+ }
305
+ async waitForCgroupEmpty(cgroupPath, timeoutMs) {
306
+ const interval = this.#options.pollIntervalMs ?? 25;
307
+ const attempts = Math.max(1, Math.ceil(timeoutMs / interval));
308
+ for (let index = 0; index < attempts; index += 1) {
309
+ if (cgroupEmpty(cgroupPath))
310
+ return true;
311
+ await (this.#options.sleep ?? sleep)(interval);
312
+ }
313
+ return false;
314
+ }
315
+ async killCgroup(cgroupPath) {
316
+ fs.writeFileSync(path.join(cgroupPath, "cgroup.kill"), "1", "utf8");
317
+ }
318
+ async removeCgroup(cgroupPath) {
319
+ fs.rmdirSync(cgroupPath);
320
+ }
321
+ async sleep(milliseconds) {
322
+ await (this.#options.sleep ?? sleep)(milliseconds);
323
+ }
324
+ now() {
325
+ return this.#options.now?.() ?? new Date().toISOString();
326
+ }
327
+ async verify(request, phase, permit) {
328
+ if (request.profile !== "file.digest.v1")
329
+ throw new Error("Sanctuary host verification profile is unsupported");
330
+ const digest = fileDigest(permit.targetResource);
331
+ return phase === "before" ? { digest } : { matches: digest === request.expectedStateDigest, digest };
332
+ }
333
+ }
334
+ exports.LinuxSanctuaryHostSupervisorKernel = LinuxSanctuaryHostSupervisorKernel;
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.runSanctuaryHostSupervisor = runSanctuaryHostSupervisor;
38
+ exports.runSanctuaryHostSupervisorCli = runSanctuaryHostSupervisorCli;
39
+ const node_crypto_1 = require("node:crypto");
40
+ const fs = __importStar(require("node:fs"));
41
+ const path = __importStar(require("node:path"));
42
+ const runtime_1 = require("../../nerves/runtime");
43
+ const sanctuary_authority_codec_1 = require("./sanctuary-authority-codec");
44
+ const sanctuary_host_linux_kernel_1 = require("./sanctuary-host-linux-kernel");
45
+ const sanctuary_host_supervisor_1 = require("./sanctuary-host-supervisor");
46
+ const DIGEST = /^sha256:[a-f0-9]{64}$/u;
47
+ function digest(bytes) {
48
+ return `sha256:${(0, node_crypto_1.createHash)("sha256").update(bytes).digest("hex")}`;
49
+ }
50
+ function readPrivateSpec(specPath, expectedUid) {
51
+ if (!path.isAbsolute(specPath))
52
+ throw new Error("Sanctuary host supervisor spec path must be absolute");
53
+ const stat = fs.lstatSync(specPath);
54
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== expectedUid || (stat.mode & 0o777) !== 0o600) {
55
+ throw new Error("Sanctuary host supervisor spec metadata is invalid");
56
+ }
57
+ const descriptor = fs.openSync(specPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
58
+ try {
59
+ const bytes = fs.readFileSync(descriptor, "utf8");
60
+ const spec = JSON.parse(bytes);
61
+ if (spec.schemaVersion !== 1
62
+ || spec.permitId !== spec.permitArtifact?.payload?.permitId
63
+ || !Array.isArray(spec.arguments)
64
+ || spec.cwd !== "/"
65
+ || !Number.isSafeInteger(spec.timeoutMs)
66
+ || spec.timeoutMs < 1_000
67
+ || spec.timeoutMs > 900_000) {
68
+ throw new Error("Sanctuary host supervisor spec is invalid");
69
+ }
70
+ return { bytes, spec };
71
+ }
72
+ finally {
73
+ fs.closeSync(descriptor);
74
+ }
75
+ }
76
+ function assertDigest(filePath, expectedDigest, expectedUid, kind = "executable") {
77
+ const actual = kind === "system" ? fs.realpathSync(filePath) : filePath;
78
+ const stat = fs.lstatSync(actual);
79
+ if (!DIGEST.test(expectedDigest)
80
+ || !stat.isFile()
81
+ || stat.isSymbolicLink()
82
+ || stat.uid !== expectedUid
83
+ || (kind !== "source" && (stat.mode & 0o111) === 0)
84
+ || (stat.mode & 0o022) !== 0) {
85
+ throw new Error(`Sanctuary host supervisor primitive metadata is invalid: ${filePath}`);
86
+ }
87
+ const descriptor = fs.openSync(actual, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
88
+ try {
89
+ if (digest(fs.readFileSync(descriptor)) === expectedDigest)
90
+ return;
91
+ }
92
+ finally {
93
+ fs.closeSync(descriptor);
94
+ }
95
+ {
96
+ throw new Error(`Sanctuary host supervisor primitive digest changed: ${filePath}`);
97
+ }
98
+ }
99
+ function writePrivateJson(filePath, value) {
100
+ fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
101
+ fs.chmodSync(path.dirname(filePath), 0o700);
102
+ const temporaryPath = `${filePath}.${process.pid}.tmp`;
103
+ const descriptor = fs.openSync(temporaryPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW, 0o600);
104
+ try {
105
+ fs.writeFileSync(descriptor, `${JSON.stringify(value)}\n`, "utf8");
106
+ fs.fsyncSync(descriptor);
107
+ }
108
+ finally {
109
+ fs.closeSync(descriptor);
110
+ }
111
+ fs.renameSync(temporaryPath, filePath);
112
+ (0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.sanctuary_host_supervisor_state_written", message: "Sanctuary host supervisor state atomically written" });
113
+ fs.chmodSync(filePath, 0o600);
114
+ }
115
+ async function waitForStart(filePath, permitId, specDigest) {
116
+ for (let attempt = 0; attempt < 200; attempt += 1) {
117
+ if (fs.existsSync(filePath)) {
118
+ const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
119
+ if (value.schemaVersion !== 1 || value.permitId !== permitId || value.specDigest !== specDigest) {
120
+ throw new Error("Sanctuary host supervisor start gate is invalid");
121
+ }
122
+ return;
123
+ }
124
+ await new Promise((resolve) => setTimeout(resolve, 25));
125
+ }
126
+ throw new Error("Sanctuary host supervisor start gate timed out");
127
+ }
128
+ async function runSanctuaryHostSupervisor(specPath, options = {}) {
129
+ const expectedUid = options.expectedUid ?? 0;
130
+ if ((options.getuid ?? process.getuid)() !== expectedUid)
131
+ throw new Error("Sanctuary host supervisor must run as root");
132
+ const { bytes, spec } = readPrivateSpec(specPath, expectedUid);
133
+ const publicKey = (0, node_crypto_1.createPublicKey)(spec.publicKeyPem);
134
+ const permit = (0, sanctuary_authority_codec_1.verifyAuthorityPayload)({
135
+ artifact: spec.permitArtifact,
136
+ expectedDomain: "ouro.sanctuary.host-permit.v1",
137
+ expectedKeyId: spec.keyId,
138
+ publicKey,
139
+ });
140
+ const publicKeyDigest = `sha256:${(0, node_crypto_1.createHash)("sha256").update(publicKey.export({ type: "spki", format: "der" })).digest("hex")}`;
141
+ if (publicKeyDigest !== permit.publicKeyDigest) {
142
+ throw new Error("Sanctuary host supervisor permit key digest changed");
143
+ }
144
+ assertDigest(spec.launcherPath, spec.launcherDigest, expectedUid);
145
+ assertDigest(spec.prlimitPath, spec.prlimitDigest, expectedUid, "system");
146
+ assertDigest(spec.setsidPath, spec.setsidDigest, expectedUid, "system");
147
+ assertDigest(spec.shellPath, spec.shellDigest, expectedUid, "system");
148
+ assertDigest(spec.supervisorProgramPath, spec.supervisorProgramDigest, expectedUid, "source");
149
+ const specDigest = digest(bytes);
150
+ const processId = options.processId ?? process.pid;
151
+ const bootId = options.bootId?.() ?? fs.readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
152
+ const processStartTime = options.processStartTime?.(processId)
153
+ ?? fs.readFileSync(`/proc/${processId}/stat`, "utf8").trim().split(" ")[21]
154
+ ?? "";
155
+ const binding = {
156
+ schemaVersion: 1,
157
+ permitId: spec.permitId,
158
+ specDigest,
159
+ supervisorPid: processId,
160
+ bootId,
161
+ processStartTime,
162
+ };
163
+ await waitForStart(spec.startPath, spec.permitId, specDigest);
164
+ const lock = fs.openSync(spec.lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW, 0o600);
165
+ try {
166
+ fs.writeFileSync(lock, `${JSON.stringify({ schemaVersion: 1, permitId: spec.permitId, supervisorPid: processId })}\n`, "utf8");
167
+ fs.fsyncSync(lock);
168
+ writePrivateJson(spec.readyPath, binding);
169
+ const kernel = options.kernel ?? new sanctuary_host_linux_kernel_1.LinuxSanctuaryHostSupervisorKernel({
170
+ cgroupRoot: spec.cgroupRoot,
171
+ shellPath: spec.shellPath,
172
+ launcherPath: spec.launcherPath,
173
+ prlimitPath: spec.prlimitPath,
174
+ setsidPath: spec.setsidPath,
175
+ });
176
+ const supervisor = new sanctuary_host_supervisor_1.KernelSanctuaryHostSupervisor(kernel);
177
+ const attempt = await supervisor.execute({
178
+ permit,
179
+ permitArtifact: spec.permitArtifact,
180
+ executable: spec.executable,
181
+ arguments: spec.arguments,
182
+ cwd: spec.cwd,
183
+ environment: spec.environment,
184
+ timeoutMs: spec.timeoutMs,
185
+ });
186
+ writePrivateJson(spec.terminalPath, { ...binding, attempt });
187
+ return attempt;
188
+ }
189
+ finally {
190
+ fs.closeSync(lock);
191
+ fs.rmSync(spec.lockPath, { force: true });
192
+ }
193
+ }
194
+ async function runSanctuaryHostSupervisorCli(argv = process.argv, run = runSanctuaryHostSupervisor) {
195
+ const index = argv.indexOf("--spec");
196
+ const specPath = index >= 0 ? argv[index + 1] : undefined;
197
+ if (!specPath)
198
+ throw new Error("Sanctuary host supervisor requires --spec");
199
+ await run(specPath);
200
+ }
201
+ /* v8 ignore next 6 -- executable guard delegates to the tested CLI runner @preserve */
202
+ if (require.main === module) {
203
+ void runSanctuaryHostSupervisorCli().catch((error) => {
204
+ process.stderr.write(`${error instanceof Error ? error.message : "Sanctuary host supervisor failed"}\n`);
205
+ process.exitCode = 1;
206
+ });
207
+ }
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KernelSanctuaryHostSupervisor = void 0;
4
+ const runtime_1 = require("../../nerves/runtime");
5
+ class KernelSanctuaryHostSupervisor {
6
+ #kernel;
7
+ #active = false;
8
+ constructor(kernel) {
9
+ this.#kernel = kernel;
10
+ }
11
+ async execute(input) {
12
+ if (this.#active)
13
+ throw new Error("Sanctuary host supervisor is already active");
14
+ this.#active = true;
15
+ const startedAt = this.#kernel.now();
16
+ const deadline = Date.parse(startedAt) + input.timeoutMs;
17
+ let cgroupPath = null;
18
+ try {
19
+ const verificationBefore = input.permit.verification
20
+ ? await this.#kernel.verify(input.permit.verification, "before", input.permit)
21
+ : null;
22
+ cgroupPath = await this.#kernel.prepareCgroup(input.permit.permitId, {
23
+ cpuMax: "50000 100000",
24
+ memoryMax: "1073741824",
25
+ pidsMax: "256",
26
+ });
27
+ const child = await this.#kernel.launch({ permit: input.permit, executable: input.executable, arguments: input.arguments, cwd: input.cwd, environment: input.environment, timeoutMs: input.timeoutMs, cgroupPath });
28
+ await child.ready;
29
+ const completion = await child.completion;
30
+ let timedOut = completion.deadlineExpired === true;
31
+ const remainingMs = Math.max(0, deadline - Date.parse(this.#kernel.now()));
32
+ let cleanupEmpty = !timedOut && await this.#kernel.waitForCgroupEmpty(cgroupPath, remainingMs);
33
+ if (!cleanupEmpty || completion.outputOverflow) {
34
+ timedOut = timedOut || !completion.outputOverflow;
35
+ await child.signalGroup("SIGTERM");
36
+ await this.#kernel.sleep(10_000);
37
+ await this.#kernel.killCgroup(cgroupPath);
38
+ cleanupEmpty = await this.#kernel.waitForCgroupEmpty(cgroupPath, 10_000);
39
+ }
40
+ if (cleanupEmpty)
41
+ await this.#kernel.removeCgroup(cgroupPath);
42
+ if (!cleanupEmpty)
43
+ throw new Error("Sanctuary host output drain cannot be proven");
44
+ const output = await Promise.race([
45
+ child.drained,
46
+ this.#kernel.sleep(10_000).then(() => {
47
+ throw new Error("Sanctuary host output drain timed out");
48
+ }),
49
+ ]);
50
+ const verificationAfter = input.permit.verification
51
+ ? await this.#kernel.verify(input.permit.verification, "after", input.permit)
52
+ : null;
53
+ (0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.sanctuary_host_cleanup_observed", message: "Sanctuary host cgroup cleanup and output drain observed", meta: { timedOut, outputOverflow: completion.outputOverflow } });
54
+ return {
55
+ startedAt,
56
+ completedAt: this.#kernel.now(),
57
+ exitCode: completion.exitCode,
58
+ signal: completion.signal,
59
+ timedOut,
60
+ cancelled: false,
61
+ outputOverflow: completion.outputOverflow,
62
+ stdout: output.stdout,
63
+ stderr: output.stderr,
64
+ stdoutDigest: output.stdoutDigest,
65
+ stderrDigest: output.stderrDigest,
66
+ stdoutBytes: output.stdoutBytes,
67
+ stderrBytes: output.stderrBytes,
68
+ cleanup: "cgroup_empty",
69
+ containment: completion.migrationSuspected
70
+ ? "unprovable_after_approved_root_migration"
71
+ : "approved_root_may_escape",
72
+ verificationBefore,
73
+ verificationAfter,
74
+ };
75
+ }
76
+ catch (error) {
77
+ if (cgroupPath) {
78
+ try {
79
+ await this.#kernel.killCgroup(cgroupPath);
80
+ if (await this.#kernel.waitForCgroupEmpty(cgroupPath, 10_000)) {
81
+ await this.#kernel.removeCgroup(cgroupPath);
82
+ }
83
+ }
84
+ catch {
85
+ // The caller records the failed dependency while startup reconciliation owns uncertain cgroup state.
86
+ }
87
+ }
88
+ throw error;
89
+ }
90
+ finally {
91
+ this.#active = false;
92
+ }
93
+ }
94
+ }
95
+ exports.KernelSanctuaryHostSupervisor = KernelSanctuaryHostSupervisor;