@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
@@ -19,6 +19,8 @@ const CONTAINER_ID = /^[0-9a-f]{64}$/u
19
19
  const VERSION_TAG = /^ghcr\.io\/ourostack\/ouroboros-butler:[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/u
20
20
  const JELLYFIN_STATES = new Set(["created", "running", "paused", "restarting", "removing", "exited", "dead"])
21
21
  const JELLYFIN_FORMAT = '{"name":{{json .Name}},"containerId":{{json .Id}},"imageId":{{json .Image}},"state":{{json .State.Status}},"restartCount":{{json .RestartCount}}}'
22
+ const AUTHORITY_INSTALL_STEPS = ["freeze-resident", "stage-authority", "verify-token-rotation", "transfer-cursor", "remove-resident-token", "start-gateway", "configure-resident", "start-resident", "verify-install"]
23
+ const AUTHORITY_ROLLBACK_STEPS = ["freeze-resident", "retire-registrations", "reconcile-executions", "stop-gateway", "end-epoch", "restore-token-cursor", "restore-resident", "verify-rollback"]
22
24
 
23
25
  function object(value, label) {
24
26
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`)
@@ -171,12 +173,12 @@ function syncDirectory(path) {
171
173
  try { fsyncSync(fd) } finally { closeSync(fd) }
172
174
  }
173
175
 
174
- function atomicWrite(path, temporaryPath, bytes, metadata, expectedUid, expectedGid) {
176
+ function atomicWrite(path, temporaryPath, bytes, metadata) {
175
177
  const fd = openSync(temporaryPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, metadata.mode)
176
178
  try {
177
179
  writeFileSync(fd, bytes)
178
180
  fchmodSync(fd, metadata.mode)
179
- fchownSync(fd, metadata.uid ?? expectedUid, metadata.gid ?? expectedGid)
181
+ fchownSync(fd, metadata.uid, metadata.gid)
180
182
  fsyncSync(fd)
181
183
  } finally {
182
184
  closeSync(fd)
@@ -197,7 +199,8 @@ function decodeCanonicalBase64(value) {
197
199
 
198
200
  function validateJournalRecord(raw, state) {
199
201
  const record = object(raw, "template transaction journal")
200
- exactKeys(record, ["schemaVersion", "state", "target", "priorTemplate", "priorTemplateDigest", "targetTemplateDigest", "canonicalVersionTag", "reviewedManifestDigest", "rollbackImageId", "targetImageId", "jellyfin", "digest"], "template transaction journal")
202
+ exactKeys(record, ["schemaVersion", "state", "target", "priorTemplate", "priorTemplateDigest", "targetTemplateDigest", "canonicalVersionTag", "reviewedManifestDigest", "rollbackImageId", "targetImageId", "jellyfin", "digest", ...(Object.hasOwn(record, "authority") ? ["authority"] : [])], "template transaction journal")
203
+ if (Object.hasOwn(record, "authority")) validateAuthorityHandoff(record.authority, record)
201
204
  if (record.schemaVersion !== 1 || !["rollback", "committing"].includes(record.state)) throw new Error("template transaction journal state is invalid")
202
205
  const target = object(record.target, "template transaction journal target")
203
206
  exactKeys(target, ["path", "name", "templateUrl", "icon"], "template transaction journal target")
@@ -230,7 +233,7 @@ function readJournal(state) {
230
233
 
231
234
  function writeJournal(record, state) {
232
235
  const complete = { ...record, digest: recordDigest(record) }
233
- atomicWrite(state.journalPath, state.journalTemporary, Buffer.from(`${JSON.stringify(complete)}\n`), { uid: state.expectedUid, gid: state.expectedGid, mode: 0o600 }, state.expectedUid, state.expectedGid)
236
+ atomicWrite(state.journalPath, state.journalTemporary, Buffer.from(`${JSON.stringify(complete)}\n`), { uid: state.expectedUid, gid: state.expectedGid, mode: 0o600 })
234
237
  return complete
235
238
  }
236
239
 
@@ -344,7 +347,7 @@ export function prepareDockerManTemplateTransaction(input, options = {}) {
344
347
  }
345
348
  const record = writeJournal(unsigned, state)
346
349
  options.checkpoint?.("after-template-journal")
347
- atomicWrite(state.targetPath, state.targetTemporary, sourceBytes, { uid: state.expectedUid, gid: state.expectedGid, mode: 0o600 }, state.expectedUid, state.expectedGid)
350
+ atomicWrite(state.targetPath, state.targetTemporary, sourceBytes, { uid: state.expectedUid, gid: state.expectedGid, mode: 0o600 })
348
351
  options.checkpoint?.("after-template-replacement")
349
352
  return record
350
353
  }
@@ -354,6 +357,7 @@ export function markDockerManTemplateTransactionCommitting(options = {}) {
354
357
  validateParents(state)
355
358
  const current = readJournal(state)
356
359
  if (!current) throw new Error("template transaction journal is absent")
360
+ requireActiveAuthority(current, state)
357
361
  if (currentTargetDigest(state) !== current.targetTemplateDigest) throw new Error("installed DockerMan template does not match the transaction")
358
362
  assertJellyfinUnchanged(current.jellyfin)
359
363
  if (current.state === "committing") {
@@ -371,11 +375,12 @@ export function rollbackDockerManTemplateTransaction(options = {}) {
371
375
  validateParents(state)
372
376
  const current = readJournal(state)
373
377
  if (!current) return false
378
+ if (current.authority && current.authority.state !== "retired") throw new Error("authority epoch must be retired before template rollback")
374
379
  const installedDigest = currentTargetDigest(state)
375
380
  assertJellyfinUnchanged(current.jellyfin)
376
381
  if (current.priorTemplate.present) {
377
382
  if (installedDigest !== current.targetTemplateDigest && installedDigest !== current.priorTemplateDigest) throw new Error("installed DockerMan template cannot be safely restored")
378
- if (installedDigest !== current.priorTemplateDigest) atomicWrite(state.targetPath, state.targetTemporary, current.priorBytes, current.priorTemplate.metadata, state.expectedUid, state.expectedGid)
383
+ if (installedDigest !== current.priorTemplateDigest) atomicWrite(state.targetPath, state.targetTemporary, current.priorBytes, current.priorTemplate.metadata)
379
384
  } else if (installedDigest !== null) {
380
385
  if (installedDigest !== current.targetTemplateDigest) throw new Error("installed DockerMan template cannot be safely removed")
381
386
  deleteDurably(state.targetPath)
@@ -410,6 +415,7 @@ export function commitDockerManTemplateTransaction(proof, options = {}) {
410
415
  validateParents(state)
411
416
  const current = readJournal(state)
412
417
  if (!current) return false
418
+ requireActiveAuthority(current, state)
413
419
  if (current.state !== "committing" || currentTargetDigest(state) !== current.targetTemplateDigest) throw new Error("template transaction is not ready to commit")
414
420
  validateFinalProof(proof, current, state)
415
421
  assertJellyfinUnchanged(current.jellyfin)
@@ -417,6 +423,146 @@ export function commitDockerManTemplateTransaction(proof, options = {}) {
417
423
  return true
418
424
  }
419
425
 
426
+ function requireActiveAuthority(current, state) {
427
+ const document = xmlValidator.parseDockerManTemplateXml(readFileSync(state.targetPath))
428
+ const gateway = document?.children.some((child) => child.name === "Config" && child.attributes.Target === "/run/ouro-authority")
429
+ if ((gateway || current.authority) && current.authority?.state !== "active") throw new Error("authority handoff is not verified")
430
+ }
431
+
432
+ function validateAuthorityPlan(raw) {
433
+ const plan = object(raw, "authority handoff plan")
434
+ exactKeys(plan, ["schemaVersion", "epochId", "packageDigest", "publicKeyDigest", "botId", "ownerUserId", "ownerChatId", "predecessorContract", "targetContract"], "authority handoff plan")
435
+ if (plan.schemaVersion !== 1 || !/^[A-Za-z0-9_-]{1,128}$/u.test(plan.epochId) || !IMAGE_ID.test(plan.packageDigest) || (plan.publicKeyDigest !== null && !IMAGE_ID.test(plan.publicKeyDigest))
436
+ || !["botId", "ownerUserId", "ownerChatId"].every((key) => typeof plan[key] === "string" && /^[1-9][0-9]*$/u.test(plan[key]))
437
+ || plan.ownerUserId !== plan.ownerChatId || plan.predecessorContract !== "canonical-pre-gateway" || plan.targetContract !== "canonical-gateway") throw new Error("authority handoff plan is invalid")
438
+ return plan
439
+ }
440
+
441
+ function authorityEffectIdentity(record, effect) {
442
+ return digest(Buffer.from(JSON.stringify([record.targetImageId, record.rollbackImageId, record.reviewedManifestDigest, record.authority.plan, effect.direction, effect.step, effect.beforeDigest, effect.afterDigest])))
443
+ }
444
+
445
+ function validateAuthorityHandoff(raw, record) {
446
+ const handoff = object(raw, "authority handoff")
447
+ exactKeys(handoff, ["plan", "state", "completed", "rollbackCompleted", "pending", "cancelled"], "authority handoff")
448
+ validateAuthorityPlan(handoff.plan)
449
+ if (!["installing", "active", "retiring", "retired"].includes(handoff.state) || !Array.isArray(handoff.completed) || !Array.isArray(handoff.rollbackCompleted) || !Array.isArray(handoff.cancelled)) throw new Error("authority handoff state is invalid")
450
+ const validateEffect = (effect, direction, step) => {
451
+ object(effect, "authority effect")
452
+ exactKeys(effect, ["direction", "step", "effectId", "beforeDigest", "afterDigest"], "authority effect")
453
+ if (effect.direction !== direction || effect.step !== step || !IMAGE_ID.test(effect.beforeDigest) || !IMAGE_ID.test(effect.afterDigest) || effect.beforeDigest === effect.afterDigest
454
+ || effect.effectId !== authorityEffectIdentity(record, effect)) throw new Error("authority effect identity is invalid")
455
+ }
456
+ for (const [direction, entries, steps] of [["install", handoff.completed, AUTHORITY_INSTALL_STEPS], ["rollback", handoff.rollbackCompleted, AUTHORITY_ROLLBACK_STEPS]]) {
457
+ if (entries.length > steps.length) throw new Error("authority effect order is invalid")
458
+ entries.forEach((effect, index) => validateEffect(effect, direction, steps[index]))
459
+ }
460
+ const rollback = handoff.state === "retiring" || handoff.state === "retired"
461
+ if (handoff.cancelled.length > 1 || (handoff.cancelled.length !== 0 && !rollback)) throw new Error("authority cancelled effect is invalid")
462
+ for (const effect of handoff.cancelled) validateEffect(effect, "install", AUTHORITY_INSTALL_STEPS[handoff.completed.length])
463
+ if ((!rollback && handoff.rollbackCompleted.length !== 0) || (handoff.state === "active" && handoff.completed.length !== AUTHORITY_INSTALL_STEPS.length)
464
+ || (handoff.state === "retired" && handoff.rollbackCompleted.length !== AUTHORITY_ROLLBACK_STEPS.length)) throw new Error("authority terminal handoff is invalid")
465
+ if (handoff.pending !== null) {
466
+ if (handoff.state === "active" || handoff.state === "retired") throw new Error("authority terminal handoff has a pending effect")
467
+ const steps = rollback ? AUTHORITY_ROLLBACK_STEPS : AUTHORITY_INSTALL_STEPS
468
+ const completed = rollback ? handoff.rollbackCompleted : handoff.completed
469
+ validateEffect(handoff.pending, rollback ? "rollback" : "install", steps[completed.length])
470
+ }
471
+ return handoff
472
+ }
473
+
474
+ function writeAuthorityHandoff(record, authority, state) {
475
+ const { priorBytes: _bytes, digest: _digest, ...unsigned } = record
476
+ const next = { ...unsigned, authority }
477
+ validateAuthorityHandoff(authority, next)
478
+ return writeJournal(next, state)
479
+ }
480
+
481
+ export function beginDockerManAuthorityHandoff(plan, options = {}) {
482
+ validateAuthorityPlan(plan)
483
+ const state = paths(options)
484
+ validateParents(state)
485
+ const current = readJournal(state)
486
+ if (!current || (!current.authority && current.state !== "rollback")) throw new Error("authority handoff requires the prepared deployment transaction")
487
+ assertJellyfinUnchanged(current.jellyfin)
488
+ if (current.authority) {
489
+ if (JSON.stringify(current.authority.plan) !== JSON.stringify(plan)) throw new Error("authority handoff plan changed")
490
+ return current.authority
491
+ }
492
+ return writeAuthorityHandoff(current, { plan, state: "installing", completed: [], rollbackCompleted: [], cancelled: [], pending: null }, state).authority
493
+ }
494
+
495
+ async function withAuthorityLease(options, work) {
496
+ const state = paths(options)
497
+ validateParents(state)
498
+ const withLease = options.withLease ?? (await import("../../dist/mind/session-transaction.js")).withSessionTurnLease
499
+ return withLease(state.journalPath, () => work(state), { timeoutMs: 0, confinementRoot: state.journalParent })
500
+ }
501
+
502
+ export async function runDockerManAuthorityEffect(name, effect, options = {}) {
503
+ return withAuthorityLease(options, (state) => runAuthorityEffectLocked(name, effect, state))
504
+ }
505
+
506
+ export async function beginDockerManAuthorityRollback(readback, options = {}) {
507
+ return withAuthorityLease(options, (state) => beginAuthorityRollbackLocked(readback, state))
508
+ }
509
+
510
+ async function beginAuthorityRollbackLocked(readback, state) {
511
+ const current = readJournal(state)
512
+ if (!current?.authority) throw new Error("authority handoff is absent")
513
+ const handoff = current.authority
514
+ if (handoff.state === "retired" || handoff.state === "retiring") return handoff
515
+ const pending = handoff.pending
516
+ const observed = pending ? await readback() : null
517
+ if (pending && observed !== pending.beforeDigest && observed !== pending.afterDigest) throw new Error("authority pending effect is ambiguous")
518
+ if (readJournal(state).digest !== current.digest) throw new Error("authority transaction changed during recovery")
519
+ assertJellyfinUnchanged(current.jellyfin)
520
+ return writeAuthorityHandoff(current, {
521
+ ...handoff, state: "retiring", pending: null,
522
+ completed: pending && observed === pending.afterDigest ? [...handoff.completed, pending] : handoff.completed,
523
+ cancelled: pending && observed === pending.beforeDigest ? [...handoff.cancelled, pending] : handoff.cancelled,
524
+ }, state).authority
525
+ }
526
+
527
+ async function runAuthorityEffectLocked(name, effect, state) {
528
+ let current = readJournal(state)
529
+ if (!current?.authority) throw new Error("authority handoff is absent")
530
+ assertJellyfinUnchanged(current.jellyfin)
531
+ const direction = name.startsWith("rollback:") ? "rollback" : "install"
532
+ const step = direction === "rollback" ? name.slice("rollback:".length) : name
533
+ const steps = direction === "rollback" ? AUTHORITY_ROLLBACK_STEPS : AUTHORITY_INSTALL_STEPS
534
+ if (!steps.includes(step)) throw new Error("authority handoff step is invalid")
535
+ if (!IMAGE_ID.test(effect.beforeDigest) || !IMAGE_ID.test(effect.afterDigest) || effect.beforeDigest === effect.afterDigest) throw new Error("authority effect identity is invalid")
536
+ let handoff = current.authority
537
+ if (direction === "install" && ["retiring", "retired"].includes(handoff.state)) throw new Error("authority handoff is retiring")
538
+ const expected = { direction, step, beforeDigest: effect.beforeDigest, afterDigest: effect.afterDigest }
539
+ const intent = { ...expected, effectId: authorityEffectIdentity(current, expected) }
540
+ const completedKey = direction === "install" ? "completed" : "rollbackCompleted"
541
+ const completed = handoff[completedKey]
542
+ const prior = completed.find((entry) => entry.step === step)
543
+ if (prior) {
544
+ if (prior.effectId !== intent.effectId || await effect.readback() !== prior.afterDigest) throw new Error("authority completed effect changed")
545
+ return prior
546
+ }
547
+ if (steps[completed.length] !== step) throw new Error("authority handoff effect order is invalid")
548
+ if (handoff.pending && handoff.pending.effectId !== intent.effectId) throw new Error("authority pending effect changed; recovery is required before rollback")
549
+ if (!handoff.pending) {
550
+ handoff = { ...handoff, state: direction === "rollback" ? "retiring" : "installing", pending: intent }
551
+ current = writeAuthorityHandoff(current, handoff, state)
552
+ }
553
+ const observed = await effect.readback()
554
+ if (observed !== intent.beforeDigest && observed !== intent.afterDigest) throw new Error("authority handoff effect is ambiguous")
555
+ if (observed === intent.beforeDigest) await effect.apply(intent.effectId)
556
+ if (await effect.readback() !== intent.afterDigest) throw new Error("authority handoff effect readback failed")
557
+ const latest = readJournal(state)
558
+ if (latest.digest !== current.digest) throw new Error("authority transaction changed during effect")
559
+ assertJellyfinUnchanged(latest.jellyfin)
560
+ const entries = [...completed, intent]
561
+ handoff = { ...handoff, [completedKey]: entries, pending: null, state: entries.length === steps.length ? direction === "install" ? "active" : "retired" : direction === "install" ? "installing" : "retiring" }
562
+ writeAuthorityHandoff(latest, handoff, state)
563
+ return intent
564
+ }
565
+
420
566
  export function verifyDockerManTemplateTransactionJellyfin(options = {}) {
421
567
  const state = paths(options)
422
568
  validateParents(state)
@@ -472,6 +618,9 @@ function readRootJson(path, label) {
472
618
 
473
619
  export function runDockerManTemplateTransactionCli(argv, options = {}, write = (text) => process.stdout.write(text)) {
474
620
  const { operation, values } = parseArguments(argv)
621
+ if (operation?.startsWith("authority-") && values.size === 0) {
622
+ return runRootLifecycle(operation, options).then((result) => write(`${JSON.stringify(result)}\n`))
623
+ }
475
624
  let result
476
625
  if (operation === "prepare" && values.size === 5) {
477
626
  result = prepareDockerManTemplateTransaction({
@@ -523,4 +672,40 @@ export function runDockerManTemplateTransactionCli(argv, options = {}, write = (
523
672
  write(`${JSON.stringify(result)}\n`)
524
673
  }
525
674
 
526
- if (process.argv[1] === fileURLToPath(import.meta.url)) runDockerManTemplateTransactionCli(process.argv.slice(2))
675
+ async function runRootLifecycle(operation, options) {
676
+ const selected = {
677
+ "authority-install": AUTHORITY_INSTALL_STEPS.slice(0, 6),
678
+ "authority-activate": AUTHORITY_INSTALL_STEPS.slice(6),
679
+ "authority-retire": AUTHORITY_ROLLBACK_STEPS.slice(0, 6).map((step) => `rollback:${step}`),
680
+ "authority-restore": AUTHORITY_ROLLBACK_STEPS.slice(6).map((step) => `rollback:${step}`),
681
+ }[operation]
682
+ if (!selected) throw new Error("authority lifecycle operation is invalid")
683
+ return withAuthorityLease(options, async (state) => {
684
+ let record = readJournal(state)
685
+ if (!record) throw new Error("authority lifecycle requires the prepared deployment transaction")
686
+ const RootLifecycle = options.RootLifecycle ?? (await import("../../dist/heart/daemon/sanctuary-authority-root-lifecycle.js")).SanctuaryAuthorityRootLifecycle
687
+ const lifecycle = new RootLifecycle(record, options.rootLifecycleOptions)
688
+ beginDockerManAuthorityHandoff(lifecycle.plan(), options)
689
+ record = readJournal(state)
690
+ if (["authority-install", "authority-activate"].includes(operation) && ["retiring", "retired"].includes(record.authority.state)) throw new Error("authority epoch is retiring or retired")
691
+ if (operation === "authority-activate" && record.authority.completed.length < 6) throw new Error("gateway installation must finish before resident activation")
692
+ if (operation === "authority-restore" && record.authority.rollbackCompleted.length < 6) throw new Error("authority retirement must finish before resident restoration")
693
+ if (operation === "authority-retire") {
694
+ const pending = record.authority.pending
695
+ await beginAuthorityRollbackLocked(pending ? lifecycle.effect(pending.step).readback : null, state)
696
+ }
697
+ for (const name of selected) {
698
+ const rollback = name.startsWith("rollback:")
699
+ const step = rollback ? name.slice("rollback:".length) : name
700
+ const current = readJournal(state).authority
701
+ // Completed historical effects may have intentionally changed at later
702
+ // steps (a frozen resident becomes running). Recheck the next real boundary,
703
+ // never replay earlier physical work merely to reproduce a historical hash.
704
+ if (current[rollback ? "rollbackCompleted" : "completed"].some((entry) => entry.step === step)) continue
705
+ await runAuthorityEffectLocked(name, lifecycle.effect(name), state)
706
+ }
707
+ return readJournal(state).authority
708
+ })
709
+ }
710
+
711
+ if (process.argv[1] === fileURLToPath(import.meta.url)) await runDockerManTemplateTransactionCli(process.argv.slice(2))
@@ -6,7 +6,7 @@ VAULT_ENTRY='import("/opt/ouro/dist/heart/daemon/sanctuary-acceptance-adapter.js
6
6
  REVOKED_ENTRY='import fs from "node:fs"; import("/opt/ouro/dist/heart/daemon/sanctuary-acceptance-adapter.js").then(async (module) => { const result = await module.executeSanctuaryAcceptanceRevokedProbe(process.argv[1], process.argv[2], fs.readFileSync(3, "utf8")); process.stdout.write(JSON.stringify(result)); }).catch(() => { process.exitCode = 1; });'
7
7
  CALLBACK_ENTRY='import fs from "node:fs"; import("/opt/ouro/dist/heart/daemon/sanctuary-acceptance-adapter.js").then(async (module) => { const result = await module.executeSanctuaryAcceptanceCallbackProbe(JSON.parse(fs.readFileSync(0, "utf8")), process.argv[1] === "replay"); process.stdout.write(JSON.stringify(result)); }).catch(() => { process.exitCode = 1; });'
8
8
  MATERIALIZE_ENTRY='import("/opt/ouro/dist/heart/daemon/sanctuary-acceptance-adapter.js").then(async (module) => { const payload = { operation: "materialize_config", command: process.argv[1] }; if (process.argv[2]) payload.phase = process.argv[2]; const result = await module.executeSanctuaryAcceptanceAdapter(payload); process.stdout.write(JSON.stringify(result)); }).catch(() => { process.exitCode = 1; });'
9
- TELEGRAM_READINESS_ENTRY='import("/opt/ouro/dist/heart/daemon/sanctuary-acceptance-adapter.js").then(async (module) => { const result = await module.executeSanctuaryAcceptanceAdapter({ operation: "telegram_readiness" }); process.stdout.write(JSON.stringify(result)); }).catch((error) => { const categories = new Set(["Telegram runtime credentials are unavailable; actor: human-required; unlock or repair vault runtime/config", "Telegram runtime credentials are invalid; actor: human-required; repair vault runtime/config", "Telegram client initialization failed; actor: agent-runnable; retry Telegram readiness", "Telegram getMe failed; actor: agent-runnable; retry Telegram readiness", "Telegram client cleanup failed; actor: agent-runnable; retry Telegram readiness", "Telegram bot identity mismatch; actor: human-required; repair vault runtime/config"]); const message = String(error?.message ?? ""); const category = categories.has(message) ? message : "Telegram readiness failed; actor: agent-runnable; inspect packaged readiness"; process.stderr.write(`${category}\n`); process.exitCode = 1; });'
9
+ TELEGRAM_READINESS_ENTRY='import("/opt/ouro/dist/heart/daemon/sanctuary-acceptance-adapter.js").then(async (module) => { const result = await module.executeSanctuaryAcceptanceAdapter({ operation: "telegram_readiness" }); process.stdout.write(JSON.stringify(result)); }).catch((error) => { const categories = new Set(["Telegram gateway inventory or pins are unavailable; actor: agent-runnable; inspect root migration", "Telegram gateway health or identity probe failed; actor: agent-runnable; inspect root authority", "Telegram client cleanup failed; actor: agent-runnable; retry Telegram readiness", "Telegram bot identity mismatch; actor: human-required; repair root gateway identity"]); const message = String(error?.message ?? ""); const category = categories.has(message) ? message : "Telegram readiness failed; actor: agent-runnable; inspect packaged readiness"; process.stderr.write(`${category}\n`); process.exitCode = 1; });'
10
10
 
11
11
  if test "${1:-}" = vault-probe; then
12
12
  test "$#" -eq 3 || exit 2
@@ -3,7 +3,7 @@
3
3
  "harnessExecutable": "/opt/ouro/deploy/unraid/sanctuary-acceptance-harness.sh",
4
4
  "adapterExecutable": "/opt/ouro/deploy/unraid/sanctuary-acceptance-adapter.sh",
5
5
  "adapterTimeoutMs": 240000,
6
- "telegramTimeoutMs": 10000,
6
+ "telegramTimeoutMs": 65000,
7
7
  "deploymentTargetProfiles": {
8
8
  "staging": { "command": "sanctuary-unit16-run.sh", "acceptanceInvocation": "sanctuary-unit16-run.sh <image-id> ...", "containerName": "ouro-butler-staging", "requiredRunning": 1, "restartPolicy": "unless-stopped", "networkMode": "host", "inboundTcpListeners": 0, "inboundUdpListeners": 0, "loopbackTcpControls": [6876] },
9
9
  "final": { "command": "sanctuary-unit18-target-audit.sh", "acceptanceInvocation": "sanctuary-unit16-run.sh <image-id> --profile final ...", "containerName": "ouro-butler", "requiredRunning": 1, "optionalStopped": "ouro-butler-rollback", "restartPolicy": "unless-stopped", "networkMode": "host", "inboundTcpListeners": 0, "inboundUdpListeners": 0, "loopbackTcpControls": [6876] }
@@ -11,9 +11,9 @@
11
11
  "configMaterializer": {
12
12
  "invocation": "sanctuary-acceptance-adapter.sh materialize-config <command> [before|after]",
13
13
  "output": "complete harness config JSON on stdout",
14
- "authority": "fixed-contract-offset-inventory-evidence-read",
14
+ "authority": "fixed-contract-signed-gateway-cursor-inventory-evidence-read",
15
15
  "modelReachable": false,
16
- "timeoutMs": 15000
16
+ "timeoutMs": 90000
17
17
  },
18
18
  "commands": {
19
19
  "telegram-bootstrap": { "invocation": "sanctuary-acceptance-harness.sh telegram-bootstrap --config <private-json>" },
@@ -28,7 +28,7 @@
28
28
  "evidence-bundle-verify": { "invocation": "sanctuary-acceptance-harness.sh evidence-bundle-verify --config <private-json>" }
29
29
  },
30
30
  "adapters": {
31
- "telegram-poller-quiescence": { "operation": "quiesce_telegram_poller", "authority": "fixed-managed-telegram-poller-stop-and-count", "modelReachable": false, "timeoutMs": 15000 },
31
+ "telegram-poller-quiescence": { "operation": "quiesce_telegram_poller", "authority": "fresh-pinned-gateway-health-and-root-lock-process-with-stopped-resident", "modelReachable": false, "timeoutMs": 120000 },
32
32
  "cursor-snapshot": { "operation": "snapshot", "authority": "fixed-telegram-state-read", "modelReachable": false, "timeoutMs": 15000 },
33
33
  "callback-live": { "operation": "callback_playback_preflight|inject_callbacks_concurrently|inject_callback_replay", "authority": "fixed-live-telegram-callback-probe", "modelReachable": false, "timeoutMs": 15000 },
34
34
  "key-inventory": { "operation": "inventory_keys", "authority": "fixed-unraid-key-directory-read", "modelReachable": false, "timeoutMs": 15000 },
@@ -50,7 +50,7 @@
50
50
  "callback-inject": { "operation": "callback-inject", "authority": "fixed-harness-orchestration", "modelReachable": false, "timeoutMs": 120000 },
51
51
  "reboot-request": { "operation": "reboot-request", "authority": "fixed-harness-orchestration", "modelReachable": false, "timeoutMs": 120000 },
52
52
  "unraid-key-rotate": { "operation": "unraid-key-rotate", "authority": "fixed-harness-orchestration", "modelReachable": false, "timeoutMs": 120000 },
53
- "config-materializer": { "operation": "materialize_config", "authority": "fixed-contract-offset-inventory-evidence-read", "modelReachable": false, "timeoutMs": 120000 },
53
+ "config-materializer": { "operation": "materialize_config", "authority": "fixed-contract-signed-gateway-cursor-inventory-evidence-read", "modelReachable": false, "timeoutMs": 120000 },
54
54
  "scenario-capture": { "operation": "capture_acceptance_scenario", "authority": "fixed-runtime-audit-telegram-container-scenario", "modelReachable": false, "timeoutMs": 210000, "phases": ["begin", "poll"], "publicGateStatus": "/evidence/current-scenario-gate.json contains only label,gate,phase,startedAt", "response": "waiting:{state,checkpointDigest}|complete:{state,checkpointDigest,sourceDigests,assertions}" },
55
55
  "scenario-finalize": { "operation": "finalize_acceptance_scenarios", "authority": "fixed-public-gate-private-marker-receipt-cleanup", "modelReachable": false, "timeoutMs": 120000, "response": "{finalized:true}" },
56
56
  "health-probe-start": { "operation": "start_health_probe", "authority": "fixed-owner-bound-packaged-health-probe-start", "modelReachable": false, "timeoutMs": 170000, "response": "{state:started,operationDigest}" },
@@ -64,7 +64,7 @@
64
64
  "identity-key": { "kind": "fixed-file", "path": "/home/ouro/AgentBundles/sanctuary.ouro/state/senses/telegram/identity.key", "redaction": "sha256-only" },
65
65
  "identity-surface-audit": { "kind": "bounded-bundle-surface-audit", "path": "/home/ouro/AgentBundles/sanctuary.ouro", "redaction": "record-match-raw-leak-counts-and-surface-digest-only" },
66
66
  "telegram-audit": { "kind": "fixed-mac-chain", "path": "/home/ouro/AgentBundles/sanctuary.ouro/state/acceptance/telegram-audit-chain.ndjson", "headPath": "/home/ouro/AgentBundles/sanctuary.ouro/state/acceptance/telegram-audit-chain.head.json", "redaction": "canonical-redacted-rows-with-hmac-chain" },
67
- "telegram-offset": { "kind": "fixed-json", "path": "/home/ouro/AgentBundles/sanctuary.ouro/state/senses/telegram/offset.json", "redaction": "sha256-only" },
67
+ "telegram-offset": { "kind": "signed-gateway-cursor", "operation": "telegram.cursor.snapshot", "redaction": "stable-logical-progress-digest-only" },
68
68
  "approval-journal": { "kind": "fixed-sqlite", "path": "/home/ouro/AgentBundles/sanctuary.ouro/state/approvals/approvals.sqlite", "redaction": "typed-state-counters-and-digests-only" },
69
69
  "approval-checkpoints": { "kind": "fixed-json", "path": "/home/ouro/AgentBundles/sanctuary.ouro/state/approvals/checkpoints.json", "redaction": "typed-state-counters-and-digests-only" },
70
70
  "restart-attempt-ledger": { "kind": "fixed-ndjson", "path": "/home/ouro/AgentBundles/sanctuary.ouro/state/acceptance/restart-attempts.ndjson", "redaction": "scenario-action-target-digests-and-state-only" },
@@ -93,7 +93,7 @@
93
93
  "deadlineMs": 600000,
94
94
  "pollTimeoutSeconds": 20
95
95
  },
96
- "dynamic": ["expectedBotId:getMe.id", "expectedUsername:getMe.username", "currentOffset:fixed-cursor-read"]
96
+ "dynamic": ["expectedBotId:pinned-gateway-getMe.id", "expectedUsername:pinned-gateway-getMe.username", "currentOffset:signed-gateway-cursor"]
97
97
  },
98
98
  "cursor-snapshot": {
99
99
  "fixed": {
@@ -0,0 +1,36 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "rootUid": 0,
4
+ "rootGid": 0,
5
+ "residentUid": 10001,
6
+ "residentGid": 10001,
7
+ "stateRoot": "/mnt/user/appdata/ouro-authority",
8
+ "stateMode": 448,
9
+ "secretMode": 384,
10
+ "packageRoot": "/mnt/user/appdata/ouro-authority/package",
11
+ "activeConfigPath": "/mnt/user/appdata/ouro-authority/active.json",
12
+ "requestPath": "/mnt/user/appdata/ouro-authority/request.json",
13
+ "incomingTokenPath": "/mnt/user/appdata/ouro-authority/incoming-token",
14
+ "incomingPackagePath": "/mnt/user/appdata/ouro-authority/incoming-package",
15
+ "packageManifestPath": "/mnt/user/appdata/ouro-authority/package-manifest.json",
16
+ "bootPath": "/boot/config/custom/ouro-authority/start.sh",
17
+ "socketRoot": "/run/ouro-authority",
18
+ "socketDirectoryMode": 488,
19
+ "socketMode": 432,
20
+ "residentPinsMode": 416,
21
+ "socketPath": "/run/ouro-authority/authority.sock",
22
+ "residentPinsPath": "/run/ouro-authority/resident.json",
23
+ "stagingRoot": "/var/lib/ouro-authority/staging",
24
+ "cgroupRoot": "/sys/fs/cgroup/ouro-authority",
25
+ "controllers": ["cpu", "memory", "pids"],
26
+ "sourceMountContract": "canonical-pre-gateway",
27
+ "targetMountContract": "canonical-gateway",
28
+ "servicePath": "/mnt/user/appdata/ouro-authority/package/deploy/unraid/sanctuary-authority-service.sh",
29
+ "serviceMode": 448,
30
+ "nodePath": "/usr/local/bin/node",
31
+ "prlimitPath": "/usr/bin/prlimit",
32
+ "setsidPath": "/usr/bin/setsid",
33
+ "shellPath": "/bin/sh",
34
+ "transactionOwner": "docker-man-template-transaction.mjs",
35
+ "epochRetirementRequiredBeforeRollback": true
36
+ }
@@ -0,0 +1,30 @@
1
+ #!/bin/sh
2
+ set -eu
3
+ umask 077
4
+ test "$(id -u):$(id -g)" = 0:0
5
+ cd /
6
+ case "${1-}" in
7
+ --boot)
8
+ test "$#" -eq 1
9
+ # Unraid's go hook can run before the array and Docker are available.
10
+ attempt=0
11
+ while test ! -f /mnt/user/appdata/ouro-authority/active.json || test ! -S /var/run/docker.sock || ! /usr/bin/docker info >/dev/null 2>&1; do
12
+ attempt=$((attempt + 1))
13
+ test "$attempt" -le 300
14
+ sleep 1
15
+ done
16
+ exec /usr/bin/env -i PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
17
+ /usr/local/bin/node /mnt/user/appdata/ouro-authority/package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js boot </dev/null
18
+ ;;
19
+ "") test "$#" -eq 0 ;;
20
+ *) exit 2 ;;
21
+ esac
22
+ test -d /mnt/user/appdata/ouro-authority
23
+ test ! -L /mnt/user/appdata/ouro-authority
24
+ test "$(stat -c '%u:%g:%a' /mnt/user/appdata/ouro-authority)" = 0:0:700
25
+ test -f /mnt/user/appdata/ouro-authority/active.json
26
+ test ! -L /mnt/user/appdata/ouro-authority/active.json
27
+ test "$(stat -c '%u:%g:%a' /mnt/user/appdata/ouro-authority/active.json)" = 0:0:600
28
+ exec /usr/bin/env -i PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
29
+ /usr/local/bin/node /mnt/user/appdata/ouro-authority/package/dist/heart/daemon/sanctuary-telegram-authority-entry.js \
30
+ --config /mnt/user/appdata/ouro-authority/active.json </dev/null
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { spawn, spawnSync } from "node:child_process"
4
4
  import { createHash, createHmac, timingSafeEqual } from "node:crypto"
5
- import { chmodSync, chownSync, closeSync, constants, fstatSync, fsyncSync, mkdirSync, openSync, opendirSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs"
5
+ import { chmodSync, chownSync, closeSync, constants, fstatSync, fsyncSync, mkdirSync, openSync, opendirSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs"
6
6
  import { createServer } from "node:net"
7
7
  import { targetProfile } from "./sanctuary-deployment-target.mjs"
8
8
 
@@ -18,6 +18,7 @@ const RUNTIME_POLICY_FILE = "/opt/ouro/container-runtime.json"
18
18
  const PRODUCTION_RUNTIME_SOURCE = "/mnt/user/appdata/ouro-butler/runtime/.ouro-cli"
19
19
  const PRODUCTION_BUNDLE_SOURCE = "/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro"
20
20
  const PRODUCTION_EVENT_SPOOL_SOURCE = "/boot/config/custom/ouro-events/spool"
21
+ const AUTHORITY_ROOT = "/mnt/user/appdata/ouro-authority"
21
22
  const GRAPHQL_ENDPOINT = "http://127.0.0.1/graphql"
22
23
  const BOOT_ID = "/proc/sys/kernel/random/boot_id"
23
24
  const MDCMD = "/usr/local/sbin/mdcmd"
@@ -361,8 +362,8 @@ function parseVaultStatus(output, succeeded) {
361
362
  return match[1].split(", ").includes("apiKey") && match[2].split(", ").includes("baseUrl")
362
363
  }
363
364
  const unlocked = succeeded && /^local unlock: available$/mu.test(output)
364
- && ["telegramBotToken", "telegramAuthorizedUserId", "telegramAuthorizedChatId"].every((field) => runtimeFields.includes(field))
365
- && providerReady("openai-compatible") && providerReady("openai-compatible-gemini")
365
+ && runtimeMatch !== null && !runtimeFields.includes("telegramBotToken")
366
+ && providerReady("minimax")
366
367
  return { vaultUnlocked: unlocked, manualAuthRequired: !unlocked }
367
368
  }
368
369
 
@@ -503,6 +504,7 @@ async function containerSnapshot(expectedImage) {
503
504
  { destination: "/home/ouro/.ouro-cli", source: PRODUCTION_RUNTIME_SOURCE, propagation: "rprivate", rw: true, type: "bind" },
504
505
  { destination: "/home/ouro/AgentBundles/sanctuary.ouro", source: PRODUCTION_BUNDLE_SOURCE, propagation: "rprivate", rw: true, type: "bind" },
505
506
  { destination: "/run/ouro-events", source: PRODUCTION_EVENT_SPOOL_SOURCE, propagation: "rprivate", rw: false, type: "bind" },
507
+ { destination: "/run/ouro-authority", source: "/run/ouro-authority", propagation: "rprivate", rw: false, type: "bind" },
506
508
  ]
507
509
  const mountsExact = mounts.length === expectedMounts.length && expectedMounts.every((expected) => mounts.some((mount) => mount.destination === expected.destination && mount.source === expected.source && mount.propagation === expected.propagation && mount.rw === expected.rw && mount.type === expected.type))
508
510
  const securityExact = value.privileged === false && (value.capAdd === null || (Array.isArray(value.capAdd) && value.capAdd.length === 0))
@@ -552,7 +554,7 @@ async function containerSnapshot(expectedImage) {
552
554
  }
553
555
  }
554
556
 
555
- function inspectRebootOwner(containerId = PRODUCTION_CONTAINER) {
557
+ function inspectRebootOwner(containerId = activeContainerId) {
556
558
  const template = '{"containerId":{{json .Id}},"name":{{json .Name}},"imageId":{{json .Image}},"running":{{json .State.Running}},"pid":{{json .State.Pid}},"startedAt":{{json .State.StartedAt}},"health":{{json .State.Health.Status}},"restartCount":{{json .RestartCount}}}'
557
559
  const result = spawnSync(DOCKER, ["inspect", "--format", template, containerId], {
558
560
  cwd: "/", encoding: "utf8", timeout: 20_000, maxBuffer: 64 * 1024,
@@ -562,9 +564,55 @@ function inspectRebootOwner(containerId = PRODUCTION_CONTAINER) {
562
564
  return object(JSON.parse(result.stdout ?? ""), "reboot owner inspection")
563
565
  }
564
566
 
567
+ function readAuthorityText(file) {
568
+ if (!file.startsWith(`${AUTHORITY_ROOT}/`) || realpathSync(file) !== file) throw new Error("root authority path is unsafe")
569
+ const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW)
570
+ try {
571
+ const stat = fstatSync(fd)
572
+ if (!stat.isFile() || stat.uid !== 0 || stat.gid !== 0 || stat.nlink !== 1 || (stat.mode & 0o7777) !== 0o600 || stat.size < 1 || stat.size > 65536) throw new Error("root authority metadata is unsafe")
573
+ return readFileSync(fd, "utf8")
574
+ } finally { closeSync(fd) }
575
+ }
576
+
577
+ function telegramGatewayQuiescence() {
578
+ const observe = () => {
579
+ const config = object(JSON.parse(readAuthorityText(`${AUTHORITY_ROOT}/active.json`)), "root authority configuration")
580
+ const keyId = text(config.keyId, "root issuer", /^[A-Za-z0-9_-]{1,128}$/u)
581
+ const botId = text(config.botId, "root bot identity", /^[1-9][0-9]*$/u)
582
+ const publicKeyDigest = text(config.publicKeyDigest, "root issuer digest", /^sha256:[a-f0-9]{64}$/u)
583
+ const epochRoot = `${AUTHORITY_ROOT}/epochs/${keyId}`
584
+ if (config.lockPath !== `${epochRoot}/authority.lock`) throw new Error("root authority lock path changed")
585
+ const epoch = object(JSON.parse(readAuthorityText(`${epochRoot}/epoch.json`)), "root authority epoch")
586
+ if (epoch.schemaVersion !== 1 || epoch.state !== "prepared" || epoch.epochId !== keyId || epoch.botId !== botId || epoch.publicKeyDigest !== publicKeyDigest
587
+ || epoch.revokedTokenStatus !== 401 || !/^sha256:[a-f0-9]{64}$/u.test(epoch.tokenDigest) || !/^sha256:[a-f0-9]{64}$/u.test(epoch.previousTokenDigest)
588
+ || epoch.tokenDigest === epoch.previousTokenDigest) throw new Error("root authority token epoch is invalid")
589
+ const pid = Number(readAuthorityText(config.lockPath))
590
+ if (!Number.isSafeInteger(pid) || pid <= 0 || pid > 4_194_304) throw new Error("root authority lock PID is invalid")
591
+ const processes = spawnSync(PGREP, ["-f", "sanctuary-telegram-authority-entry[.]js"], {
592
+ cwd: "/", encoding: "utf8", timeout: 5_000, maxBuffer: 64 * 1024, stdio: ["ignore", "pipe", "ignore"],
593
+ })
594
+ if (processes.error || processes.status !== 0 || processes.stdout.trim() !== String(pid)) throw new Error("root authority does not have exactly one live poller")
595
+ const status = readBoundedProcStatus(`/proc/${pid}/status`)
596
+ if (!status.split("\n").includes("Uid:\t0\t0\t0\t0") || !status.split("\n").includes("Gid:\t0\t0\t0\t0")) throw new Error("root authority process identity is invalid")
597
+ const command = readBoundedProcStatus(`/proc/${pid}/cmdline`)
598
+ if (command !== ["/usr/local/bin/node", `${AUTHORITY_ROOT}/package/dist/heart/daemon/sanctuary-telegram-authority-entry.js`, "--config", `${AUTHORITY_ROOT}/active.json`, ""].join("\0")) throw new Error("root authority process command changed")
599
+ const processStartTime = parseProcStartTime(readBoundedProcStatus(`/proc/${pid}/stat`))
600
+ const resident = inspectRebootOwner(activeContainerId)
601
+ if (resident.containerId !== activeContainerId || resident.name !== `/${activeContainer}` || resident.imageId !== expectedImageId || resident.running !== false || resident.pid !== 0) throw new Error("resident Telegram owner is not stopped")
602
+ return { keyId, botId, publicKeyDigest, pid, processStartTime, bootId: readFileSync(BOOT_ID, "utf8"), resident }
603
+ }
604
+ const before = observe()
605
+ const after = observe()
606
+ if (JSON.stringify(before) !== JSON.stringify(after)) throw new Error("root authority process generation changed")
607
+ return {
608
+ schemaVersion: 1, activePollers: 1, residentStopped: true, keyId: after.keyId, botId: after.botId, publicKeyDigest: after.publicKeyDigest,
609
+ processBindingDigest: createHash("sha256").update(JSON.stringify(after)).digest("hex"), observedAt: new Date().toISOString(),
610
+ }
611
+ }
612
+
565
613
  function runningRebootOwnerGeneration() {
566
614
  const before = inspectRebootOwner()
567
- if (before.name !== `/${PRODUCTION_CONTAINER}` || before.imageId !== expectedImageId || before.running !== true || before.health !== "healthy"
615
+ if (before.name !== `/${activeContainer}` || before.imageId !== expectedImageId || before.running !== true || before.health !== "healthy"
568
616
  || !Number.isSafeInteger(before.pid) || before.pid <= 0 || !Number.isSafeInteger(before.restartCount) || before.restartCount < 0) {
569
617
  throw new Error("reboot owner generation is invalid")
570
618
  }
@@ -591,7 +639,7 @@ function stopExactRebootOwner(expectedBinding) {
591
639
 
592
640
  function verifyStoppedRebootOwner(proof) {
593
641
  const value = inspectRebootOwner(text(proof.containerId, "stopped owner container id", SHA256))
594
- if (value.containerId !== proof.containerId || value.name !== `/${PRODUCTION_CONTAINER}` || value.imageId !== proof.imageId
642
+ if (value.containerId !== proof.containerId || value.name !== `/${activeContainer}` || value.imageId !== proof.imageId
595
643
  || value.restartCount !== proof.restartCount || value.startedAt !== proof.startedAt || value.running !== false || value.pid !== 0) {
596
644
  throw new Error("exact stopped production owner generation changed")
597
645
  }
@@ -1280,7 +1328,7 @@ function createOwnerMutationCoordinator() {
1280
1328
  const enqueue = (operation) => {
1281
1329
  if (rebootReservation !== null) return Promise.reject(new Error("owner mutation refused by reboot reservation"))
1282
1330
  pendingOperations += 1
1283
- const task = ownerTail.catch(() => {}).then(async () => {
1331
+ const task = ownerTail.then(async () => {
1284
1332
  try { return await operation() } finally { pendingOperations -= 1 }
1285
1333
  })
1286
1334
  ownerTail = task.then(() => {}, () => {})
@@ -1294,7 +1342,7 @@ function createOwnerMutationCoordinator() {
1294
1342
  if (rebootReservation !== null) throw new Error("reboot reservation already exists")
1295
1343
  rebootReservation = { id: reservationId, processBindingDigest, stoppedProof: null, attempted: false }
1296
1344
  try {
1297
- await ownerTail.catch(() => {})
1345
+ await ownerTail
1298
1346
  if (pendingOperations !== 0 || activeHealth.size !== 0) throw new Error("reboot reservation could not drain owner mutations")
1299
1347
  return await operation()
1300
1348
  } catch (error) {
@@ -1376,7 +1424,7 @@ function createInteractiveRestartDriver() {
1376
1424
  (error) => { records.set(key, { state: "failed", errorDigest: createHash("sha256").update(interactiveFailureCategory(error)).digest("hex") }) },
1377
1425
  )
1378
1426
  tasks.add(task)
1379
- void task.finally(() => tasks.delete(task)).catch(() => {})
1427
+ void task.finally(() => tasks.delete(task))
1380
1428
  },
1381
1429
  async stopAndDrain() { await Promise.allSettled([...tasks]) },
1382
1430
  }
@@ -1747,6 +1795,11 @@ async function dispatch(request, dependencies = {
1747
1795
  }) {
1748
1796
  const payload = object(request, "broker request")
1749
1797
  const operation = text(payload.operation, "operation")
1798
+ if (operation === "telegram_gateway_quiescence") {
1799
+ exactKeys(payload, ["operation", "targetId"], operation)
1800
+ if (payload.targetId !== TARGET_HOST) throw new Error("target host is invalid")
1801
+ return telegramGatewayQuiescence()
1802
+ }
1750
1803
  if (operation === "inventory_keys") {
1751
1804
  exactKeys(payload, ["operation", "targetServerId"], operation)
1752
1805
  if (payload.targetServerId !== TARGET_SERVER) throw new Error("target server is invalid")