@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,670 @@
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.SanctuaryHostPermitExecutor = void 0;
37
+ const node_crypto_1 = require("node:crypto");
38
+ const fs = __importStar(require("node:fs"));
39
+ const path = __importStar(require("node:path"));
40
+ const runtime_1 = require("../../nerves/runtime");
41
+ const sanctuary_authority_codec_1 = require("./sanctuary-authority-codec");
42
+ const sanctuary_host_authority_1 = require("./sanctuary-host-authority");
43
+ const PERMIT_DOMAIN = "ouro.sanctuary.host-permit.v1";
44
+ const RECEIPT_DOMAIN = "ouro.sanctuary.host-receipt.v1";
45
+ const DIGEST = /^sha256:[a-f0-9]{64}$/u;
46
+ const PERMIT_ID = /^permit-[A-Za-z0-9_-]{43}$/u;
47
+ const REGISTRATION_ID = /^hostreg-[A-Za-z0-9_-]{43}$/u;
48
+ const NONCE = /^[A-Za-z0-9_-]{64}$/u;
49
+ const PRINTABLE = /^[\x20-\x7e]+$/u;
50
+ const SCRIPT_BYTES = /^[\x0a\x20-\x7e]+$/u;
51
+ const OUTPUT_LIMIT = 64 * 1024;
52
+ const EXCERPT_LIMIT = 4_096;
53
+ const ACKNOWLEDGED_SUFFIX = ".acknowledged";
54
+ const CLEAN_ENVIRONMENT = Object.freeze({
55
+ PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
56
+ });
57
+ function isObject(value) {
58
+ return typeof value === "object" && value !== null && !Array.isArray(value);
59
+ }
60
+ function exactKeys(value, keys) {
61
+ const actual = Object.keys(value).sort();
62
+ const expected = [...keys].sort();
63
+ return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
64
+ }
65
+ function validTime(value) {
66
+ if (typeof value !== "string")
67
+ return false;
68
+ const instant = new Date(value);
69
+ return !Number.isNaN(instant.getTime()) && instant.toISOString() === value;
70
+ }
71
+ function printable(value, maximum) {
72
+ return typeof value === "string" && value.length > 0 && value.length <= maximum && PRINTABLE.test(value);
73
+ }
74
+ function digest(value) {
75
+ return `sha256:${(0, node_crypto_1.createHash)("sha256").update(value).digest("hex")}`;
76
+ }
77
+ function absoluteExecutable(value) {
78
+ return printable(value, 512)
79
+ && path.posix.isAbsolute(value)
80
+ && path.posix.normalize(value) === value
81
+ && !value.includes("/../")
82
+ && !value.endsWith("/..");
83
+ }
84
+ function validateCommand(command, scriptDigest) {
85
+ if (!isObject(command)
86
+ || !Array.isArray(command.arguments)
87
+ || command.arguments.length > 64
88
+ || !command.arguments.every((argument) => printable(argument, 512))
89
+ || (0, sanctuary_host_authority_1.hasHostInlineCodeSwitch)(command.arguments, command.kind === "script" ? command.interpreter : command.executable)) {
90
+ throw new Error("Sanctuary host permit command is invalid");
91
+ }
92
+ if (command.kind === "executable") {
93
+ if (!exactKeys(command, ["kind", "executable", "arguments"])
94
+ || !absoluteExecutable(command.executable)
95
+ || scriptDigest !== null) {
96
+ throw new Error("Sanctuary host permit executable is invalid");
97
+ }
98
+ return;
99
+ }
100
+ if (command.kind !== "script"
101
+ || !exactKeys(command, ["kind", "interpreter", "arguments", "script"])
102
+ || !absoluteExecutable(command.interpreter)
103
+ || typeof command.script !== "string"
104
+ || Buffer.byteLength(command.script, "utf8") === 0
105
+ || Buffer.byteLength(command.script, "utf8") > 2_048
106
+ || !SCRIPT_BYTES.test(command.script)
107
+ || command.script.split("\n").some((line) => line.endsWith(" "))
108
+ || typeof scriptDigest !== "string"
109
+ || scriptDigest !== digest(command.script)) {
110
+ throw new Error("Sanctuary host permit script is invalid");
111
+ }
112
+ }
113
+ function validatePermit(value, options, now) {
114
+ if (!isObject(value) || !exactKeys(value, [
115
+ "targetHost",
116
+ "permitId",
117
+ "registrationId",
118
+ "registrationDigest",
119
+ "ownerUserId",
120
+ "ownerChatId",
121
+ "ownerObservationDigest",
122
+ "callbackObservationDigest",
123
+ "residentFriendId",
124
+ "relationshipProfileId",
125
+ "relationshipProfileVersion",
126
+ "requestId",
127
+ "sessionKey",
128
+ "sessionEventId",
129
+ "residentApprovalId",
130
+ "effectClass",
131
+ "executionProfile",
132
+ "targetResource",
133
+ "command",
134
+ "scriptDigest",
135
+ "workingDirectoryProfile",
136
+ "environmentProfile",
137
+ "environmentProfileDigest",
138
+ "timeoutMs",
139
+ "stewardPolicy",
140
+ "verification",
141
+ "issuedAt",
142
+ "expiresAt",
143
+ "nonce",
144
+ "publicKeyDigest",
145
+ ])) {
146
+ throw new Error("Sanctuary host permit payload is malformed");
147
+ }
148
+ if (value.targetHost !== options.expectedTargetHost
149
+ || value.ownerUserId !== options.expectedOwnerUserId
150
+ || value.ownerChatId !== options.expectedOwnerChatId
151
+ || !PERMIT_ID.test(String(value.permitId))
152
+ || !REGISTRATION_ID.test(String(value.registrationId))
153
+ || !DIGEST.test(String(value.registrationDigest))
154
+ || !DIGEST.test(String(value.ownerObservationDigest))
155
+ || !DIGEST.test(String(value.callbackObservationDigest))
156
+ || value.effectClass !== "owner_approved_arbitrary_host"
157
+ || value.executionProfile !== "host.owner_approved.v1"
158
+ || value.workingDirectoryProfile !== "host.root.v1"
159
+ || value.environmentProfile !== "host.clean.v1"
160
+ || value.environmentProfileDigest !== digest("host.clean.v1")
161
+ || value.publicKeyDigest !== options.expectedPublicKeyDigest
162
+ || !Number.isSafeInteger(value.timeoutMs)
163
+ || value.timeoutMs < 1_000
164
+ || value.timeoutMs > 900_000
165
+ || !validTime(value.issuedAt)
166
+ || !validTime(value.expiresAt)
167
+ || Date.parse(value.expiresAt) - Date.parse(value.issuedAt) > 120_000
168
+ || Date.parse(value.expiresAt) < Date.parse(now)
169
+ || Date.parse(value.issuedAt) > Date.parse(now)
170
+ || !NONCE.test(String(value.nonce))) {
171
+ throw new Error("Sanctuary host permit payload is invalid");
172
+ }
173
+ for (const [field, maximum] of [
174
+ ["ownerUserId", 64],
175
+ ["ownerChatId", 64],
176
+ ["residentFriendId", 512],
177
+ ["relationshipProfileId", 256],
178
+ ["requestId", 512],
179
+ ["sessionKey", 1_024],
180
+ ["sessionEventId", 512],
181
+ ["residentApprovalId", 512],
182
+ ["targetResource", 256],
183
+ ]) {
184
+ if (!printable(value[field], maximum))
185
+ throw new Error(`Sanctuary host permit ${field} is invalid`);
186
+ }
187
+ if (!Number.isSafeInteger(value.relationshipProfileVersion) || value.relationshipProfileVersion < 1) {
188
+ throw new Error("Sanctuary host permit relationship profile is invalid");
189
+ }
190
+ validateCommand(value.command, value.scriptDigest);
191
+ if (value.verification !== null && (!isObject(value.verification)
192
+ || !exactKeys(value.verification, ["profile", "expectedStateDigest"])
193
+ || !printable(value.verification.profile, 128)
194
+ || !DIGEST.test(String(value.verification.expectedStateDigest)))) {
195
+ throw new Error("Sanctuary host permit verification is invalid");
196
+ }
197
+ if (value.stewardPolicy !== null && (!isObject(value.stewardPolicy)
198
+ || !exactKeys(value.stewardPolicy, ["key", "version", "digest"])
199
+ || !printable(value.stewardPolicy.key, 256)
200
+ || !Number.isSafeInteger(value.stewardPolicy.version)
201
+ || value.stewardPolicy.version < 1
202
+ || !DIGEST.test(String(value.stewardPolicy.digest)))) {
203
+ throw new Error("Sanctuary host permit steward policy is invalid");
204
+ }
205
+ }
206
+ function validateAttempt(value) {
207
+ if (!isObject(value)
208
+ || !exactKeys(value, [
209
+ "startedAt",
210
+ "completedAt",
211
+ "exitCode",
212
+ "signal",
213
+ "timedOut",
214
+ "cancelled",
215
+ "outputOverflow",
216
+ "stdout",
217
+ "stderr",
218
+ "stdoutDigest",
219
+ "stderrDigest",
220
+ "stdoutBytes",
221
+ "stderrBytes",
222
+ "cleanup",
223
+ "containment",
224
+ "verificationBefore",
225
+ "verificationAfter",
226
+ ])
227
+ || !validTime(value.startedAt)
228
+ || !validTime(value.completedAt)
229
+ || Date.parse(value.completedAt) < Date.parse(value.startedAt)
230
+ || (value.exitCode !== null && !Number.isSafeInteger(value.exitCode))
231
+ || (value.signal !== null && !printable(value.signal, 32))
232
+ || typeof value.timedOut !== "boolean"
233
+ || typeof value.cancelled !== "boolean"
234
+ || typeof value.outputOverflow !== "boolean"
235
+ || typeof value.stdout !== "string"
236
+ || typeof value.stderr !== "string"
237
+ || !DIGEST.test(String(value.stdoutDigest))
238
+ || !DIGEST.test(String(value.stderrDigest))
239
+ || !Number.isSafeInteger(value.stdoutBytes)
240
+ || value.stdoutBytes < 0
241
+ || !Number.isSafeInteger(value.stderrBytes)
242
+ || value.stderrBytes < 0
243
+ || !["cgroup_empty", "cleanup_unproven"].includes(String(value.cleanup))
244
+ || !["approved_root_may_escape", "unprovable_after_approved_root_migration"].includes(String(value.containment))
245
+ || (value.verificationBefore !== null && (!isObject(value.verificationBefore)
246
+ || !exactKeys(value.verificationBefore, ["digest"])
247
+ || !DIGEST.test(String(value.verificationBefore.digest))))
248
+ || (value.verificationAfter !== null && (!isObject(value.verificationAfter)
249
+ || !exactKeys(value.verificationAfter, ["matches", "digest"])
250
+ || (value.verificationAfter.matches !== null && typeof value.verificationAfter.matches !== "boolean")
251
+ || !DIGEST.test(String(value.verificationAfter.digest))))) {
252
+ throw new Error("Sanctuary host supervisor attempt is invalid");
253
+ }
254
+ }
255
+ class SanctuaryHostPermitExecutor {
256
+ #options;
257
+ #active = false;
258
+ constructor(options) {
259
+ if (!path.isAbsolute(options.stagingRoot))
260
+ throw new Error("Sanctuary host staging root must be absolute");
261
+ if (!path.isAbsolute(options.stateRoot))
262
+ throw new Error("Sanctuary host execution state root must be absolute");
263
+ this.#options = options;
264
+ }
265
+ async execute(artifact) {
266
+ (0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.sanctuary_host_permit_submitted", message: "Sanctuary host permit submitted for verification" });
267
+ return this.#execute(artifact, false);
268
+ }
269
+ isHealthy() {
270
+ try {
271
+ this.#options.supervisor.verifyInstallation?.();
272
+ return true;
273
+ }
274
+ catch {
275
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "daemon", event: "daemon.sanctuary_host_authority_unavailable", message: "Host execution is unavailable until root program pins are reviewed and refreshed", meta: { reason: "program_pin_verification" } });
276
+ return false;
277
+ }
278
+ }
279
+ async reconcile() {
280
+ let entries;
281
+ try {
282
+ entries = fs.readdirSync(this.#options.stateRoot);
283
+ }
284
+ catch (error) {
285
+ if (error.code === "ENOENT")
286
+ entries = [];
287
+ else
288
+ throw error;
289
+ }
290
+ const receipts = [];
291
+ const permitIds = [];
292
+ for (const entry of entries.sort()) {
293
+ if (entry.endsWith(ACKNOWLEDGED_SUFFIX)) {
294
+ const permitId = entry.slice(0, -ACKNOWLEDGED_SUFFIX.length);
295
+ if (!PERMIT_ID.test(permitId))
296
+ throw new Error("Sanctuary host execution acknowledgement entry is invalid");
297
+ const ledger = this.#options.ledger.read(permitId);
298
+ if (!ledger || ledger.state === "reserved")
299
+ throw new Error("Sanctuary host execution acknowledgement is not terminal");
300
+ this.#finishAcknowledgement(path.join(this.#options.stateRoot, entry));
301
+ continue;
302
+ }
303
+ const permitId = entry;
304
+ if (!PERMIT_ID.test(permitId))
305
+ throw new Error("Sanctuary host execution state entry is invalid");
306
+ const ledger = this.#options.ledger.read(permitId);
307
+ if (!ledger) {
308
+ this.#discardUnreservedPublication(permitId);
309
+ continue;
310
+ }
311
+ permitIds.push(permitId);
312
+ const artifact = this.#readArtifact(permitId, "permit.json");
313
+ const receiptPath = this.#statePath(permitId, "receipt.json");
314
+ const temporaryReceiptPath = `${receiptPath}.tmp`;
315
+ if (!fs.existsSync(receiptPath) && fs.existsSync(temporaryReceiptPath)) {
316
+ const receipt = this.#verifiedReceipt(this.#readTemporaryArtifact(temporaryReceiptPath), permitId, ledger.permitDigest);
317
+ fs.renameSync(temporaryReceiptPath, receiptPath);
318
+ this.#syncDirectory(path.dirname(receiptPath));
319
+ if (ledger.state === "reserved") {
320
+ this.#options.ledger.terminalize({
321
+ permitId,
322
+ state: receipt.payload.state,
323
+ outcomeDigest: (0, sanctuary_authority_codec_1.authorityArtifactDigest)(receipt.domain, receipt.payload),
324
+ updatedAt: receipt.payload.completedAt,
325
+ });
326
+ }
327
+ this.#cleanupStaged(permitId);
328
+ receipts.push(receipt);
329
+ continue;
330
+ }
331
+ if (fs.existsSync(receiptPath)) {
332
+ const receipt = this.#verifiedReceipt(this.#readArtifact(permitId, "receipt.json"), permitId, ledger.permitDigest);
333
+ if (ledger.state === "reserved") {
334
+ this.#options.ledger.terminalize({
335
+ permitId,
336
+ state: receipt.payload.state,
337
+ outcomeDigest: (0, sanctuary_authority_codec_1.authorityArtifactDigest)(receipt.domain, receipt.payload),
338
+ updatedAt: receipt.payload.completedAt,
339
+ });
340
+ }
341
+ this.#cleanupStaged(permitId);
342
+ receipts.push(receipt);
343
+ continue;
344
+ }
345
+ if (ledger.state !== "reserved")
346
+ throw new Error(`Sanctuary host execution receipt is missing for ${permitId}`);
347
+ receipts.push(await this.#execute(artifact, true));
348
+ }
349
+ await this.#options.supervisor.reconcileOrphans?.(permitIds);
350
+ return receipts;
351
+ }
352
+ acknowledge(permitId) {
353
+ if (!PERMIT_ID.test(permitId))
354
+ throw new Error("Sanctuary host permit id is invalid");
355
+ const ledger = this.#options.ledger.read(permitId);
356
+ if (!ledger || ledger.state === "reserved")
357
+ throw new Error("Sanctuary host execution is not terminal");
358
+ const root = path.join(this.#options.stateRoot, permitId);
359
+ const acknowledgedRoot = `${root}${ACKNOWLEDGED_SUFFIX}`;
360
+ if (fs.existsSync(acknowledgedRoot)) {
361
+ this.#finishAcknowledgement(acknowledgedRoot);
362
+ return;
363
+ }
364
+ if (!fs.existsSync(root))
365
+ return;
366
+ this.#options.supervisor.acknowledge?.(permitId);
367
+ fs.renameSync(root, acknowledgedRoot);
368
+ this.#syncStateRoot();
369
+ this.#finishAcknowledgement(acknowledgedRoot);
370
+ }
371
+ async #execute(artifact, resume) {
372
+ if (this.#active)
373
+ throw new Error("Sanctuary host execution is already active");
374
+ const permit = (0, sanctuary_authority_codec_1.verifyAuthorityPayload)({
375
+ artifact,
376
+ expectedDomain: PERMIT_DOMAIN,
377
+ expectedKeyId: this.#options.expectedKeyId,
378
+ publicKey: this.#options.publicKey,
379
+ });
380
+ const permitDigest = (0, sanctuary_authority_codec_1.authorityArtifactDigest)(PERMIT_DOMAIN, permit);
381
+ const existing = resume ? this.#options.ledger.read(permit.permitId) : null;
382
+ if (resume && (existing.state !== "reserved" || existing.permitDigest !== permitDigest || existing.nonce !== permit.nonce)) {
383
+ throw new Error("Sanctuary host reserved permit binding changed");
384
+ }
385
+ const now = existing?.reservedAt ?? this.#now();
386
+ validatePermit(permit, this.#options, now);
387
+ if (!resume) {
388
+ this.#writeArtifact(permit.permitId, "permit.json", artifact);
389
+ this.#options.ledger.reserve({
390
+ permitId: permit.permitId,
391
+ nonce: permit.nonce,
392
+ permitDigest,
393
+ reservedAt: now,
394
+ });
395
+ }
396
+ this.#active = true;
397
+ let stagedPath = null;
398
+ try {
399
+ let executable;
400
+ let arguments_;
401
+ if (permit.command.kind === "script") {
402
+ stagedPath = resume
403
+ ? this.#resumeScript(permit.permitId, permit.command)
404
+ : this.#stageScript(permit.permitId, permit.command);
405
+ executable = permit.command.interpreter;
406
+ arguments_ = [...permit.command.arguments, stagedPath];
407
+ }
408
+ else {
409
+ executable = permit.command.executable;
410
+ arguments_ = [...permit.command.arguments];
411
+ }
412
+ let attempt;
413
+ let errorCategory = null;
414
+ try {
415
+ const operation = resume ? this.#options.supervisor.resume : this.#options.supervisor.execute;
416
+ if (!operation)
417
+ throw new Error("Sanctuary host supervisor cannot resume reserved execution");
418
+ attempt = await operation.call(this.#options.supervisor, {
419
+ permit,
420
+ permitArtifact: artifact,
421
+ executable,
422
+ arguments: arguments_,
423
+ cwd: "/",
424
+ environment: CLEAN_ENVIRONMENT,
425
+ timeoutMs: permit.timeoutMs,
426
+ });
427
+ validateAttempt(attempt);
428
+ }
429
+ catch {
430
+ const failedAt = this.#now();
431
+ attempt = {
432
+ startedAt: now,
433
+ completedAt: failedAt,
434
+ exitCode: null,
435
+ signal: null,
436
+ timedOut: false,
437
+ cancelled: false,
438
+ outputOverflow: false,
439
+ stdout: "",
440
+ stderr: "",
441
+ stdoutDigest: digest(""),
442
+ stderrDigest: digest(""),
443
+ stdoutBytes: 0,
444
+ stderrBytes: 0,
445
+ cleanup: "cleanup_unproven",
446
+ containment: "approved_root_may_escape",
447
+ verificationBefore: null,
448
+ verificationAfter: null,
449
+ };
450
+ errorCategory = "supervisor_failure";
451
+ }
452
+ const outputOverflow = attempt.outputOverflow
453
+ || Buffer.byteLength(attempt.stdout, "utf8") > OUTPUT_LIMIT
454
+ || Buffer.byteLength(attempt.stderr, "utf8") > OUTPUT_LIMIT;
455
+ if (attempt.cleanup !== "cgroup_empty") {
456
+ throw new Error("Sanctuary host execution cleanup requires reconciliation");
457
+ }
458
+ const state = errorCategory
459
+ || attempt.timedOut
460
+ || attempt.cancelled
461
+ || outputOverflow
462
+ || attempt.exitCode !== 0
463
+ || attempt.signal !== null
464
+ ? "failed"
465
+ : permit.verification && attempt.verificationAfter?.matches !== true
466
+ ? "ambiguous"
467
+ : "verified";
468
+ const receipt = (0, sanctuary_authority_codec_1.signAuthorityPayload)({
469
+ domain: RECEIPT_DOMAIN,
470
+ keyId: this.#options.expectedKeyId,
471
+ privateKey: this.#options.privateKey,
472
+ payload: {
473
+ targetHost: this.#options.expectedTargetHost,
474
+ permitId: permit.permitId,
475
+ permitDigest,
476
+ registrationId: permit.registrationId,
477
+ state,
478
+ startedAt: attempt.startedAt,
479
+ completedAt: attempt.completedAt,
480
+ exitCode: attempt.exitCode,
481
+ signal: attempt.signal,
482
+ timedOut: attempt.timedOut,
483
+ cancelled: attempt.cancelled,
484
+ outputOverflow,
485
+ stdoutDigest: attempt.stdoutDigest,
486
+ stderrDigest: attempt.stderrDigest,
487
+ stdoutBytes: attempt.stdoutBytes,
488
+ stderrBytes: attempt.stderrBytes,
489
+ stdoutExcerpt: attempt.stdout.slice(0, EXCERPT_LIMIT),
490
+ stderrExcerpt: attempt.stderr.slice(0, EXCERPT_LIMIT),
491
+ cleanup: attempt.cleanup,
492
+ containment: attempt.containment,
493
+ verificationBefore: attempt.verificationBefore,
494
+ verificationAfter: attempt.verificationAfter,
495
+ errorCategory,
496
+ publicKeyDigest: this.#options.expectedPublicKeyDigest,
497
+ },
498
+ });
499
+ this.#writeArtifact(permit.permitId, "receipt.json", receipt);
500
+ this.#options.ledger.terminalize({
501
+ permitId: permit.permitId,
502
+ state,
503
+ outcomeDigest: (0, sanctuary_authority_codec_1.authorityArtifactDigest)(receipt.domain, receipt.payload),
504
+ updatedAt: attempt.completedAt,
505
+ });
506
+ if (stagedPath)
507
+ fs.unlinkSync(stagedPath);
508
+ return receipt;
509
+ }
510
+ finally {
511
+ this.#active = false;
512
+ }
513
+ }
514
+ #statePath(permitId, name) {
515
+ return path.join(this.#options.stateRoot, permitId, name);
516
+ }
517
+ #writeArtifact(permitId, name, value) {
518
+ const filePath = this.#statePath(permitId, name);
519
+ const bytes = `${JSON.stringify(value)}\n`;
520
+ const root = path.dirname(filePath);
521
+ fs.mkdirSync(root, { recursive: true, mode: 0o700 });
522
+ fs.chmodSync(root, 0o700);
523
+ if (fs.existsSync(filePath)) {
524
+ if (fs.readFileSync(filePath, "utf8") !== bytes)
525
+ throw new Error(`Sanctuary host ${name} changed`);
526
+ return;
527
+ }
528
+ const temporaryPath = `${filePath}.tmp`;
529
+ if (fs.existsSync(temporaryPath))
530
+ throw new Error(`Sanctuary host ${name} temporary publication exists`);
531
+ const descriptor = fs.openSync(temporaryPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW, 0o600);
532
+ try {
533
+ fs.writeFileSync(descriptor, bytes, "utf8");
534
+ fs.fsyncSync(descriptor);
535
+ }
536
+ finally {
537
+ fs.closeSync(descriptor);
538
+ }
539
+ fs.renameSync(temporaryPath, filePath);
540
+ this.#syncDirectory(root);
541
+ }
542
+ #readArtifact(permitId, name) {
543
+ const filePath = this.#statePath(permitId, name);
544
+ const stat = fs.lstatSync(filePath);
545
+ if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o777) !== 0o600) {
546
+ throw new Error(`Sanctuary host ${name} metadata is invalid`);
547
+ }
548
+ const descriptor = fs.openSync(filePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
549
+ try {
550
+ return JSON.parse(fs.readFileSync(descriptor, "utf8"));
551
+ }
552
+ finally {
553
+ fs.closeSync(descriptor);
554
+ }
555
+ }
556
+ #readTemporaryArtifact(filePath) {
557
+ const stat = fs.lstatSync(filePath);
558
+ if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o777) !== 0o600) {
559
+ throw new Error("Sanctuary host temporary artifact metadata is invalid");
560
+ }
561
+ const descriptor = fs.openSync(filePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
562
+ try {
563
+ return JSON.parse(fs.readFileSync(descriptor, "utf8"));
564
+ }
565
+ finally {
566
+ fs.closeSync(descriptor);
567
+ }
568
+ }
569
+ #verifiedReceipt(artifact, permitId, permitDigest) {
570
+ const payload = (0, sanctuary_authority_codec_1.verifyAuthorityPayload)({
571
+ artifact,
572
+ expectedDomain: RECEIPT_DOMAIN,
573
+ expectedKeyId: this.#options.expectedKeyId,
574
+ publicKey: this.#options.publicKey,
575
+ });
576
+ if (payload.permitId !== permitId
577
+ || payload.permitDigest !== permitDigest
578
+ || !["verified", "failed", "ambiguous"].includes(String(payload.state))
579
+ || !validTime(payload.completedAt)) {
580
+ throw new Error("Sanctuary host durable receipt binding changed");
581
+ }
582
+ return artifact;
583
+ }
584
+ #cleanupStaged(permitId) {
585
+ const stagedPath = path.join(this.#options.stagingRoot, `${permitId}.script`);
586
+ try {
587
+ fs.unlinkSync(stagedPath);
588
+ }
589
+ catch (error) {
590
+ if (error.code !== "ENOENT")
591
+ throw error;
592
+ }
593
+ }
594
+ #finishAcknowledgement(root) {
595
+ const entries = fs.readdirSync(root);
596
+ if (entries.some((name) => !["permit.json", "receipt.json"].includes(name))) {
597
+ throw new Error("Sanctuary host execution acknowledgement contents changed");
598
+ }
599
+ for (const name of entries)
600
+ fs.unlinkSync(path.join(root, name));
601
+ fs.rmdirSync(root);
602
+ this.#syncStateRoot();
603
+ }
604
+ #syncStateRoot() {
605
+ this.#syncDirectory(this.#options.stateRoot);
606
+ }
607
+ #syncDirectory(directory) {
608
+ const descriptor = fs.openSync(directory, fs.constants.O_RDONLY);
609
+ try {
610
+ fs.fsyncSync(descriptor);
611
+ }
612
+ finally {
613
+ fs.closeSync(descriptor);
614
+ }
615
+ }
616
+ #discardUnreservedPublication(permitId) {
617
+ const root = path.join(this.#options.stateRoot, permitId);
618
+ const entries = fs.readdirSync(root);
619
+ if (entries.some((name) => !["permit.json", "permit.json.tmp"].includes(name))) {
620
+ throw new Error(`Sanctuary host execution ledger is missing for ${permitId}`);
621
+ }
622
+ for (const name of entries)
623
+ fs.unlinkSync(path.join(root, name));
624
+ fs.rmdirSync(root);
625
+ this.#syncStateRoot();
626
+ }
627
+ #stageScript(permitId, command) {
628
+ fs.mkdirSync(this.#options.stagingRoot, { recursive: true, mode: 0o700 });
629
+ const root = fs.lstatSync(this.#options.stagingRoot);
630
+ if (!root.isDirectory() || root.isSymbolicLink() || (root.mode & 0o777) !== 0o700) {
631
+ throw new Error("Sanctuary host staging root metadata is invalid");
632
+ }
633
+ const target = path.join(this.#options.stagingRoot, `${permitId}.script`);
634
+ const descriptor = fs.openSync(target, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW, 0o700);
635
+ try {
636
+ fs.writeFileSync(descriptor, command.script, "utf8");
637
+ fs.fsyncSync(descriptor);
638
+ }
639
+ finally {
640
+ fs.closeSync(descriptor);
641
+ }
642
+ return target;
643
+ }
644
+ #resumeScript(permitId, command) {
645
+ const target = path.join(this.#options.stagingRoot, `${permitId}.script`);
646
+ if (!fs.existsSync(target))
647
+ return this.#stageScript(permitId, command);
648
+ const stat = fs.lstatSync(target);
649
+ if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o777) !== 0o700) {
650
+ throw new Error("Sanctuary host staged script metadata is invalid");
651
+ }
652
+ const descriptor = fs.openSync(target, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
653
+ try {
654
+ if (fs.readFileSync(descriptor, "utf8") !== command.script) {
655
+ throw new Error("Sanctuary host staged script changed");
656
+ }
657
+ }
658
+ finally {
659
+ fs.closeSync(descriptor);
660
+ }
661
+ return target;
662
+ }
663
+ #now() {
664
+ const value = this.#options.now?.() ?? new Date().toISOString();
665
+ if (!validTime(value))
666
+ throw new Error("Sanctuary host executor time is invalid");
667
+ return value;
668
+ }
669
+ }
670
+ exports.SanctuaryHostPermitExecutor = SanctuaryHostPermitExecutor;