@ouro.bot/cli 0.1.0-alpha.83 → 0.1.0-alpha.830

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 (647) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -23
  3. package/RepairGuide.ouro/agent.json +5 -0
  4. package/RepairGuide.ouro/psyche/IDENTITY.md +19 -0
  5. package/RepairGuide.ouro/psyche/SOUL.md +55 -0
  6. package/RepairGuide.ouro/skills/diagnose-broken-remote.md +63 -0
  7. package/RepairGuide.ouro/skills/diagnose-stacked-typed-issues.md +35 -0
  8. package/RepairGuide.ouro/skills/diagnose-sync-blocked.md +54 -0
  9. package/RepairGuide.ouro/skills/diagnose-vault-expired.md +60 -0
  10. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/agent.json +4 -2
  11. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/SOUL.md +2 -2
  12. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-serpent.md +1 -1
  13. package/assets/bluebubbles-host +264 -0
  14. package/assets/sanctuary-host-launcher.sh +16 -0
  15. package/changelog.json +5302 -16
  16. package/deploy/unraid/Dockerfile +54 -0
  17. package/deploy/unraid/README.txt +2983 -0
  18. package/deploy/unraid/audit-container-spec.sh +4 -0
  19. package/deploy/unraid/container-runtime.json +1 -0
  20. package/deploy/unraid/docker-man-template-transaction.mjs +711 -0
  21. package/deploy/unraid/docker-man-template-xml.cjs +105 -0
  22. package/deploy/unraid/migrate-sanctuary-bundle.mjs +60 -0
  23. package/deploy/unraid/ouro-events/bootstrap-spool.sh +92 -0
  24. package/deploy/unraid/ouro-events/emit-event.mjs +310 -0
  25. package/deploy/unraid/ouro-events/emit-usenet-event.sh +54 -0
  26. package/deploy/unraid/ouro-events/install-usenet-guard.sh +435 -0
  27. package/deploy/unraid/ouro-events/usenet-health.sh +176 -0
  28. package/deploy/unraid/sanctuary-acceptance-adapter.sh +43 -0
  29. package/deploy/unraid/sanctuary-acceptance-contract.json +198 -0
  30. package/deploy/unraid/sanctuary-acceptance-harness.sh +23 -0
  31. package/deploy/unraid/sanctuary-authority-installation.json +36 -0
  32. package/deploy/unraid/sanctuary-authority-service.sh +30 -0
  33. package/deploy/unraid/sanctuary-deployment-target.mjs +710 -0
  34. package/deploy/unraid/sanctuary-safe-rename.py +24 -0
  35. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +2084 -0
  36. package/deploy/unraid/sanctuary-unit16-run.sh +560 -0
  37. package/deploy/unraid/sanctuary-unit18-target-audit.sh +27 -0
  38. package/deploy/unraid/sanctuary.ouro/agent.json +66 -0
  39. package/deploy/unraid/sanctuary.ouro/arc/README.md +3 -0
  40. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +5 -0
  41. package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +8 -0
  42. package/deploy/unraid/sanctuary.ouro/mcp/media-mcp.mjs +795 -0
  43. package/deploy/unraid/sanctuary.ouro/provider-readiness.json +11 -0
  44. package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +5 -0
  45. package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +5 -0
  46. package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +15 -0
  47. package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +25 -0
  48. package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +15 -0
  49. package/deploy/unraid/sanctuary.ouro/state/policy/steward.json +7 -0
  50. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +135 -0
  51. package/deploy/unraid/sanctuary.xml +28 -0
  52. package/dist/a2a/card.js +57 -0
  53. package/dist/a2a/client.js +180 -0
  54. package/dist/a2a/config.js +50 -0
  55. package/dist/a2a/delegation-stores.js +56 -0
  56. package/dist/a2a/did-resolution.js +93 -0
  57. package/dist/a2a/identity.js +170 -0
  58. package/dist/a2a/inbound-share.js +107 -0
  59. package/dist/a2a/mission-result-wire.js +196 -0
  60. package/dist/a2a/onboarding.js +115 -0
  61. package/dist/a2a/pin-store.js +132 -0
  62. package/dist/a2a/seen-ledger.js +120 -0
  63. package/dist/a2a/server.js +604 -0
  64. package/dist/a2a/task-store.js +69 -0
  65. package/dist/a2a/transport.js +45 -0
  66. package/dist/a2a/types.js +3 -0
  67. package/dist/arc/attention-types.js +8 -0
  68. package/dist/arc/cares.js +360 -0
  69. package/dist/arc/episodes.js +118 -0
  70. package/dist/arc/evolution.js +490 -0
  71. package/dist/arc/flight-recorder.js +1034 -0
  72. package/dist/arc/intentions.js +134 -0
  73. package/dist/arc/json-store.js +117 -0
  74. package/dist/arc/obligations.js +386 -0
  75. package/dist/arc/packets.js +336 -0
  76. package/dist/arc/presence.js +185 -0
  77. package/dist/arc/task-lifecycle.js +57 -0
  78. package/dist/commerce/store.js +755 -0
  79. package/dist/commerce/types.js +3 -0
  80. package/dist/heart/active-work.js +798 -40
  81. package/dist/heart/agent-entry.js +252 -3
  82. package/dist/heart/approval-files.js +113 -0
  83. package/dist/heart/approval-store.js +960 -0
  84. package/dist/heart/attachments/image-normalize.js +194 -0
  85. package/dist/heart/attachments/materialize.js +97 -0
  86. package/dist/heart/attachments/originals.js +88 -0
  87. package/dist/heart/attachments/render.js +29 -0
  88. package/dist/heart/attachments/sources/bluebubbles.js +156 -0
  89. package/dist/heart/attachments/sources/cli-local-file.js +78 -0
  90. package/dist/heart/attachments/sources/index.js +18 -0
  91. package/dist/heart/attachments/sources/telegram.js +71 -0
  92. package/dist/heart/attachments/store.js +132 -0
  93. package/dist/heart/attachments/types.js +93 -0
  94. package/dist/heart/auth/auth-flow.js +495 -0
  95. package/dist/heart/autonomy-budget.js +478 -0
  96. package/dist/heart/awaiting/await-alert.js +148 -0
  97. package/dist/heart/awaiting/await-expiry.js +143 -0
  98. package/dist/heart/awaiting/await-loader.js +91 -0
  99. package/dist/heart/awaiting/await-parser.js +149 -0
  100. package/dist/heart/awaiting/await-runtime-state.js +111 -0
  101. package/dist/heart/awaiting/await-scheduler.js +382 -0
  102. package/dist/heart/background-operations.js +281 -0
  103. package/dist/heart/bridges/manager.js +137 -17
  104. package/dist/heart/bridges/store.js +14 -2
  105. package/dist/heart/bundle-state.js +168 -0
  106. package/dist/heart/commitments.js +44 -18
  107. package/dist/heart/config-registry.js +331 -0
  108. package/dist/heart/config.js +174 -131
  109. package/dist/heart/context-loss-gauntlet.js +387 -0
  110. package/dist/heart/context-loss-sentinel.js +1157 -0
  111. package/dist/heart/core.js +2169 -581
  112. package/dist/heart/cross-chat-delivery.js +9 -22
  113. package/dist/heart/daemon/agent-config-check.js +451 -0
  114. package/dist/heart/daemon/agent-discovery.js +185 -3
  115. package/dist/heart/daemon/agent-service.js +542 -0
  116. package/dist/heart/daemon/agentic-repair.js +547 -0
  117. package/dist/heart/daemon/await-private-wake.js +59 -0
  118. package/dist/heart/daemon/bluebubbles-cli-integration.js +71 -0
  119. package/dist/heart/daemon/bluebubbles-health-diagnostics.js +122 -0
  120. package/dist/heart/daemon/bluebubbles-host-protocol.js +433 -0
  121. package/dist/heart/daemon/bluebubbles-host.js +273 -0
  122. package/dist/heart/daemon/boot-sync-probe.js +197 -0
  123. package/dist/heart/daemon/cadence.js +180 -0
  124. package/dist/heart/daemon/cli-defaults.js +854 -0
  125. package/dist/heart/daemon/cli-desk.js +322 -0
  126. package/dist/heart/daemon/cli-exec.js +8822 -0
  127. package/dist/heart/daemon/cli-help.js +716 -0
  128. package/dist/heart/daemon/cli-parse.js +2771 -0
  129. package/dist/heart/daemon/cli-render-doctor.js +57 -0
  130. package/dist/heart/daemon/cli-render.js +777 -0
  131. package/dist/heart/daemon/cli-types.js +8 -0
  132. package/dist/heart/daemon/connect-bay.js +323 -0
  133. package/dist/heart/daemon/container-credential-bootstrap.js +273 -0
  134. package/dist/heart/daemon/container-healthcheck.js +112 -0
  135. package/dist/heart/daemon/container-runtime.js +97 -0
  136. package/dist/heart/daemon/container-spec-auditor-main.js +171 -0
  137. package/dist/heart/daemon/container-spec-auditor.js +229 -0
  138. package/dist/heart/daemon/daemon-bootstrap-startup.js +115 -0
  139. package/dist/heart/daemon/daemon-cli.js +29 -2349
  140. package/dist/heart/daemon/daemon-entry.js +930 -11
  141. package/dist/heart/daemon/daemon-health.js +186 -0
  142. package/dist/heart/daemon/daemon-rollup.js +57 -0
  143. package/dist/heart/daemon/daemon-runtime-sync.js +88 -13
  144. package/dist/heart/daemon/daemon-tombstone.js +236 -0
  145. package/dist/heart/daemon/daemon.js +2084 -105
  146. package/dist/heart/daemon/dns-workflow.js +394 -0
  147. package/dist/heart/daemon/doctor-types.js +8 -0
  148. package/dist/heart/daemon/doctor.js +1755 -0
  149. package/dist/heart/daemon/freshness.js +345 -0
  150. package/dist/heart/daemon/habit-private-wake.js +61 -0
  151. package/dist/heart/daemon/health-monitor.js +142 -11
  152. package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
  153. package/dist/heart/daemon/hooks/bundle-meta.js +135 -1
  154. package/dist/heart/daemon/http-health-probe.js +80 -0
  155. package/dist/heart/daemon/human-command-screens.js +234 -0
  156. package/dist/heart/daemon/human-readiness.js +114 -0
  157. package/dist/heart/daemon/inner-status.js +78 -0
  158. package/dist/heart/daemon/interactive-repair.js +394 -0
  159. package/dist/heart/daemon/launchd.js +37 -8
  160. package/dist/heart/daemon/log-tailer.js +79 -10
  161. package/dist/heart/daemon/logs-prune.js +132 -0
  162. package/dist/heart/daemon/mcp-canary.js +524 -0
  163. package/dist/heart/daemon/message-router.js +65 -5
  164. package/dist/heart/daemon/migrate-to-desk.js +848 -0
  165. package/dist/heart/daemon/os-cron-deps.js +135 -0
  166. package/dist/heart/daemon/os-cron.js +22 -14
  167. package/dist/heart/daemon/ouro-bot-entry.js +4 -2
  168. package/dist/heart/daemon/ouro-entry.js +3 -1
  169. package/dist/heart/daemon/plugin-cli.js +432 -0
  170. package/dist/heart/daemon/process-manager.js +733 -61
  171. package/dist/heart/daemon/provider-discovery.js +137 -0
  172. package/dist/heart/daemon/provider-ping-progress.js +83 -0
  173. package/dist/heart/daemon/prunable-bundle.js +144 -0
  174. package/dist/heart/daemon/pulse.js +511 -0
  175. package/dist/heart/daemon/readiness-repair.js +365 -0
  176. package/dist/heart/daemon/run-hooks.js +2 -0
  177. package/dist/heart/daemon/runtime-logging.js +47 -14
  178. package/dist/heart/daemon/runtime-metadata.js +2 -30
  179. package/dist/heart/daemon/runtime-mode.js +13 -2
  180. package/dist/heart/daemon/safe-mode.js +161 -0
  181. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +2654 -0
  182. package/dist/heart/daemon/sanctuary-acceptance-harness.js +1830 -0
  183. package/dist/heart/daemon/sanctuary-acceptance-marker.js +267 -0
  184. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +955 -0
  185. package/dist/heart/daemon/sanctuary-authority-codec.js +86 -0
  186. package/dist/heart/daemon/sanctuary-authority-epoch.js +256 -0
  187. package/dist/heart/daemon/sanctuary-authority-installation.js +140 -0
  188. package/dist/heart/daemon/sanctuary-authority-ledger.js +241 -0
  189. package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +819 -0
  190. package/dist/heart/daemon/sanctuary-authority-vault-migration.js +79 -0
  191. package/dist/heart/daemon/sanctuary-bundle-migration.js +729 -0
  192. package/dist/heart/daemon/sanctuary-host-authority.js +1008 -0
  193. package/dist/heart/daemon/sanctuary-host-detached-supervisor.js +494 -0
  194. package/dist/heart/daemon/sanctuary-host-executor.js +670 -0
  195. package/dist/heart/daemon/sanctuary-host-linux-kernel.js +334 -0
  196. package/dist/heart/daemon/sanctuary-host-supervisor-entry.js +207 -0
  197. package/dist/heart/daemon/sanctuary-host-supervisor.js +95 -0
  198. package/dist/heart/daemon/sanctuary-package-management.js +103 -0
  199. package/dist/heart/daemon/sanctuary-scheduler-liveness.js +227 -0
  200. package/dist/heart/daemon/sanctuary-scheduler-origin.js +132 -0
  201. package/dist/heart/daemon/sanctuary-telegram-authority-entry.js +445 -0
  202. package/dist/heart/daemon/sanctuary-telegram-authority-gateway.js +585 -0
  203. package/dist/heart/daemon/sanctuary-telegram-authority-service.js +630 -0
  204. package/dist/heart/daemon/sense-manager.js +699 -48
  205. package/dist/heart/daemon/session-id-resolver.js +131 -0
  206. package/dist/heart/daemon/skill-management-installer.js +1 -1
  207. package/dist/heart/daemon/socket-client.js +180 -12
  208. package/dist/heart/daemon/stale-bundle-prune.js +113 -0
  209. package/dist/heart/daemon/startup-tui.js +339 -0
  210. package/dist/heart/daemon/supercronic-supervisor.js +282 -0
  211. package/dist/heart/daemon/task-scheduler.js +117 -39
  212. package/dist/heart/daemon/terminal-ui.js +499 -0
  213. package/dist/heart/daemon/thoughts.js +138 -54
  214. package/dist/heart/daemon/up-progress.js +366 -0
  215. package/dist/heart/daemon/vault-items.js +56 -0
  216. package/dist/heart/delegation.js +1 -4
  217. package/dist/heart/external-events/router.js +1430 -0
  218. package/dist/heart/frontend-approval-runtime.js +506 -0
  219. package/dist/heart/frontend-journal.js +215 -0
  220. package/dist/heart/frontend-session-service.js +237 -0
  221. package/dist/heart/frontend-socket-client.js +154 -0
  222. package/dist/heart/frontend-socket.js +400 -0
  223. package/dist/heart/habits/habit-cancel.js +810 -0
  224. package/dist/heart/habits/habit-lifecycle.js +1327 -0
  225. package/dist/heart/habits/habit-migration.js +194 -0
  226. package/dist/heart/habits/habit-parser.js +278 -0
  227. package/dist/heart/habits/habit-runtime-state.js +93 -0
  228. package/dist/heart/habits/habit-scheduler.js +653 -0
  229. package/dist/heart/habits/habit-session-summary.js +318 -0
  230. package/dist/heart/habits/habit-session.js +636 -0
  231. package/dist/heart/{daemon → hatch}/hatch-flow.js +55 -62
  232. package/dist/heart/{daemon → hatch}/hatch-specialist.js +6 -8
  233. package/dist/heart/{daemon → hatch}/specialist-prompt.js +12 -9
  234. package/dist/heart/{daemon → hatch}/specialist-tools.js +53 -18
  235. package/dist/heart/identity.js +196 -59
  236. package/dist/heart/kept-notes.js +289 -0
  237. package/dist/heart/kicks.js +1 -1
  238. package/dist/heart/machine-identity.js +161 -0
  239. package/dist/heart/mail-import-discovery.js +356 -0
  240. package/dist/heart/mailbox/mailbox-http-hooks.js +98 -0
  241. package/dist/heart/mailbox/mailbox-http-response.js +7 -0
  242. package/dist/heart/mailbox/mailbox-http-routes.js +398 -0
  243. package/dist/heart/mailbox/mailbox-http-static.js +103 -0
  244. package/dist/heart/mailbox/mailbox-http-transport.js +129 -0
  245. package/dist/heart/mailbox/mailbox-http.js +99 -0
  246. package/dist/heart/mailbox/mailbox-read.js +39 -0
  247. package/dist/heart/mailbox/mailbox-types.js +27 -0
  248. package/dist/heart/mailbox/mailbox-view.js +197 -0
  249. package/dist/heart/mailbox/readers/agent-machine.js +428 -0
  250. package/dist/heart/mailbox/readers/continuity-readers.js +329 -0
  251. package/dist/heart/mailbox/readers/mail.js +375 -0
  252. package/dist/heart/mailbox/readers/runtime-readers.js +823 -0
  253. package/dist/heart/mailbox/readers/sessions.js +231 -0
  254. package/dist/heart/mailbox/readers/shared.js +111 -0
  255. package/dist/heart/mcp/mcp-server.js +696 -0
  256. package/dist/heart/migrate-config.js +100 -0
  257. package/dist/heart/model-capabilities.js +11 -0
  258. package/dist/heart/orientation-frame.js +345 -0
  259. package/dist/heart/platform.js +81 -0
  260. package/dist/heart/primitives.js +1 -1
  261. package/dist/heart/private-runtime/decision-renderer.js +158 -0
  262. package/dist/heart/private-runtime/index.js +18 -0
  263. package/dist/heart/private-runtime/ledger.js +221 -0
  264. package/dist/heart/private-runtime/policy.js +252 -0
  265. package/dist/heart/private-runtime/types.js +2 -0
  266. package/dist/heart/progress-story.js +1 -1
  267. package/dist/heart/provider-attempt.js +137 -0
  268. package/dist/heart/provider-binding-resolver.js +273 -0
  269. package/dist/heart/provider-credentials.js +449 -0
  270. package/dist/heart/provider-failover.js +311 -0
  271. package/dist/heart/provider-models.js +96 -0
  272. package/dist/heart/provider-ping.js +265 -0
  273. package/dist/heart/provider-readiness-cache.js +119 -0
  274. package/dist/heart/provider-visibility.js +188 -0
  275. package/dist/heart/providers/anthropic-token.js +131 -0
  276. package/dist/heart/providers/anthropic.js +173 -60
  277. package/dist/heart/providers/azure.js +23 -11
  278. package/dist/heart/providers/error-classification.js +127 -0
  279. package/dist/heart/providers/github-copilot.js +34 -38
  280. package/dist/heart/providers/minimax-vlm.js +189 -0
  281. package/dist/heart/providers/minimax.js +26 -8
  282. package/dist/heart/providers/openai-codex-token.js +349 -0
  283. package/dist/heart/providers/openai-codex.js +55 -40
  284. package/dist/heart/providers/openai-compatible.js +177 -0
  285. package/dist/heart/run-ledger.js +247 -0
  286. package/dist/heart/runtime-capability-check.js +170 -0
  287. package/dist/heart/runtime-credentials.js +607 -0
  288. package/dist/heart/runtime-cwd.js +87 -0
  289. package/dist/heart/sense-truth.js +22 -4
  290. package/dist/heart/session-activity.js +92 -24
  291. package/dist/heart/session-events.js +1504 -0
  292. package/dist/heart/session-playback-cli-main.js +5 -0
  293. package/dist/heart/session-playback-cli.js +36 -0
  294. package/dist/heart/session-playback.js +231 -0
  295. package/dist/heart/session-redaction-repair-cli-main.js +31 -0
  296. package/dist/heart/session-redaction-repair.js +552 -0
  297. package/dist/heart/session-stats-cli-main.js +5 -0
  298. package/dist/heart/session-stats.js +183 -0
  299. package/dist/heart/session-transcript.js +133 -0
  300. package/dist/heart/start-of-turn-packet.js +372 -0
  301. package/dist/heart/steward-policy.js +618 -0
  302. package/dist/heart/streaming.js +604 -207
  303. package/dist/heart/structured-output.js +196 -0
  304. package/dist/heart/sync-classification.js +176 -0
  305. package/dist/heart/sync.js +449 -0
  306. package/dist/heart/target-resolution.js +11 -7
  307. package/dist/heart/tempo.js +93 -0
  308. package/dist/heart/temporal-view.js +41 -0
  309. package/dist/heart/timeouts.js +101 -0
  310. package/dist/heart/tool-activity-callbacks.js +59 -0
  311. package/dist/heart/tool-approval.js +382 -0
  312. package/dist/heart/tool-description.js +155 -0
  313. package/dist/heart/tool-friction.js +55 -0
  314. package/dist/heart/tool-loop.js +200 -0
  315. package/dist/heart/turn-context.js +482 -0
  316. package/dist/heart/turn-execution-lease.js +30 -0
  317. package/dist/heart/{daemon → versioning}/ouro-bot-global-installer.js +6 -5
  318. package/dist/heart/{daemon → versioning}/ouro-bot-wrapper.js +1 -1
  319. package/dist/heart/versioning/ouro-path-installer.js +432 -0
  320. package/dist/heart/versioning/ouro-recovery-launcher.js +76 -0
  321. package/dist/heart/versioning/ouro-version-manager.js +409 -0
  322. package/dist/heart/{daemon → versioning}/staged-restart.js +40 -8
  323. package/dist/heart/{daemon → versioning}/update-checker.js +6 -1
  324. package/dist/heart/versioning/update-hooks.js +154 -0
  325. package/dist/heart/versioning/version-intent.js +114 -0
  326. package/dist/heart/work-card.js +380 -0
  327. package/dist/mailbox-ui/assets/index-Cyv4mw3Y.js +1 -0
  328. package/dist/mailbox-ui/assets/index-Du_9G9WO.css +1 -0
  329. package/dist/mailbox-ui/assets/vendor-CcN1XpQ9.js +61 -0
  330. package/dist/mailbox-ui/index.html +16 -0
  331. package/dist/mailroom/attention.js +167 -0
  332. package/dist/mailroom/autonomy.js +209 -0
  333. package/dist/mailroom/blob-store.js +889 -0
  334. package/dist/mailroom/body-cache.js +61 -0
  335. package/dist/mailroom/cache-sync-cli.js +58 -0
  336. package/dist/mailroom/core.js +803 -0
  337. package/dist/mailroom/entry.js +160 -0
  338. package/dist/mailroom/file-store.js +568 -0
  339. package/dist/mailroom/hosted-cache-sync.js +384 -0
  340. package/dist/mailroom/mbox-import.js +393 -0
  341. package/dist/mailroom/migration.js +164 -0
  342. package/dist/mailroom/outbound.js +380 -0
  343. package/dist/mailroom/policy.js +263 -0
  344. package/dist/mailroom/reader.js +292 -0
  345. package/dist/mailroom/search-cache.js +513 -0
  346. package/dist/mailroom/search-relevance.js +319 -0
  347. package/dist/mailroom/smtp-ingress.js +176 -0
  348. package/dist/mailroom/source-state.js +176 -0
  349. package/dist/mailroom/thread.js +109 -0
  350. package/dist/mailroom/travel-extract.js +89 -0
  351. package/dist/mind/bundle-manifest.js +24 -2
  352. package/dist/mind/context.js +309 -111
  353. package/dist/mind/desk-section.js +362 -0
  354. package/dist/mind/diary-integrity.js +60 -0
  355. package/dist/mind/{memory.js → diary.js} +68 -77
  356. package/dist/mind/embedding-provider.js +60 -0
  357. package/dist/mind/file-state.js +179 -0
  358. package/dist/mind/{associative-recall.js → note-search.js} +47 -58
  359. package/dist/mind/obligation-steering.js +199 -3
  360. package/dist/mind/pending.js +36 -10
  361. package/dist/mind/prompt-budget.js +479 -0
  362. package/dist/mind/prompt-refresh.js +3 -2
  363. package/dist/mind/prompt.js +1050 -189
  364. package/dist/mind/provenance-trust.js +26 -0
  365. package/dist/mind/record-paths.js +312 -0
  366. package/dist/mind/scrutiny.js +173 -0
  367. package/dist/mind/session-transaction.js +564 -0
  368. package/dist/nerves/cli-logging.js +7 -1
  369. package/dist/nerves/coverage/audit-rules.js +15 -6
  370. package/dist/nerves/coverage/audit.js +28 -2
  371. package/dist/nerves/coverage/cli.js +1 -1
  372. package/dist/nerves/coverage/contract.js +5 -5
  373. package/dist/nerves/coverage/file-completeness.js +139 -5
  374. package/dist/nerves/coverage/run-artifacts.js +55 -35
  375. package/dist/nerves/coverage/source-scanner.js +4 -4
  376. package/dist/nerves/event-buffer.js +111 -0
  377. package/dist/nerves/index.js +293 -9
  378. package/dist/nerves/observation.js +20 -0
  379. package/dist/nerves/redact.js +79 -0
  380. package/dist/nerves/review/cli-main.js +5 -0
  381. package/dist/nerves/review/cli.js +156 -0
  382. package/dist/nerves/review/core.js +159 -0
  383. package/dist/nerves/runtime.js +17 -1
  384. package/dist/repertoire/ado-client.js +15 -56
  385. package/dist/repertoire/ado-semantic.js +16 -10
  386. package/dist/repertoire/api-client.js +97 -0
  387. package/dist/repertoire/bitwarden-store.js +1045 -0
  388. package/dist/repertoire/bundle-templates.js +71 -0
  389. package/dist/repertoire/bw-installer.js +180 -0
  390. package/dist/repertoire/coding/codex-jsonl.js +64 -0
  391. package/dist/repertoire/coding/context-pack.js +331 -0
  392. package/dist/repertoire/coding/feedback.js +174 -32
  393. package/dist/repertoire/coding/index.js +15 -3
  394. package/dist/repertoire/coding/manager.js +159 -10
  395. package/dist/repertoire/coding/spawner.js +58 -11
  396. package/dist/repertoire/coding/tools.js +221 -15
  397. package/dist/repertoire/coding/workbench-client.js +272 -0
  398. package/dist/repertoire/coding/workbench-manager.js +346 -0
  399. package/dist/repertoire/commerce-errors.js +109 -0
  400. package/dist/repertoire/commerce-self-test.js +156 -0
  401. package/dist/repertoire/credential-access.js +178 -0
  402. package/dist/repertoire/desk/classifier.js +362 -0
  403. package/dist/repertoire/duffel-client.js +185 -0
  404. package/dist/repertoire/github-client.js +14 -55
  405. package/dist/repertoire/graph-client.js +11 -52
  406. package/dist/repertoire/guardrails.js +811 -18
  407. package/dist/repertoire/mcp-client.js +44 -3
  408. package/dist/repertoire/mcp-manager.js +427 -139
  409. package/dist/repertoire/mcp-tools.js +124 -0
  410. package/dist/repertoire/plugin-mcp.js +175 -0
  411. package/dist/repertoire/plugins.js +253 -0
  412. package/dist/repertoire/relationship-authorization.js +322 -0
  413. package/dist/repertoire/shell-sessions.js +134 -0
  414. package/dist/repertoire/skills.js +48 -4
  415. package/dist/repertoire/stripe-client.js +131 -0
  416. package/dist/repertoire/tool-arguments.js +170 -0
  417. package/dist/repertoire/tool-results.js +29 -0
  418. package/dist/repertoire/tools-a2a.js +699 -0
  419. package/dist/repertoire/tools-attachments.js +322 -0
  420. package/dist/repertoire/tools-awaiting.js +575 -0
  421. package/dist/repertoire/tools-base.js +77 -1138
  422. package/dist/repertoire/tools-bluebubbles.js +2 -0
  423. package/dist/repertoire/tools-bridge.js +144 -0
  424. package/dist/repertoire/tools-bundle.js +993 -0
  425. package/dist/repertoire/tools-commerce.js +253 -0
  426. package/dist/repertoire/tools-config.js +186 -0
  427. package/dist/repertoire/tools-continuity.js +575 -0
  428. package/dist/repertoire/tools-credential.js +383 -0
  429. package/dist/repertoire/tools-evolution.js +527 -0
  430. package/dist/repertoire/tools-files.js +344 -0
  431. package/dist/repertoire/tools-flight.js +290 -0
  432. package/dist/repertoire/tools-flow.js +119 -0
  433. package/dist/repertoire/tools-github.js +3 -8
  434. package/dist/repertoire/tools-habits.js +103 -0
  435. package/dist/repertoire/tools-mail.js +1828 -0
  436. package/dist/repertoire/tools-notes.js +485 -0
  437. package/dist/repertoire/tools-obligations.js +182 -0
  438. package/dist/repertoire/tools-orientation.js +31 -0
  439. package/dist/repertoire/tools-record.js +482 -0
  440. package/dist/repertoire/tools-rsvp.js +139 -0
  441. package/dist/repertoire/tools-runtime.js +150 -0
  442. package/dist/repertoire/tools-sanctuary-host.js +202 -0
  443. package/dist/repertoire/tools-session.js +1133 -0
  444. package/dist/repertoire/tools-shell.js +149 -0
  445. package/dist/repertoire/tools-steward-policy.js +119 -0
  446. package/dist/repertoire/tools-stripe.js +224 -0
  447. package/dist/repertoire/tools-surface.js +369 -0
  448. package/dist/repertoire/tools-teams.js +12 -39
  449. package/dist/repertoire/tools-telegram-contacts.js +37 -0
  450. package/dist/repertoire/tools-travel.js +125 -0
  451. package/dist/repertoire/tools-trip.js +982 -0
  452. package/dist/repertoire/tools-unraid.js +531 -0
  453. package/dist/repertoire/tools-user-profile.js +146 -0
  454. package/dist/repertoire/tools-vault.js +40 -0
  455. package/dist/repertoire/tools-voice.js +145 -0
  456. package/dist/repertoire/tools.js +494 -148
  457. package/dist/repertoire/travel-api-client.js +360 -0
  458. package/dist/repertoire/unraid-client.js +155 -0
  459. package/dist/repertoire/unraid-restart.js +339 -0
  460. package/dist/repertoire/user-profile.js +131 -0
  461. package/dist/repertoire/vault-setup.js +246 -0
  462. package/dist/repertoire/vault-unlock.js +628 -0
  463. package/dist/rsvp/aisleplanner-client.js +354 -0
  464. package/dist/rsvp/cli.js +926 -0
  465. package/dist/rsvp/config.js +327 -0
  466. package/dist/rsvp/cutover.js +433 -0
  467. package/dist/rsvp/diagnostics.js +232 -0
  468. package/dist/rsvp/diff-renderer.js +176 -0
  469. package/dist/rsvp/habit-policy.js +110 -0
  470. package/dist/rsvp/habit-stage.js +123 -0
  471. package/dist/rsvp/incident-bundle.js +97 -0
  472. package/dist/rsvp/migration.js +204 -0
  473. package/dist/rsvp/native-habit-runner.js +630 -0
  474. package/dist/rsvp/outbound-state.js +628 -0
  475. package/dist/rsvp/query.js +118 -0
  476. package/dist/rsvp/replay.js +233 -0
  477. package/dist/rsvp/snapshot.js +232 -0
  478. package/dist/rsvp/spend-ledger.js +175 -0
  479. package/dist/scripts/claude-code-hook.js +41 -0
  480. package/dist/scripts/claude-code-stop-hook.js +47 -0
  481. package/dist/senses/a2a-entry.js +115 -0
  482. package/dist/senses/acp-server.js +386 -0
  483. package/dist/senses/attention-queue.js +186 -0
  484. package/dist/senses/await-turn-message.js +58 -0
  485. package/dist/senses/bluebubbles/active-turns.js +216 -0
  486. package/dist/senses/bluebubbles/attachment-cache.js +53 -0
  487. package/dist/senses/bluebubbles/attachment-download.js +137 -0
  488. package/dist/senses/{bluebubbles-client.js → bluebubbles/client.js} +355 -40
  489. package/dist/senses/bluebubbles/context-packet.js +207 -0
  490. package/dist/senses/bluebubbles/context-smoke.js +144 -0
  491. package/dist/senses/bluebubbles/entry.js +90 -0
  492. package/dist/senses/{bluebubbles-inbound-log.js → bluebubbles/inbound-log.js} +21 -3
  493. package/dist/senses/bluebubbles/index.js +3841 -0
  494. package/dist/senses/bluebubbles/latest-turn.js +311 -0
  495. package/dist/senses/{bluebubbles-media.js → bluebubbles/media.js} +121 -71
  496. package/dist/senses/{bluebubbles-model.js → bluebubbles/model.js} +159 -28
  497. package/dist/senses/{bluebubbles-mutation-log.js → bluebubbles/mutation-log.js} +24 -13
  498. package/dist/senses/bluebubbles/outbound-state.js +308 -0
  499. package/dist/senses/bluebubbles/processed-log.js +133 -0
  500. package/dist/senses/bluebubbles/reaction-policy.js +47 -0
  501. package/dist/senses/bluebubbles/replay.js +284 -0
  502. package/dist/senses/{bluebubbles-runtime-state.js → bluebubbles/runtime-state.js} +30 -2
  503. package/dist/senses/bluebubbles/semantic-receipts.js +1986 -0
  504. package/dist/senses/{bluebubbles-session-cleanup.js → bluebubbles/session-cleanup.js} +1 -1
  505. package/dist/senses/bluebubbles/webhook-registration.js +228 -0
  506. package/dist/senses/bluebubbles-meta-guard.js +39 -0
  507. package/dist/senses/cli/bracketed-paste.js +82 -0
  508. package/dist/senses/cli/image-paste.js +287 -0
  509. package/dist/senses/cli/image-ref-navigation.js +75 -0
  510. package/dist/senses/cli/ink-app.js +156 -0
  511. package/dist/senses/cli/inline-diff.js +64 -0
  512. package/dist/senses/cli/input-keys.js +174 -0
  513. package/dist/senses/cli/kill-ring.js +86 -0
  514. package/dist/senses/cli/message-list.js +51 -0
  515. package/dist/senses/cli/ouro-tui.js +607 -0
  516. package/dist/senses/cli/spinner-imperative.js +135 -0
  517. package/dist/senses/cli/spinner.js +101 -0
  518. package/dist/senses/cli/status-line.js +60 -0
  519. package/dist/senses/cli/streaming-markdown.js +526 -0
  520. package/dist/senses/cli/tool-display.js +85 -0
  521. package/dist/senses/cli/tool-render.js +85 -0
  522. package/dist/senses/cli/tui-store.js +241 -0
  523. package/dist/senses/cli/virtual-list.js +35 -0
  524. package/dist/senses/cli-entry.js +60 -8
  525. package/dist/senses/cli.js +604 -320
  526. package/dist/senses/commands.js +66 -3
  527. package/dist/senses/context-packet-ledger.js +371 -0
  528. package/dist/senses/context-packets.js +154 -0
  529. package/dist/senses/habit-lifecycle-guard.js +24 -0
  530. package/dist/senses/habit-turn-message.js +160 -0
  531. package/dist/senses/ingress-evidence.js +2 -0
  532. package/dist/senses/mail-entry.js +66 -0
  533. package/dist/senses/mail.js +524 -0
  534. package/dist/senses/pipeline.js +1115 -204
  535. package/dist/senses/private-runtime-worker.js +859 -0
  536. package/dist/senses/private-runtime.js +1517 -0
  537. package/dist/senses/proactive-content-guard.js +51 -0
  538. package/dist/senses/root-host-approval-port.js +345 -0
  539. package/dist/senses/root-host-approval-runtime.js +393 -0
  540. package/dist/senses/sanctuary-authority-resident.js +102 -0
  541. package/dist/senses/sanctuary-download-credit-presentation.js +31 -0
  542. package/dist/senses/sanctuary-full-visibility-contract.js +302 -0
  543. package/dist/senses/sanctuary-grounding.js +118 -0
  544. package/dist/senses/sanctuary-health-acceptance-probe-entry.js +12 -0
  545. package/dist/senses/sanctuary-health-acceptance-probe.js +774 -0
  546. package/dist/senses/sanctuary-health-runner.js +100 -0
  547. package/dist/senses/sanctuary-health.js +408 -0
  548. package/dist/senses/sanctuary-install-state-contract.js +126 -0
  549. package/dist/senses/sanctuary-interactive-control.js +410 -0
  550. package/dist/senses/sanctuary-media-catalog-contract.js +297 -0
  551. package/dist/senses/sanctuary-media-optimization.js +514 -0
  552. package/dist/senses/sanctuary-runtime.js +340 -0
  553. package/dist/senses/sanctuary-sab.js +85 -0
  554. package/dist/senses/sanctuary-storage-optimization-contract.js +32 -0
  555. package/dist/senses/shared-turn.js +772 -0
  556. package/dist/senses/surface-tool.js +109 -0
  557. package/dist/senses/teams-entry.js +60 -8
  558. package/dist/senses/teams.js +519 -218
  559. package/dist/senses/telegram-admission.js +718 -0
  560. package/dist/senses/telegram-approval-runtime.js +793 -0
  561. package/dist/senses/telegram-attachments.js +186 -0
  562. package/dist/senses/telegram-audit-ledger.js +220 -0
  563. package/dist/senses/telegram-authority-transport.js +231 -0
  564. package/dist/senses/telegram-client.js +1717 -0
  565. package/dist/senses/telegram-effect-adapter.js +896 -0
  566. package/dist/senses/telegram-entry.js +83 -0
  567. package/dist/senses/telegram.js +2324 -0
  568. package/dist/senses/trust-gate.js +104 -9
  569. package/dist/senses/voice/audio-playback.js +237 -0
  570. package/dist/senses/voice/audio-routing.js +119 -0
  571. package/dist/senses/voice/elevenlabs.js +202 -0
  572. package/dist/senses/voice/floor-control.js +431 -0
  573. package/dist/senses/voice/floor-controller.js +115 -0
  574. package/dist/senses/voice/golden-path.js +116 -0
  575. package/dist/senses/voice/index.js +29 -0
  576. package/dist/senses/voice/meeting.js +113 -0
  577. package/dist/senses/voice/outbound.js +190 -0
  578. package/dist/senses/voice/phone.js +33 -0
  579. package/dist/senses/voice/playback.js +139 -0
  580. package/dist/senses/voice/realtime-eval.js +496 -0
  581. package/dist/senses/voice/realtime-trace.js +531 -0
  582. package/dist/senses/voice/transcript.js +70 -0
  583. package/dist/senses/voice/turn.js +191 -0
  584. package/dist/senses/voice/twilio-phone-runtime.js +807 -0
  585. package/dist/senses/voice/twilio-phone.js +5031 -0
  586. package/dist/senses/voice/types.js +2 -0
  587. package/dist/senses/voice/whisper.js +161 -0
  588. package/dist/senses/voice-entry.js +81 -0
  589. package/dist/senses/voice-realtime-eval-command.js +99 -0
  590. package/dist/senses/voice-realtime-eval-entry.js +21 -0
  591. package/dist/senses/voice-twilio-entry.js +87 -0
  592. package/dist/trips/core.js +138 -0
  593. package/dist/trips/store.js +265 -0
  594. package/dist/util/frontmatter.js +69 -0
  595. package/npm-shrinkwrap.json +8138 -0
  596. package/package.json +72 -13
  597. package/skills/agent-commerce.md +113 -0
  598. package/skills/browser-navigation.md +117 -0
  599. package/skills/commerce-setup-guide.md +116 -0
  600. package/skills/commerce-setup.md +84 -0
  601. package/skills/configure-dev-tools.md +99 -0
  602. package/skills/travel-planning.md +138 -0
  603. package/dist/heart/daemon/auth-flow.js +0 -430
  604. package/dist/heart/daemon/ouro-path-installer.js +0 -260
  605. package/dist/heart/daemon/ouro-version-manager.js +0 -171
  606. package/dist/heart/daemon/update-hooks.js +0 -138
  607. package/dist/heart/obligations.js +0 -191
  608. package/dist/heart/safe-workspace.js +0 -228
  609. package/dist/heart/session-recall.js +0 -116
  610. package/dist/mind/friends/channel.js +0 -84
  611. package/dist/mind/friends/group-context.js +0 -144
  612. package/dist/mind/friends/resolver.js +0 -84
  613. package/dist/mind/friends/store-file.js +0 -190
  614. package/dist/mind/friends/store.js +0 -4
  615. package/dist/mind/friends/tokens.js +0 -26
  616. package/dist/mind/friends/trust-explanation.js +0 -74
  617. package/dist/mind/friends/types.js +0 -29
  618. package/dist/repertoire/tasks/board.js +0 -134
  619. package/dist/repertoire/tasks/index.js +0 -224
  620. package/dist/repertoire/tasks/lifecycle.js +0 -80
  621. package/dist/repertoire/tasks/middleware.js +0 -65
  622. package/dist/repertoire/tasks/parser.js +0 -173
  623. package/dist/repertoire/tasks/scanner.js +0 -132
  624. package/dist/repertoire/tasks/transitions.js +0 -144
  625. package/dist/senses/bluebubbles-entry.js +0 -13
  626. package/dist/senses/bluebubbles.js +0 -1181
  627. package/dist/senses/debug-activity.js +0 -154
  628. package/dist/senses/inner-dialog-worker.js +0 -90
  629. package/dist/senses/inner-dialog.js +0 -525
  630. package/subagents/README.md +0 -7
  631. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/basilisk.md +0 -0
  632. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jafar.md +0 -0
  633. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jormungandr.md +0 -0
  634. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/kaa.md +0 -0
  635. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/medusa.md +0 -0
  636. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +0 -0
  637. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/nagini.md +0 -0
  638. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/ouroboros.md +0 -0
  639. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/python.md +0 -0
  640. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/quetzalcoatl.md +0 -0
  641. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/sir-hiss.md +0 -0
  642. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
  643. /package/dist/{repertoire/tasks/types.js → heart/attachments/sources/adapter.js} +0 -0
  644. /package/dist/heart/{daemon → hatch}/hatch-animation.js +0 -0
  645. /package/dist/heart/{daemon → hatch}/specialist-orchestrator.js +0 -0
  646. /package/dist/heart/{daemon → versioning}/ouro-uti.js +0 -0
  647. /package/dist/heart/{daemon → versioning}/wrapper-publish-guard.js +0 -0
@@ -0,0 +1,711 @@
1
+ #!/usr/local/bin/node
2
+
3
+ import { execFileSync } from "node:child_process"
4
+ import { createHash } from "node:crypto"
5
+ import { closeSync, constants, fchmodSync, fchownSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, unlinkSync, writeFileSync } from "node:fs"
6
+ import { basename, dirname, resolve } from "node:path"
7
+ import { fileURLToPath } from "node:url"
8
+
9
+ import xmlValidator from "./docker-man-template-xml.cjs"
10
+
11
+ const TARGET_PATH = "/boot/config/plugins/dockerMan/templates-user/my-ouro-butler.xml"
12
+ const JOURNAL_PATH = "/boot/config/custom/ouro-butler/docker-man-template-transaction.json"
13
+ const TEMPLATE_URL = "https://raw.githubusercontent.com/ourostack/ouroboros/main/deploy/unraid/sanctuary.xml"
14
+ const ICON = "https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png"
15
+ const COMMUNITY_APPS_ENTRY_PATH = "/usr/local/emhttp/plugins/community.applications/include/exec.php"
16
+ const COMMUNITY_APPS_HELPER_PATH = "/usr/local/emhttp/plugins/community.applications/include/previous_apps_helpers.php"
17
+ const IMAGE_ID = /^sha256:[0-9a-f]{64}$/u
18
+ const CONTAINER_ID = /^[0-9a-f]{64}$/u
19
+ const VERSION_TAG = /^ghcr\.io\/ourostack\/ouroboros-butler:[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/u
20
+ const JELLYFIN_STATES = new Set(["created", "running", "paused", "restarting", "removing", "exited", "dead"])
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"]
24
+
25
+ function object(value, label) {
26
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`)
27
+ return value
28
+ }
29
+
30
+ function exactKeys(value, expected, label) {
31
+ const keys = Object.keys(value).sort()
32
+ if (JSON.stringify(keys) !== JSON.stringify([...expected].sort())) throw new Error(`${label} has unexpected fields`)
33
+ }
34
+
35
+ function validateJellyfinState(raw, label = "Jellyfin checkpoint") {
36
+ const value = object(raw, label)
37
+ exactKeys(value, ["containerId", "imageId", "state", "restartCount"], label)
38
+ if (!CONTAINER_ID.test(value.containerId) || !IMAGE_ID.test(value.imageId) || !JELLYFIN_STATES.has(value.state) || !Number.isSafeInteger(value.restartCount) || value.restartCount < 0) throw new Error(`${label} is invalid`)
39
+ return { containerId: value.containerId, imageId: value.imageId, state: value.state, restartCount: value.restartCount }
40
+ }
41
+
42
+ export function inspectCurrentJellyfinState() {
43
+ let inspected
44
+ try {
45
+ inspected = JSON.parse(execFileSync("/usr/bin/docker", ["container", "inspect", "--format", JELLYFIN_FORMAT, "jellyfin"], { encoding: "utf8", maxBuffer: 65_536, stdio: ["ignore", "pipe", "ignore"] }).trim())
46
+ } catch {
47
+ throw new Error("Jellyfin container inspection failed")
48
+ }
49
+ const value = object(inspected, "Jellyfin container inspection")
50
+ exactKeys(value, ["name", "containerId", "imageId", "state", "restartCount"], "Jellyfin container inspection")
51
+ if (value.name !== "/jellyfin") throw new Error("Jellyfin container identity is invalid")
52
+ return validateJellyfinState({ containerId: value.containerId, imageId: value.imageId, state: value.state, restartCount: value.restartCount })
53
+ }
54
+
55
+ function assertJellyfinUnchanged(expected) {
56
+ const current = inspectCurrentJellyfinState()
57
+ if (JSON.stringify(current) !== JSON.stringify(expected)) throw new Error("Jellyfin changed during the Butler transaction")
58
+ }
59
+
60
+ function digest(bytes) {
61
+ return `sha256:${createHash("sha256").update(bytes).digest("hex")}`
62
+ }
63
+
64
+ function recordDigest(record) {
65
+ const { digest: _ignored, ...unsigned } = record
66
+ return digest(Buffer.from(JSON.stringify(unsigned)))
67
+ }
68
+
69
+ function directChildren(document, name) {
70
+ return document.children.filter((child) => child.name === name)
71
+ }
72
+
73
+ function singleTextChild(document, name) {
74
+ const children = directChildren(document, name)
75
+ if (children.length !== 1 || children[0].form !== "text" || Object.keys(children[0].attributes).length !== 0) throw new Error(`template ${name} must appear exactly once as plain text`)
76
+ return children[0].text
77
+ }
78
+
79
+ function templateIdentity(bytes, expectedRepository) {
80
+ const document = xmlValidator.parseDockerManTemplateXml(bytes)
81
+ if (!document) throw new Error("canonical DockerMan XML structure is invalid")
82
+ const identity = {
83
+ name: singleTextChild(document, "Name"),
84
+ repository: singleTextChild(document, "Repository"),
85
+ templateUrl: singleTextChild(document, "TemplateURL"),
86
+ icon: singleTextChild(document, "Icon"),
87
+ }
88
+ if (identity.name !== "ouro-butler" || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]+$/u.test(identity.name)) throw new Error("template technical name is invalid")
89
+ if (identity.repository !== expectedRepository || !VERSION_TAG.test(identity.repository)) throw new Error("template version reference is invalid")
90
+ if (identity.templateUrl !== TEMPLATE_URL || identity.icon !== ICON) throw new Error("template identity is invalid")
91
+ const webUi = directChildren(document, "WebUI")
92
+ if (webUi.length !== 1 || webUi[0].form !== "empty" || Object.keys(webUi[0].attributes).length !== 0) throw new Error("template WebUI must be exactly empty")
93
+ return identity
94
+ }
95
+
96
+ function validateDirectory(path, expectedUid, expectedGid) {
97
+ const metadata = lstatSync(path)
98
+ if (!metadata.isDirectory() || metadata.isSymbolicLink() || metadata.uid !== expectedUid || metadata.gid !== expectedGid || (metadata.mode & 0o777) !== 0o700 || realpathSync(path) !== resolve(path)) throw new Error("transaction parent is unsafe")
99
+ }
100
+
101
+ function validateRegularFile(path, expectedUid, expectedGid, label) {
102
+ const metadata = lstatSync(path)
103
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== expectedUid || metadata.gid !== expectedGid || (metadata.mode & 0o777) !== 0o600) throw new Error(`${label} metadata is invalid`)
104
+ return metadata
105
+ }
106
+
107
+ function metadataIfPresent(path) {
108
+ try {
109
+ return lstatSync(path)
110
+ } catch (error) {
111
+ if (error?.code === "ENOENT") return null
112
+ throw error
113
+ }
114
+ }
115
+
116
+ export function isSafeRecoveryTemporaryMetadata(metadata, expectedUid, expectedGid) {
117
+ return Boolean(metadata
118
+ && typeof metadata.isFile === "function"
119
+ && typeof metadata.isSymbolicLink === "function"
120
+ && metadata.isFile()
121
+ && !metadata.isSymbolicLink()
122
+ && metadata.uid === expectedUid
123
+ && metadata.gid === expectedGid
124
+ && (metadata.mode & 0o777) === 0o600)
125
+ }
126
+
127
+ function paths(options = {}) {
128
+ const targetPath = options.targetPath ?? TARGET_PATH
129
+ const journalPath = options.journalPath ?? JOURNAL_PATH
130
+ const expectedUid = options.expectedUid ?? 0
131
+ const expectedGid = options.expectedGid ?? 0
132
+ return {
133
+ targetPath,
134
+ journalPath,
135
+ templateParent: dirname(targetPath),
136
+ journalParent: dirname(journalPath),
137
+ targetTemporary: `${dirname(targetPath)}/.${basename(targetPath)}.ouro-transaction.tmp`,
138
+ journalTemporary: `${dirname(journalPath)}/.${basename(journalPath)}.ouro-transaction.tmp`,
139
+ expectedUid,
140
+ expectedGid,
141
+ }
142
+ }
143
+
144
+ function validateTemplateParent(state, allowTemporaryResidue = false) {
145
+ validateDirectory(state.templateParent, state.expectedUid, state.expectedGid)
146
+ const matches = readdirSync(state.templateParent).filter((entry) => entry.toLowerCase() === basename(state.targetPath).toLowerCase())
147
+ if (matches.length > 1 || (matches.length === 1 && matches[0] !== basename(state.targetPath))) throw new Error("case-folded DockerMan template name is ambiguous")
148
+ if (!allowTemporaryResidue && metadataIfPresent(state.targetTemporary)) throw new Error("template transaction temporary state is ambiguous")
149
+ }
150
+
151
+ function validateJournalParent(state, allowTemporaryResidue = false) {
152
+ validateDirectory(state.journalParent, state.expectedUid, state.expectedGid)
153
+ if (!allowTemporaryResidue && metadataIfPresent(state.journalTemporary)) throw new Error("template transaction temporary state is ambiguous")
154
+ }
155
+
156
+ function validateParents(state, allowTemporaryResidue = false) {
157
+ validateTemplateParent(state, allowTemporaryResidue)
158
+ validateJournalParent(state, allowTemporaryResidue)
159
+ }
160
+
161
+ function ensureJournalParent(state) {
162
+ if (!metadataIfPresent(state.journalParent)) {
163
+ const parent = dirname(state.journalParent)
164
+ validateDirectory(parent, state.expectedUid, state.expectedGid)
165
+ mkdirSync(state.journalParent, { mode: 0o700 })
166
+ syncDirectory(parent)
167
+ }
168
+ validateJournalParent(state)
169
+ }
170
+
171
+ function syncDirectory(path) {
172
+ const fd = openSync(path, constants.O_RDONLY)
173
+ try { fsyncSync(fd) } finally { closeSync(fd) }
174
+ }
175
+
176
+ function atomicWrite(path, temporaryPath, bytes, metadata) {
177
+ const fd = openSync(temporaryPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, metadata.mode)
178
+ try {
179
+ writeFileSync(fd, bytes)
180
+ fchmodSync(fd, metadata.mode)
181
+ fchownSync(fd, metadata.uid, metadata.gid)
182
+ fsyncSync(fd)
183
+ } finally {
184
+ closeSync(fd)
185
+ }
186
+ renameSync(temporaryPath, path)
187
+ syncDirectory(dirname(path))
188
+ }
189
+
190
+ function deleteDurably(path) {
191
+ unlinkSync(path)
192
+ syncDirectory(dirname(path))
193
+ }
194
+
195
+ function decodeCanonicalBase64(value) {
196
+ if (typeof value !== "string" || Buffer.from(value, "base64").toString("base64") !== value) throw new Error("template transaction journal prior bytes are invalid")
197
+ return Buffer.from(value, "base64")
198
+ }
199
+
200
+ function validateJournalRecord(raw, state) {
201
+ const record = object(raw, "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)
204
+ if (record.schemaVersion !== 1 || !["rollback", "committing"].includes(record.state)) throw new Error("template transaction journal state is invalid")
205
+ const target = object(record.target, "template transaction journal target")
206
+ exactKeys(target, ["path", "name", "templateUrl", "icon"], "template transaction journal target")
207
+ if (target.path !== state.targetPath || target.name !== "ouro-butler" || target.templateUrl !== TEMPLATE_URL || target.icon !== ICON) throw new Error("template transaction journal target is invalid")
208
+ const prior = object(record.priorTemplate, "template transaction journal prior template")
209
+ exactKeys(prior, ["present", "bytesBase64", "metadata"], "template transaction journal prior template")
210
+ if (typeof prior.present !== "boolean") throw new Error("template transaction journal prior presence is invalid")
211
+ let priorBytes = null
212
+ if (prior.present) {
213
+ priorBytes = decodeCanonicalBase64(prior.bytesBase64)
214
+ const metadata = object(prior.metadata, "template transaction journal prior metadata")
215
+ exactKeys(metadata, ["uid", "gid", "mode"], "template transaction journal prior metadata")
216
+ if (metadata.uid !== state.expectedUid || metadata.gid !== state.expectedGid || metadata.mode !== 0o600 || record.priorTemplateDigest !== digest(priorBytes)) throw new Error("template transaction journal prior metadata is invalid")
217
+ } else if (prior.bytesBase64 !== null || prior.metadata !== null || record.priorTemplateDigest !== null) {
218
+ throw new Error("template transaction journal absent prior state is invalid")
219
+ }
220
+ if (!VERSION_TAG.test(record.canonicalVersionTag) || !IMAGE_ID.test(record.reviewedManifestDigest) || !IMAGE_ID.test(record.rollbackImageId) || !IMAGE_ID.test(record.targetImageId) || record.rollbackImageId === record.targetImageId || !IMAGE_ID.test(record.targetTemplateDigest)) throw new Error("template transaction journal release identity is invalid")
221
+ const jellyfin = validateJellyfinState(record.jellyfin, "template transaction journal Jellyfin checkpoint")
222
+ if (!IMAGE_ID.test(record.digest) || record.digest !== recordDigest(record)) throw new Error("template transaction journal digest is invalid")
223
+ return { ...record, jellyfin, priorBytes }
224
+ }
225
+
226
+ function readJournal(state) {
227
+ if (!metadataIfPresent(state.journalPath)) return null
228
+ validateRegularFile(state.journalPath, state.expectedUid, state.expectedGid, "template transaction journal")
229
+ let parsed
230
+ try { parsed = JSON.parse(readFileSync(state.journalPath, "utf8")) } catch { throw new Error("template transaction journal JSON is invalid") }
231
+ return validateJournalRecord(parsed, state)
232
+ }
233
+
234
+ function writeJournal(record, state) {
235
+ const complete = { ...record, digest: recordDigest(record) }
236
+ atomicWrite(state.journalPath, state.journalTemporary, Buffer.from(`${JSON.stringify(complete)}\n`), { uid: state.expectedUid, gid: state.expectedGid, mode: 0o600 })
237
+ return complete
238
+ }
239
+
240
+ function currentTargetDigest(state) {
241
+ if (!metadataIfPresent(state.targetPath)) return null
242
+ validateRegularFile(state.targetPath, state.expectedUid, state.expectedGid, "DockerMan template")
243
+ return digest(readFileSync(state.targetPath))
244
+ }
245
+
246
+ function inspectRecoveryTemporary(state) {
247
+ validateParents(state, true)
248
+ const residues = [state.targetTemporary, state.journalTemporary]
249
+ .map((path) => ({ path, metadata: metadataIfPresent(path) }))
250
+ .filter((entry) => entry.metadata)
251
+ if (residues.length === 0) return null
252
+ if (residues.length !== 1) throw new Error("template transaction recovery temporary state is ambiguous")
253
+ const residue = residues[0]
254
+ if (!isSafeRecoveryTemporaryMetadata(residue.metadata, state.expectedUid, state.expectedGid)) throw new Error("template transaction recovery temporary file is unsafe")
255
+ const journalMetadata = metadataIfPresent(state.journalPath)
256
+ if (journalMetadata) readJournal(state)
257
+ if (residue.path === state.targetTemporary) {
258
+ if (!journalMetadata) throw new Error("template transaction recovery target temporary file is orphaned")
259
+ const journal = readJournal(state)
260
+ const installedDigest = currentTargetDigest(state)
261
+ const allowedDigests = [journal.targetTemplateDigest, journal.priorTemplateDigest]
262
+ if (!(allowedDigests.includes(installedDigest) || (!journal.priorTemplate.present && installedDigest === null))) throw new Error("template transaction recovery target state is ambiguous")
263
+ }
264
+ return residue
265
+ }
266
+
267
+ function cleanupRecoveryTemporary(state) {
268
+ const residue = inspectRecoveryTemporary(state)
269
+ if (!residue) return
270
+ deleteDurably(residue.path)
271
+ }
272
+
273
+ function publicJournalRecord(result) {
274
+ if (!result) return null
275
+ const { priorBytes: _ignored, ...record } = result
276
+ return record
277
+ }
278
+
279
+ export function inspectDockerManTemplateTransactionRecoveryIdentity(options = {}) {
280
+ const state = paths(options)
281
+ inspectRecoveryTemporary(state)
282
+ const current = readJournal(state)
283
+ const jellyfin = inspectCurrentJellyfinState()
284
+ if (current && JSON.stringify(jellyfin) !== JSON.stringify(current.jellyfin)) throw new Error("Jellyfin changed during the Butler transaction")
285
+ return publicJournalRecord(current)
286
+ }
287
+
288
+ export function inspectDockerManTemplateTransactionForRecovery(options = {}) {
289
+ const state = paths(options)
290
+ const expected = inspectDockerManTemplateTransactionRecoveryIdentity(options)
291
+ cleanupRecoveryTemporary(state)
292
+ const current = inspectDockerManTemplateTransaction(options)
293
+ if (JSON.stringify(current) !== JSON.stringify(expected)) throw new Error("template transaction changed during recovery")
294
+ return current
295
+ }
296
+
297
+ export function inspectDockerManTemplateTransaction(options = {}) {
298
+ const state = paths(options)
299
+ validateParents(state)
300
+ return publicJournalRecord(readJournal(state))
301
+ }
302
+
303
+ function validateTemplateSource(input, state) {
304
+ validateTemplateParent(state)
305
+ if (metadataIfPresent(state.targetPath)) validateRegularFile(state.targetPath, state.expectedUid, state.expectedGid, "DockerMan template")
306
+ validateDirectory(dirname(input.sourceTemplatePath), state.expectedUid, state.expectedGid)
307
+ validateRegularFile(input.sourceTemplatePath, state.expectedUid, state.expectedGid, "source template")
308
+ if (!VERSION_TAG.test(input.canonicalVersionTag) || !IMAGE_ID.test(input.reviewedManifestDigest) || !IMAGE_ID.test(input.rollbackImageId) || !IMAGE_ID.test(input.targetImageId) || input.rollbackImageId === input.targetImageId) throw new Error("reviewed release identity is invalid")
309
+ const sourceBytes = readFileSync(input.sourceTemplatePath)
310
+ const identity = templateIdentity(sourceBytes, input.canonicalVersionTag)
311
+ return { sourceBytes, identity }
312
+ }
313
+
314
+ export function prepareDockerManTemplateTransaction(input, options = {}) {
315
+ const state = paths(options)
316
+ const { sourceBytes, identity } = validateTemplateSource(input, state)
317
+ const jellyfin = inspectCurrentJellyfinState()
318
+ ensureJournalParent(state)
319
+ const existing = readJournal(state)
320
+ if (existing) {
321
+ if (existing.canonicalVersionTag !== input.canonicalVersionTag || existing.reviewedManifestDigest !== input.reviewedManifestDigest || existing.rollbackImageId !== input.rollbackImageId || existing.targetImageId !== input.targetImageId || existing.targetTemplateDigest !== digest(sourceBytes) || currentTargetDigest(state) !== existing.targetTemplateDigest || JSON.stringify(existing.jellyfin) !== JSON.stringify(jellyfin)) throw new Error("pending template transaction does not match the reviewed release")
322
+ const { priorBytes: _ignored, ...record } = existing
323
+ return record
324
+ }
325
+ let priorTemplate
326
+ let priorTemplateDigest = null
327
+ if (metadataIfPresent(state.targetPath)) {
328
+ const metadata = validateRegularFile(state.targetPath, state.expectedUid, state.expectedGid, "DockerMan template")
329
+ const bytes = readFileSync(state.targetPath)
330
+ priorTemplateDigest = digest(bytes)
331
+ priorTemplate = { present: true, bytesBase64: bytes.toString("base64"), metadata: { uid: metadata.uid, gid: metadata.gid, mode: metadata.mode & 0o777 } }
332
+ } else {
333
+ priorTemplate = { present: false, bytesBase64: null, metadata: null }
334
+ }
335
+ const unsigned = {
336
+ schemaVersion: 1,
337
+ state: "rollback",
338
+ target: { path: state.targetPath, name: identity.name, templateUrl: identity.templateUrl, icon: identity.icon },
339
+ priorTemplate,
340
+ priorTemplateDigest,
341
+ targetTemplateDigest: digest(sourceBytes),
342
+ canonicalVersionTag: input.canonicalVersionTag,
343
+ reviewedManifestDigest: input.reviewedManifestDigest,
344
+ rollbackImageId: input.rollbackImageId,
345
+ targetImageId: input.targetImageId,
346
+ jellyfin,
347
+ }
348
+ const record = writeJournal(unsigned, state)
349
+ options.checkpoint?.("after-template-journal")
350
+ atomicWrite(state.targetPath, state.targetTemporary, sourceBytes, { uid: state.expectedUid, gid: state.expectedGid, mode: 0o600 })
351
+ options.checkpoint?.("after-template-replacement")
352
+ return record
353
+ }
354
+
355
+ export function markDockerManTemplateTransactionCommitting(options = {}) {
356
+ const state = paths(options)
357
+ validateParents(state)
358
+ const current = readJournal(state)
359
+ if (!current) throw new Error("template transaction journal is absent")
360
+ requireActiveAuthority(current, state)
361
+ if (currentTargetDigest(state) !== current.targetTemplateDigest) throw new Error("installed DockerMan template does not match the transaction")
362
+ assertJellyfinUnchanged(current.jellyfin)
363
+ if (current.state === "committing") {
364
+ const { priorBytes: _ignored, ...record } = current
365
+ return record
366
+ }
367
+ const { priorBytes: _ignored, digest: _digest, ...record } = current
368
+ const committing = writeJournal({ ...record, state: "committing" }, state)
369
+ options.checkpoint?.("after-template-committing")
370
+ return committing
371
+ }
372
+
373
+ export function rollbackDockerManTemplateTransaction(options = {}) {
374
+ const state = paths(options)
375
+ validateParents(state)
376
+ const current = readJournal(state)
377
+ if (!current) return false
378
+ if (current.authority && current.authority.state !== "retired") throw new Error("authority epoch must be retired before template rollback")
379
+ const installedDigest = currentTargetDigest(state)
380
+ assertJellyfinUnchanged(current.jellyfin)
381
+ if (current.priorTemplate.present) {
382
+ if (installedDigest !== current.targetTemplateDigest && installedDigest !== current.priorTemplateDigest) throw new Error("installed DockerMan template cannot be safely restored")
383
+ if (installedDigest !== current.priorTemplateDigest) atomicWrite(state.targetPath, state.targetTemporary, current.priorBytes, current.priorTemplate.metadata)
384
+ } else if (installedDigest !== null) {
385
+ if (installedDigest !== current.targetTemplateDigest) throw new Error("installed DockerMan template cannot be safely removed")
386
+ deleteDurably(state.targetPath)
387
+ }
388
+ options.checkpoint?.("after-template-restore")
389
+ deleteDurably(state.journalPath)
390
+ return true
391
+ }
392
+
393
+ function validateFinalProof(proof, record, state) {
394
+ const root = object(proof, "final install proof")
395
+ const container = object(root.container, "final container proof")
396
+ const labels = object(container.labels, "final container labels")
397
+ if (container.name !== "/ouro-butler" || container.imageId !== record.targetImageId || container.imageReference !== record.canonicalVersionTag || container.running !== true || container.healthy !== true || container.autostart !== true) throw new Error("final container proof is invalid")
398
+ if (labels["net.unraid.docker.managed"] !== "dockerman" || labels["net.unraid.docker.icon"] !== record.target.icon || Object.prototype.hasOwnProperty.call(labels, "net.unraid.docker.webui")) throw new Error("final container install labels are invalid")
399
+ const bundle = object(root.bundle, "final bundle proof")
400
+ const bundleData = object(bundle.data, "final bundle proof data")
401
+ if (bundle.ok !== true || bundleData.parity !== "exact" || bundleData.journalState !== "absent" || bundleData.ready !== true) throw new Error("final bundle proof is invalid")
402
+ const dockerMan = object(root.dockerMan, "final DockerMan proof")
403
+ if (dockerMan.templatePath !== state.targetPath || dockerMan.name !== "ouro-butler" || dockerMan.repository !== record.canonicalVersionTag || dockerMan.templateUrl !== record.target.templateUrl || dockerMan.icon !== record.target.icon) throw new Error("final DockerMan proof is invalid")
404
+ const communityApps = object(root.communityApps, "final Community Apps proof")
405
+ const inlineImplementation = communityApps.stateModel === "previous-apps-inline-v1" && communityApps.entryPath === COMMUNITY_APPS_ENTRY_PATH && communityApps.entryFunction === "previous_apps" && communityApps.implementationPath === COMMUNITY_APPS_ENTRY_PATH && communityApps.implementationSymbol === "previous_apps"
406
+ const helperImplementation = communityApps.stateModel === "previous-apps-helper-v1" && communityApps.entryPath === COMMUNITY_APPS_ENTRY_PATH && communityApps.entryFunction === "previous_apps" && communityApps.implementationPath === COMMUNITY_APPS_HELPER_PATH && communityApps.implementationSymbol === "PreviousAppsHelpers::collectDockerApplications"
407
+ const recognizedImplementation = inlineImplementation || helperImplementation
408
+ if (communityApps.installed !== true || communityApps.name !== "ouro-butler" || communityApps.repository !== record.canonicalVersionTag || communityApps.templateUrl !== record.target.templateUrl || !recognizedImplementation) throw new Error("final Community Apps proof is invalid")
409
+ const jellyfin = validateJellyfinState(root.jellyfin, "final Jellyfin proof")
410
+ if (JSON.stringify(jellyfin) !== JSON.stringify(record.jellyfin)) throw new Error("final Jellyfin proof is invalid")
411
+ }
412
+
413
+ export function commitDockerManTemplateTransaction(proof, options = {}) {
414
+ const state = paths(options)
415
+ validateParents(state)
416
+ const current = readJournal(state)
417
+ if (!current) return false
418
+ requireActiveAuthority(current, state)
419
+ if (current.state !== "committing" || currentTargetDigest(state) !== current.targetTemplateDigest) throw new Error("template transaction is not ready to commit")
420
+ validateFinalProof(proof, current, state)
421
+ assertJellyfinUnchanged(current.jellyfin)
422
+ deleteDurably(state.journalPath)
423
+ return true
424
+ }
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
+
566
+ export function verifyDockerManTemplateTransactionJellyfin(options = {}) {
567
+ const state = paths(options)
568
+ validateParents(state)
569
+ const current = readJournal(state)
570
+ if (!current) throw new Error("template transaction journal is absent")
571
+ assertJellyfinUnchanged(current.jellyfin)
572
+ return true
573
+ }
574
+
575
+ export function decideDockerManTemplateRecovery(record, evidence) {
576
+ const state = record?.state
577
+ const bundle = evidence?.bundleJournalState
578
+ const production = evidence?.production
579
+ if (!["rollback", "committing"].includes(state) || !["absent", "rollback", "committing"].includes(bundle) || !["rollback-exact", "target-exact-committing", "target-exact-ready", "adoption-source-exact", "adoption-target-exact-ready", "adoption-evidence-exact-stopped"].includes(production)) throw new Error("template transaction recovery evidence is invalid")
580
+ if (bundle === "rollback" && production === "rollback-exact") return "rollback-both"
581
+ if (state === "committing" && bundle === "committing" && production === "target-exact-committing") {
582
+ const inspection = evidence?.inspection
583
+ if (inspection?.ok !== true || inspection?.data?.parity !== "exact" || inspection?.data?.journalState !== "committing" || inspection?.data?.ready !== false) throw new Error("template transaction recovery inspection is invalid")
584
+ return "finish-bundle-commit"
585
+ }
586
+ if (state === "committing" && bundle === "absent" && production === "target-exact-ready") {
587
+ const inspection = evidence?.inspection
588
+ if (inspection?.ok !== true || inspection?.data?.parity !== "exact" || inspection?.data?.journalState !== "absent" || inspection?.data?.ready !== true) throw new Error("template transaction recovery inspection is invalid")
589
+ return "finish-template-commit"
590
+ }
591
+ if (bundle === "absent" && production === "rollback-exact") return "restore-prior-template"
592
+ if (state === "rollback" && bundle === "absent" && production === "adoption-source-exact") return "restore-prior-template"
593
+ if (state === "rollback" && bundle === "absent" && production === "adoption-target-exact-ready") {
594
+ const inspection = evidence?.inspection
595
+ if (inspection?.ok !== true || inspection?.data?.parity !== "exact" || inspection?.data?.journalState !== "absent" || inspection?.data?.ready !== true) throw new Error("template transaction recovery inspection is invalid")
596
+ return "roll-forward-adoption"
597
+ }
598
+ if (state === "rollback" && bundle === "absent" && production === "adoption-evidence-exact-stopped") return "quarantine-adoption"
599
+ throw new Error("template and bundle transaction topology is incompatible")
600
+ }
601
+
602
+ function parseArguments(argv) {
603
+ const [operation, ...rest] = argv
604
+ const values = new Map()
605
+ for (let index = 0; index < rest.length; index += 2) {
606
+ const flag = rest[index]
607
+ const value = rest[index + 1]
608
+ if (!flag?.startsWith("--") || !value || values.has(flag)) throw new Error("template transaction arguments are invalid")
609
+ values.set(flag, value)
610
+ }
611
+ return { operation, values }
612
+ }
613
+
614
+ function readRootJson(path, label) {
615
+ validateRegularFile(path, 0, 0, label)
616
+ try { return JSON.parse(readFileSync(path, "utf8")) } catch { throw new Error(`${label} JSON is invalid`) }
617
+ }
618
+
619
+ export function runDockerManTemplateTransactionCli(argv, options = {}, write = (text) => process.stdout.write(text)) {
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
+ }
624
+ let result
625
+ if (operation === "prepare" && values.size === 5) {
626
+ result = prepareDockerManTemplateTransaction({
627
+ sourceTemplatePath: values.get("--source-template"),
628
+ canonicalVersionTag: values.get("--version-tag"),
629
+ reviewedManifestDigest: values.get("--manifest-digest"),
630
+ rollbackImageId: values.get("--rollback-image-id"),
631
+ targetImageId: values.get("--target-image-id"),
632
+ }, options)
633
+ } else if (operation === "mark-committing" && values.size === 0) {
634
+ ensureJournalParent(paths(options))
635
+ result = markDockerManTemplateTransactionCommitting(options)
636
+ } else if (operation === "rollback" && values.size === 0) {
637
+ ensureJournalParent(paths(options))
638
+ result = { rolledBack: rollbackDockerManTemplateTransaction(options) }
639
+ } else if (operation === "commit" && values.size === 1 && values.get("--proof")) {
640
+ ensureJournalParent(paths(options))
641
+ result = { committed: commitDockerManTemplateTransaction(readRootJson(values.get("--proof"), "final install proof"), options) }
642
+ } else if (operation === "status" && values.size === 0) {
643
+ ensureJournalParent(paths(options))
644
+ result = inspectDockerManTemplateTransaction(options)
645
+ } else if (operation === "recover-status" && values.size === 0) {
646
+ const state = paths(options)
647
+ if (!metadataIfPresent(state.journalParent)) {
648
+ inspectCurrentJellyfinState()
649
+ ensureJournalParent(state)
650
+ }
651
+ result = inspectDockerManTemplateTransactionForRecovery(options)
652
+ } else if (operation === "recovery-identity" && values.size === 0) {
653
+ const state = paths(options)
654
+ if (!metadataIfPresent(state.journalParent)) {
655
+ inspectCurrentJellyfinState()
656
+ result = null
657
+ } else {
658
+ result = inspectDockerManTemplateTransactionRecoveryIdentity(options)
659
+ }
660
+ } else if (operation === "recovery-action" && values.size === 1 && values.get("--evidence")) {
661
+ ensureJournalParent(paths(options))
662
+ const record = inspectDockerManTemplateTransaction(options)
663
+ if (!record) throw new Error("template transaction journal is absent")
664
+ result = { action: decideDockerManTemplateRecovery(record, readRootJson(values.get("--evidence"), "template recovery evidence")) }
665
+ } else if (operation === "verify-jellyfin" && values.size === 0) {
666
+ result = { unchanged: verifyDockerManTemplateTransactionJellyfin(options) }
667
+ } else if (operation === "jellyfin-status" && values.size === 0) {
668
+ result = inspectCurrentJellyfinState()
669
+ } else {
670
+ throw new Error("Usage: docker-man-template-transaction.mjs <prepare|mark-committing|rollback|commit|status|recover-status|recovery-identity|recovery-action|verify-jellyfin|jellyfin-status> [fixed operation arguments]")
671
+ }
672
+ write(`${JSON.stringify(result)}\n`)
673
+ }
674
+
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))