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

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 +5308 -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 +164 -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 +1749 -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,710 @@
1
+ #!/usr/local/bin/node
2
+
3
+ import { spawn, spawnSync } from "node:child_process"
4
+ import { closeSync, constants, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readlinkSync, readdirSync, statSync, writeFileSync } from "node:fs"
5
+ import { fileURLToPath } from "node:url"
6
+
7
+ const DOCKER = "/usr/bin/docker"
8
+ const KEY_ROOT = "/boot/config/plugins/dynamix.my.servers/keys"
9
+ const GRAPHQL_ENDPOINT = "http://127.0.0.1/graphql"
10
+ const IMAGE_ID = /^sha256:[0-9a-f]{64}$/u
11
+ const CONTAINER_ID = /^[0-9a-f]{64}$/u
12
+ const CANONICAL_NAMES = ["ouro-butler", "ouro-butler-staging", "ouro-butler-rollback"]
13
+ const PROFILES = Object.freeze({
14
+ staging: Object.freeze({ name: "staging", containerName: "ouro-butler-staging", requiredStopped: [], optionalStopped: [], forbidden: ["ouro-butler", "ouro-butler-rollback"] }),
15
+ final: Object.freeze({ name: "final", containerName: "ouro-butler", requiredStopped: [], optionalStopped: ["ouro-butler-rollback"], forbidden: ["ouro-butler-staging"] }),
16
+ })
17
+ const DOCUMENTED_UNIX_CONTROLS = [
18
+ "/tmp/ouroboros-daemon.sock",
19
+ "/tmp/ouroboros-daemon.sock.frontend",
20
+ "/home/ouro/AgentBundles/sanctuary.ouro/state/acceptance/telegram-control.sock",
21
+ ]
22
+ const DOCUMENTED_LOOPBACK_TCP_CONTROLS = new Set([6876])
23
+ const TERMINAL_CONTAINMENT_MAX_SAMPLES = 8
24
+ const TARGET_THAW_MAX_ATTEMPTS = 3
25
+ const WATCHDOG_POLL_MS = 100
26
+ const WATCHDOG_PARENT_DEATH_ENFORCEMENT_MS = 25_000
27
+ const RO_PERMISSIONS = ["ARRAY", "DASHBOARD", "DISK", "DOCKER", "INFO", "LOGS", "NOTIFICATIONS", "SHARE", "VARS"]
28
+ .map((resource) => `${resource}:READ_ANY`).sort()
29
+
30
+ function object(value, label) {
31
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`)
32
+ return value
33
+ }
34
+
35
+ function targetProfile(name) {
36
+ const profile = PROFILES[name]
37
+ if (!profile) throw new Error("deployment target profile is invalid")
38
+ return profile
39
+ }
40
+
41
+ function exactSet(left, right) {
42
+ return left.size === right.size && [...left].every((entry) => right.has(entry))
43
+ }
44
+
45
+ function readCanonicalReadDescriptor() {
46
+ const root = statSync(KEY_ROOT)
47
+ if (!root.isDirectory() || root.uid !== 0 || (root.mode & 0o077) !== 0) throw new Error("Unraid key directory metadata is invalid")
48
+ const matches = []
49
+ for (const entry of readdirSync(KEY_ROOT, { withFileTypes: true })) {
50
+ if (!entry.isFile() || !entry.name.endsWith(".json")) throw new Error("unexpected Unraid key directory entry")
51
+ const fd = openSync(`${KEY_ROOT}/${entry.name}`, constants.O_RDONLY | constants.O_NOFOLLOW)
52
+ let record
53
+ try {
54
+ const metadata = fstatSync(fd)
55
+ if (!metadata.isFile() || metadata.uid !== 0 || (metadata.mode & 0o777) !== 0o600) throw new Error("Unraid key file metadata is invalid")
56
+ record = object(JSON.parse(readFileSync(fd, "utf8")), "Unraid key record")
57
+ } finally { closeSync(fd) }
58
+ const permissions = Array.isArray(record.permissions)
59
+ ? record.permissions.flatMap((raw) => {
60
+ const permission = object(raw, "Unraid key permission")
61
+ return Array.isArray(permission.actions) ? permission.actions.map((action) => `${permission.resource}:${action}`) : []
62
+ }).sort()
63
+ : []
64
+ if (record.name === "Butler RO" && Array.isArray(record.roles) && record.roles.length === 0 && JSON.stringify(permissions) === JSON.stringify(RO_PERMISSIONS) && typeof record.key === "string" && record.key) matches.push(record.key)
65
+ }
66
+ if (matches.length !== 1) throw new Error("canonical read-only Unraid key is absent or ambiguous")
67
+ return matches[0]
68
+ }
69
+
70
+ async function queryGraphqlAutostart(fetchImpl = fetch, readDescriptor = readCanonicalReadDescriptor) {
71
+ const response = await fetchImpl(GRAPHQL_ENDPOINT, {
72
+ method: "POST",
73
+ headers: { "content-type": "application/json", "x-api-key": readDescriptor() },
74
+ body: JSON.stringify({ query: "query AcceptanceContainerTopology { vars { id } docker { containers(skipCache: true) { id names autoStart } } }", variables: {} }),
75
+ signal: AbortSignal.timeout(10_000),
76
+ })
77
+ if (!response.ok) throw new Error("Unraid container topology query failed")
78
+ const envelope = object(await response.json(), "Unraid container topology response")
79
+ if (envelope.errors || !envelope.data) throw new Error("Unraid container topology query was rejected")
80
+ const data = object(envelope.data, "Unraid topology data")
81
+ const serverIdentity = /^([0-9a-f]{64}):vars$/u.exec(object(data.vars, "Unraid topology vars").id)
82
+ if (!serverIdentity) throw new Error("Unraid server identity is invalid")
83
+ const containers = object(data.docker, "Unraid topology docker").containers
84
+ if (!Array.isArray(containers)) throw new Error("Unraid container topology is invalid")
85
+ const result = new Map()
86
+ for (const raw of containers) {
87
+ const container = object(raw, "Unraid topology container")
88
+ if (!Array.isArray(container.names) || container.names.some((name) => typeof name !== "string")) throw new Error("Unraid container topology identity is invalid")
89
+ const canonical = container.names.map((name) => name.replace(/^\//u, "")).filter((name) => CANONICAL_NAMES.includes(name))
90
+ const identity = typeof container.id === "string" ? /^([0-9a-f]{64}):([0-9a-f]{64})$/u.exec(container.id) : null
91
+ if (canonical.length > 1 || (canonical.length === 1 && (!identity || identity[1] !== serverIdentity[1]))) throw new Error("Unraid container topology identity is ambiguous or belongs to another server")
92
+ if (canonical.length === 1 && typeof container.autoStart !== "boolean") throw new Error("Unraid container autostart state is invalid")
93
+ if (canonical.length === 1) {
94
+ if (result.has(canonical[0])) throw new Error("Unraid autostart topology is ambiguous")
95
+ result.set(canonical[0], { containerId: identity[2], autoStart: container.autoStart })
96
+ }
97
+ }
98
+ return result
99
+ }
100
+
101
+ function normalizeRecord(raw, label) {
102
+ const value = object(raw, label)
103
+ if (!CONTAINER_ID.test(value.id) || !Array.isArray(value.names) || value.names.length !== 1 || typeof value.names[0] !== "string") throw new Error(`${label} identity is invalid`)
104
+ const name = value.names[0].replace(/^\//u, "")
105
+ if (!CANONICAL_NAMES.includes(name)) throw new Error(`${label} name is not canonical`)
106
+ if (!IMAGE_ID.test(value.imageId) || typeof value.running !== "boolean" || typeof value.autoStart !== "boolean" || value.restartPolicy !== "unless-stopped" || value.networkMode !== "host") throw new Error(`${label} effective configuration is invalid`)
107
+ if (!Number.isSafeInteger(value.pid) || value.pid < 0 || value.running !== (value.pid > 0)) throw new Error(`${label} PID state is invalid`)
108
+ return { id: value.id, name, imageId: value.imageId, running: value.running, autoStart: value.autoStart, restartPolicy: value.restartPolicy, networkMode: value.networkMode, pid: value.pid }
109
+ }
110
+
111
+ function records(value, label) {
112
+ if (!Array.isArray(value)) throw new Error(`${label} must be an array`)
113
+ const normalized = value.map((entry, index) => normalizeRecord(entry, `${label}[${index}]`))
114
+ if (new Set(normalized.map(({ name }) => name)).size !== normalized.length || new Set(normalized.map(({ id }) => id)).size !== normalized.length) throw new Error(`${label} is duplicate or ambiguous`)
115
+ return normalized.sort((left, right) => left.name.localeCompare(right.name))
116
+ }
117
+
118
+ function signature(value) {
119
+ return JSON.stringify(value.map(({ id, name, imageId, running, autoStart, restartPolicy, networkMode, pid }) => ({ id, name, imageId, running, autoStart, restartPolicy, networkMode, pid })))
120
+ }
121
+
122
+ function attestDeploymentTarget(input) {
123
+ const value = object(input, "deployment target input")
124
+ const profile = targetProfile(value.profile)
125
+ if (!IMAGE_ID.test(value.expectedImageId)) throw new Error("expected image ID is invalid")
126
+ const before = records(value.topologyBefore, "topology before")
127
+ const inspected = records(value.inspected, "canonical inspections")
128
+ const after = records(value.topologyAfter, "topology after")
129
+ if (signature(before) !== signature(after) || signature(before) !== signature(inspected)) throw new Error("canonical deployment topology changed or was incompletely inspected")
130
+ const byName = new Map(inspected.map((entry) => [entry.name, entry]))
131
+ const target = byName.get(profile.containerName)
132
+ if (!target || target.imageId !== value.expectedImageId || !target.running || !target.autoStart) throw new Error("deployment target identity, image, state, or autostart is invalid")
133
+ for (const name of profile.requiredStopped) {
134
+ const stopped = byName.get(name)
135
+ if (!stopped || stopped.running || stopped.autoStart) throw new Error("required rollback target state is invalid")
136
+ }
137
+ for (const name of profile.optionalStopped) {
138
+ const stopped = byName.get(name)
139
+ if (stopped && (stopped.running || stopped.autoStart)) throw new Error("optional rollback target state is invalid")
140
+ }
141
+ for (const name of profile.forbidden) if (byName.has(name)) throw new Error("mixed deployment target state is invalid")
142
+ const activeRunningCardinality = inspected.filter(({ running }) => running).length
143
+ if (activeRunningCardinality !== 1) throw new Error("active Butler running cardinality must equal one")
144
+ return { schemaVersion: "sanctuary-deployment-target-v1", profile: profile.name, targetContainerName: profile.containerName, targetContainerId: target.id, targetImageId: target.imageId, targetPid: target.pid, activeRunningCardinality }
145
+ }
146
+
147
+ function attestOwnedListeners(input) {
148
+ const value = object(input, "listener ownership input")
149
+ if (!Number.isSafeInteger(value.rootPid) || value.rootPid <= 0 || !/^net:\[[0-9]+\]$/u.test(value.netnsBefore) || value.netnsBefore !== value.netnsAfter) throw new Error("target network namespace is invalid or changed")
150
+ const processIdsBefore = value.processIdsBefore
151
+ const processIdsAfter = value.processIdsAfter
152
+ const validProcesses = (pids) => Array.isArray(pids) && pids.length > 0 && pids.includes(value.rootPid) && pids.every((pid) => Number.isSafeInteger(pid) && pid > 0) && new Set(pids).size === pids.length
153
+ if (!validProcesses(processIdsBefore) || !validProcesses(processIdsAfter) || JSON.stringify([...processIdsBefore].sort((a, b) => a - b)) !== JSON.stringify([...processIdsAfter].sort((a, b) => a - b))) throw new Error("target cgroup process membership changed or is invalid")
154
+ const socketSet = (raw, label) => {
155
+ if (!Array.isArray(raw) || raw.some((inode) => !/^[0-9]+$/u.test(inode))) throw new Error(`${label} is invalid`)
156
+ return new Set(raw)
157
+ }
158
+ const inventory = (suffix) => {
159
+ const owned = socketSet(value[`socketInodes${suffix}`], `socket ownership ${suffix.toLowerCase()}`)
160
+ const select = (kind, fallback) => value[kind] ?? value[`${fallback}${suffix}`]
161
+ const tcpRaw = select("tcpListeners", "tcpListeners")
162
+ const udpRaw = select("udpListeners", "udpListeners")
163
+ const unixRaw = select("unixSockets", "unixSockets")
164
+ if (!Array.isArray(tcpRaw) || !Array.isArray(udpRaw) || !Array.isArray(unixRaw)) throw new Error("listener inventory is invalid")
165
+ const tcp = tcpRaw.filter((raw) => owned.has(object(raw, "TCP listener").inode))
166
+ const udp = udpRaw.filter((raw) => owned.has(object(raw, "UDP listener").inode))
167
+ const unix = unixRaw.filter((raw) => owned.has(object(raw, "Unix socket").inode))
168
+ const sortRecords = (records) => [...records].sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)))
169
+ const signature = JSON.stringify({ tcp: sortRecords(tcp), udp: sortRecords(udp), unix: sortRecords(unix) })
170
+ return { owned, tcp, udp, unix, signature }
171
+ }
172
+ const before = inventory("Before")
173
+ const after = inventory("After")
174
+ const terminalFields = ["socketInodesTerminal", "tcpListenersTerminal", "udpListenersTerminal", "unixSocketsTerminal"]
175
+ const providedTerminalFields = terminalFields.filter((field) => value[field] !== undefined)
176
+ if (providedTerminalFields.length !== 0 && providedTerminalFields.length !== terminalFields.length) throw new Error("terminal listener inventory is incomplete")
177
+ const terminal = providedTerminalFields.length === terminalFields.length ? inventory("Terminal") : after
178
+ if (!exactSet(before.owned, after.owned) || !exactSet(after.owned, terminal.owned)) throw new Error("target socket ownership changed")
179
+ if (before.signature !== after.signature || after.signature !== terminal.signature) throw new Error("target listener inventory changed")
180
+ const { owned, tcp, udp, unix } = terminal
181
+ for (const listener of tcp) {
182
+ if (listener.localAddress !== "127.0.0.1" || !DOCUMENTED_LOOPBACK_TCP_CONTROLS.has(listener.port)) throw new Error("target runtime owns an inbound TCP listener")
183
+ }
184
+ if (udp.length !== 0) throw new Error("target runtime owns an inbound UDP listener")
185
+ let unixControlSocketCount = 0
186
+ for (const socket of unix) {
187
+ if (socket.path === "" && socket.flags !== "00010000") continue
188
+ if (!DOCUMENTED_UNIX_CONTROLS.includes(socket.path) || socket.flags !== "00010000" || socket.type !== "0001" || socket.state !== "01") throw new Error("target runtime owns an undocumented Unix endpoint")
189
+ unixControlSocketCount += 1
190
+ }
191
+ return { schemaVersion: "sanctuary-listener-containment-v1", targetRootPid: value.rootPid, networkNamespace: value.netnsBefore, processCount: processIdsAfter.length, ownedSocketCount: owned.size, inboundTcpListenerCount: 0, inboundUdpListenerCount: 0, loopbackTcpControlCount: tcp.length, unixControlSocketCount }
192
+ }
193
+
194
+ function runDocker(args, timeoutMs = 20_000) {
195
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 20_000) throw new Error("Docker command timeout is invalid")
196
+ const result = spawnSync(DOCKER, args, { cwd: "/", encoding: "utf8", timeout: timeoutMs, maxBuffer: 1024 * 1024, env: { PATH: "/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin" }, stdio: ["ignore", "pipe", "ignore"] })
197
+ if (result.error || result.status !== 0) throw new Error("bounded deployment target inspection failed")
198
+ return result.stdout
199
+ }
200
+
201
+ function pausedTargetState(target, run, timeoutMs = 20_000) {
202
+ const template = '{"containerId":{{json .Id}},"running":{{json .State.Running}},"paused":{{json .State.Paused}},"restarting":{{json .State.Restarting}},"dead":{{json .State.Dead}},"pid":{{json .State.Pid}}}'
203
+ const state = object(JSON.parse(run(["inspect", "--format", template, target.targetContainerId], timeoutMs)), "target pause state")
204
+ if (!CONTAINER_ID.test(state.containerId) || typeof state.running !== "boolean" || typeof state.paused !== "boolean"
205
+ || typeof state.restarting !== "boolean" || typeof state.dead !== "boolean" || !Number.isSafeInteger(state.pid) || state.pid < 0) {
206
+ throw new Error("target pause state is invalid")
207
+ }
208
+ if (state.containerId !== target.targetContainerId || state.pid !== target.targetPid) throw new Error("target pause identity changed")
209
+ return state
210
+ }
211
+
212
+ function restorePausedTarget(target, run) {
213
+ let lastError
214
+ for (let attempt = 0; attempt < TARGET_THAW_MAX_ATTEMPTS; attempt += 1) {
215
+ try { run(["unpause", target.targetContainerId]) } catch (error) { lastError = error }
216
+ try {
217
+ const resumed = pausedTargetState(target, run)
218
+ if (resumed.running && !resumed.paused && !resumed.restarting && !resumed.dead) return
219
+ lastError = new Error("target resumed state is invalid")
220
+ } catch (error) {
221
+ lastError = error
222
+ }
223
+ }
224
+ throw new Error("target did not resume after bounded thaw attempts", { cause: lastError })
225
+ }
226
+
227
+ function syncWait(milliseconds) {
228
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds)
229
+ }
230
+
231
+ function watchdogReceipt(root, expected, dependencies = {}) {
232
+ const fileExists = dependencies.existsSync ?? existsSync
233
+ const readFile = dependencies.readFileSync ?? readFileSync
234
+ const wait = dependencies.syncWait ?? syncWait
235
+ const file = `${root}/watchdog-terminal.json`
236
+ for (let attempt = 0; attempt < 100 && !fileExists(file); attempt += 1) wait(50)
237
+ const receipt = object(JSON.parse(readFile(file, "utf8")), "thaw watchdog terminal receipt")
238
+ if (receipt.status !== expected.status || receipt.containerId !== expected.containerId || receipt.pid !== expected.pid
239
+ || receipt.parentBootId !== expected.parentBootId || receipt.parentStarttime !== expected.parentStarttime
240
+ || expected.watchdogPid !== undefined && (receipt.watchdogPid !== expected.watchdogPid || receipt.watchdogBootId !== expected.watchdogBootId
241
+ || receipt.watchdogStarttime !== expected.watchdogStarttime)) throw new Error("thaw watchdog terminal receipt is invalid")
242
+ return receipt
243
+ }
244
+
245
+ function parseProcStatIdentity(content, expectedPid) {
246
+ if (typeof content !== "string" || content.includes("\0") || !Number.isSafeInteger(expectedPid) || expectedPid <= 0) throw new Error("parent process identity is invalid")
247
+ const prefix = `${expectedPid} (`
248
+ const close = content.lastIndexOf(") ")
249
+ if (!content.startsWith(prefix) || close < prefix.length || close + 4 > content.length) throw new Error("parent process identity is invalid")
250
+ const state = content[close + 2]
251
+ if (!/^[A-Za-z]$/u.test(state) || content[close + 3] !== " ") throw new Error("parent process identity is invalid")
252
+ const fields = content.slice(close + 4).trim().split(/\s+/u)
253
+ const starttime = fields[18]
254
+ if (!starttime || !/^[0-9]+$/u.test(starttime) || starttime === "0") throw new Error("parent process identity is invalid")
255
+ return { state, starttime }
256
+ }
257
+
258
+ function terminalProcState(state) {
259
+ return state === "Z" || state === "X" || state === "x"
260
+ }
261
+
262
+ function monotonicMilliseconds() {
263
+ return Number(process.hrtime.bigint() / 1_000_000n)
264
+ }
265
+
266
+ function readParentIdentity(pid) {
267
+ const bootId = readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim()
268
+ if (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/u.test(bootId)) throw new Error("parent boot identity is invalid")
269
+ return { bootId, ...parseProcStatIdentity(readFileSync(`/proc/${pid}/stat`, "utf8"), pid) }
270
+ }
271
+
272
+ function armThawWatchdog(target, dependencies = {}) {
273
+ const now = dependencies.now ?? Date.now
274
+ const readIdentity = dependencies.readParentIdentity ?? readParentIdentity
275
+ const makeDirectory = dependencies.mkdirSync ?? mkdirSync
276
+ const spawnChild = dependencies.spawn ?? spawn
277
+ const fileExists = dependencies.existsSync ?? existsSync
278
+ const wait = dependencies.syncWait ?? syncWait
279
+ const writeFile = dependencies.writeFileSync ?? writeFileSync
280
+ const readFile = dependencies.readFileSync ?? readFileSync
281
+ const parentIdentity = readIdentity(process.pid)
282
+ if (terminalProcState(parentIdentity.state)) throw new Error("thaw watchdog parent is not alive")
283
+ const root = `/run/ouro-thaw-watchdog.${process.pid}.${now()}`
284
+ makeDirectory(root, { mode: 0o700 })
285
+ const child = spawnChild(process.execPath, [fileURLToPath(import.meta.url), "--thaw-watchdog", target.targetContainerId, String(target.targetPid), String(process.pid), parentIdentity.bootId, parentIdentity.starttime, root], {
286
+ cwd: "/", detached: true, stdio: "ignore",
287
+ })
288
+ child.unref()
289
+ for (let attempt = 0; attempt < 100 && !fileExists(`${root}/ready`); attempt += 1) wait(50)
290
+ if (!fileExists(`${root}/ready`)) throw new Error("thaw watchdog did not arm")
291
+ if (!Number.isSafeInteger(child.pid) || child.pid <= 0) {
292
+ if (!dependencies.spawn) throw new Error("thaw watchdog process identity is invalid")
293
+ return { assertLive() {}, disarm() { writeFile(`${root}/disarm`, "\n", { flag: "wx", mode: 0o600 }); return { status: "test-disarmed" } } }
294
+ }
295
+ const ready = object(JSON.parse(readFile(`${root}/ready`, "utf8")), "thaw watchdog ready receipt")
296
+ const watchdogIdentity = { pid: child.pid, bootId: ready.watchdogBootId, starttime: ready.watchdogStarttime }
297
+ if (ready.watchdogPid !== watchdogIdentity.pid || ready.readyAt === undefined || !Number.isSafeInteger(ready.readyAt)
298
+ || watchdogIdentity.bootId !== parentIdentity.bootId || !/^[0-9]+$/u.test(watchdogIdentity.starttime) || watchdogIdentity.starttime === "0") {
299
+ throw new Error("thaw watchdog ready identity is invalid")
300
+ }
301
+ const assertLive = () => {
302
+ let observed
303
+ try { observed = readIdentity(watchdogIdentity.pid) } catch (error) { throw new Error("thaw watchdog child exited", { cause: error }) }
304
+ if (observed.bootId !== watchdogIdentity.bootId || observed.starttime !== watchdogIdentity.starttime || terminalProcState(observed.state)) throw new Error("thaw watchdog child identity is not alive")
305
+ }
306
+ assertLive()
307
+ return {
308
+ assertLive,
309
+ disarm() {
310
+ assertLive()
311
+ writeFile(`${root}/disarm`, "\n", { flag: "wx", mode: 0o600 })
312
+ return watchdogReceipt(root, { status: "disarmed", containerId: target.targetContainerId, pid: target.targetPid, parentBootId: parentIdentity.bootId, parentStarttime: parentIdentity.starttime, watchdogPid: watchdogIdentity.pid, watchdogBootId: watchdogIdentity.bootId, watchdogStarttime: watchdogIdentity.starttime }, dependencies)
313
+ },
314
+ }
315
+ }
316
+
317
+ function runKillableCommand(executable, args, timeoutMs, dependencies = {}) {
318
+ if (typeof executable !== "string" || !executable.startsWith("/") || !Array.isArray(args) || args.some((arg) => typeof arg !== "string")
319
+ || !Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 20_000) return Promise.reject(new Error("killable command deadline is invalid"))
320
+ const spawnChild = dependencies.spawn ?? spawn
321
+ const killGroup = dependencies.killProcessGroup ?? ((pid, signal) => {
322
+ try { process.kill(-pid, signal) } catch {
323
+ try { process.kill(pid, signal) } catch {}
324
+ }
325
+ })
326
+ return new Promise((resolve, reject) => {
327
+ let settled = false
328
+ let stdout = ""
329
+ const child = spawnChild(executable, args, {
330
+ cwd: "/", detached: true, stdio: ["ignore", "pipe", "ignore"],
331
+ env: { PATH: "/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin" },
332
+ })
333
+ const finish = (error, value) => {
334
+ if (settled) return
335
+ settled = true
336
+ clearTimeout(termTimer)
337
+ clearTimeout(timer)
338
+ if (error) reject(error)
339
+ else resolve(value)
340
+ }
341
+ child.stdout?.on("data", (chunk) => {
342
+ stdout += String(chunk)
343
+ if (Buffer.byteLength(stdout, "utf8") > 1024 * 1024) {
344
+ killGroup(child.pid, "SIGKILL")
345
+ finish(new Error("killable command output exceeded its bound"))
346
+ }
347
+ })
348
+ child.once("error", () => finish(new Error("killable command failed to launch")))
349
+ child.once("close", (status, signal) => {
350
+ if (status === 0 && signal === null) finish(undefined, stdout)
351
+ else finish(new Error("killable command failed"))
352
+ })
353
+ const graceMs = Math.min(100, Math.max(1, Math.floor(timeoutMs / 4)))
354
+ const termTimer = setTimeout(() => { killGroup(child.pid, "SIGTERM") }, timeoutMs - graceMs)
355
+ const timer = setTimeout(() => {
356
+ killGroup(child.pid, "SIGKILL")
357
+ finish(new Error("killable command hard deadline timed out"))
358
+ }, timeoutMs)
359
+ })
360
+ }
361
+
362
+ async function runDockerWatchdog(args, timeoutMs) {
363
+ return await runKillableCommand(DOCKER, args, timeoutMs)
364
+ }
365
+
366
+ async function pausedTargetStateAsync(target, run, timeoutMs) {
367
+ const template = '{"containerId":{{json .Id}},"running":{{json .State.Running}},"paused":{{json .State.Paused}},"restarting":{{json .State.Restarting}},"dead":{{json .State.Dead}},"pid":{{json .State.Pid}}}'
368
+ const state = object(JSON.parse(await run(["inspect", "--format", template, target.targetContainerId], timeoutMs)), "target pause state")
369
+ if (!CONTAINER_ID.test(state.containerId) || typeof state.running !== "boolean" || typeof state.paused !== "boolean"
370
+ || typeof state.restarting !== "boolean" || typeof state.dead !== "boolean" || !Number.isSafeInteger(state.pid) || state.pid < 0) throw new Error("target pause state is invalid")
371
+ if (state.containerId !== target.targetContainerId || state.pid !== target.targetPid) throw new Error("target pause identity changed")
372
+ return state
373
+ }
374
+
375
+ async function runThawWatchdog(targetContainerId, targetPid, parentPid, parentBootId, parentStarttime, root, dependencies = {}) {
376
+ const target = { targetContainerId, targetPid }
377
+ const wallNow = dependencies.now ?? Date.now
378
+ const monotonicNow = dependencies.monotonicNow ?? monotonicMilliseconds
379
+ const sleep = dependencies.sleep ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)))
380
+ const fileExists = dependencies.existsSync ?? existsSync
381
+ const writeFile = dependencies.writeFileSync ?? writeFileSync
382
+ const readIdentity = dependencies.readParentIdentity ?? readParentIdentity
383
+ const run = dependencies.runDocker ?? runDockerWatchdog
384
+ const enforcementMs = dependencies.enforcementMs ?? WATCHDOG_PARENT_DEATH_ENFORCEMENT_MS
385
+ const recoveryPollMs = dependencies.recoveryPollMs ?? 250
386
+ const rootIdentity = /^\/run\/ouro-thaw-watchdog\.([0-9]+)\.([0-9]+)$/u.exec(root)
387
+ if (!CONTAINER_ID.test(targetContainerId) || !Number.isSafeInteger(targetPid) || targetPid <= 0
388
+ || !Number.isSafeInteger(parentPid) || parentPid <= 0
389
+ || !/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/u.test(parentBootId)
390
+ || !/^[0-9]+$/u.test(parentStarttime) || parentStarttime === "0"
391
+ || !rootIdentity || Number(rootIdentity[1]) !== parentPid
392
+ || !Number.isSafeInteger(enforcementMs) || enforcementMs <= 0 || !Number.isSafeInteger(recoveryPollMs) || recoveryPollMs <= 0) throw new Error("thaw watchdog lease is invalid")
393
+ const initial = await pausedTargetStateAsync(target, run, 20_000)
394
+ if (!initial.running || initial.paused || initial.restarting || initial.dead) throw new Error("thaw watchdog initial state is invalid")
395
+ const selfIdentity = dependencies.readSelfIdentity?.() ?? (dependencies.readParentIdentity
396
+ ? { bootId: parentBootId, state: "S", starttime: "1" }
397
+ : readParentIdentity(process.pid))
398
+ if (terminalProcState(selfIdentity.state) || selfIdentity.bootId !== parentBootId || !/^[0-9]+$/u.test(selfIdentity.starttime) || selfIdentity.starttime === "0") throw new Error("thaw watchdog process identity is invalid")
399
+ const readyAt = wallNow()
400
+ writeFile(`${root}/ready`, `${JSON.stringify({ watchdogPid: process.pid, watchdogBootId: selfIdentity.bootId, watchdogStarttime: selfIdentity.starttime, readyAt })}\n`, { flag: "wx", mode: 0o600 })
401
+ const terminalReceipt = (status) => writeFile(`${root}/watchdog-terminal.json`, `${JSON.stringify({ status, containerId: targetContainerId, pid: targetPid, parentBootId, parentStarttime, watchdogPid: process.pid, watchdogBootId: selfIdentity.bootId, watchdogStarttime: selfIdentity.starttime })}\n`, { flag: "wx", mode: 0o600 })
402
+ let transientIdentityFailures = 0
403
+ for (;;) {
404
+ if (fileExists(`${root}/disarm`)) {
405
+ const restored = await pausedTargetStateAsync(target, run, 20_000)
406
+ if (!restored.running || restored.paused || restored.restarting || restored.dead) throw new Error("thaw watchdog disarm state is invalid")
407
+ terminalReceipt("disarmed")
408
+ return
409
+ }
410
+ let parentAlive = false
411
+ try {
412
+ const observed = readIdentity(parentPid)
413
+ parentAlive = observed.bootId === parentBootId && observed.starttime === parentStarttime && !terminalProcState(observed.state)
414
+ transientIdentityFailures = 0
415
+ } catch (error) {
416
+ const code = error && typeof error === "object" && "code" in error ? error.code : undefined
417
+ const confirmedMissing = code === "ENOENT" || code === "ESRCH" || error instanceof Error && /^(?:ENOENT|ESRCH)$/u.test(error.message)
418
+ if (confirmedMissing) break
419
+ transientIdentityFailures += 1
420
+ if (transientIdentityFailures >= 3) throw new Error("thaw watchdog parent identity remained unreadable", { cause: error })
421
+ await sleep(WATCHDOG_POLL_MS)
422
+ continue
423
+ }
424
+ if (!parentAlive) break
425
+ await sleep(WATCHDOG_POLL_MS)
426
+ }
427
+ const deadline = monotonicNow() + enforcementMs
428
+ const remaining = () => {
429
+ const milliseconds = deadline - monotonicNow()
430
+ if (!Number.isSafeInteger(milliseconds) || milliseconds <= 0) throw new Error("thaw watchdog recovery deadline expired")
431
+ return Math.min(20_000, milliseconds)
432
+ }
433
+ for (;;) {
434
+ const state = await pausedTargetStateAsync(target, run, remaining())
435
+ remaining()
436
+ if (!state.running || state.restarting || state.dead) throw new Error("thaw watchdog target state drifted")
437
+ if (state.paused) {
438
+ await run(["unpause", targetContainerId], remaining())
439
+ remaining()
440
+ }
441
+ else if (deadline - monotonicNow() <= recoveryPollMs) {
442
+ remaining()
443
+ terminalReceipt("parent-death-recovered")
444
+ return
445
+ }
446
+ const sleepMs = Math.min(recoveryPollMs, deadline - monotonicNow())
447
+ if (sleepMs <= 0) throw new Error("thaw watchdog recovery deadline expired")
448
+ await sleep(sleepMs)
449
+ }
450
+ }
451
+
452
+ function withPausedTarget(target, operation, dependencies = {}) {
453
+ const run = dependencies.runDocker ?? runDocker
454
+ const armWatchdog = dependencies.armWatchdog ?? (dependencies.runDocker ? () => ({ disarm: () => ({ status: "test-disarmed" }) }) : armThawWatchdog)
455
+ const original = pausedTargetState(target, run)
456
+ if (!original.running || original.paused || original.restarting || original.dead) throw new Error("target original running state is invalid")
457
+ const watchdog = armWatchdog(target)
458
+ watchdog.assertLive?.()
459
+ let result
460
+ let operationError
461
+ let restoreError
462
+ try {
463
+ run(["pause", target.targetContainerId])
464
+ watchdog.assertLive?.()
465
+ const paused = pausedTargetState(target, run)
466
+ if (!paused.running || !paused.paused || paused.restarting || paused.dead) throw new Error("target paused state is invalid")
467
+ watchdog.assertLive?.()
468
+ result = operation()
469
+ watchdog.assertLive?.()
470
+ } catch (error) {
471
+ operationError = error
472
+ } finally {
473
+ try {
474
+ restorePausedTarget(target, run)
475
+ } catch (error) {
476
+ restoreError = error
477
+ }
478
+ }
479
+ if (!restoreError) {
480
+ try { watchdog.disarm() } catch (error) { restoreError = error }
481
+ }
482
+ if (restoreError && operationError) throw new AggregateError([operationError, restoreError], "target scan and restoration both failed")
483
+ if (restoreError) throw new Error("target failed to restore its original running state", { cause: restoreError })
484
+ if (operationError) throw operationError
485
+ return result
486
+ }
487
+
488
+ function dockerTopology(run = runDocker) {
489
+ const rows = run(["container", "ls", "-a", "--no-trunc", "--format", '{"ID":{{json .ID}},"Names":{{json .Names}}}'])
490
+ return rows.split(/\r?\n/u).filter(Boolean).map((line) => JSON.parse(line))
491
+ .filter((row) => typeof row.Names === "string" && CANONICAL_NAMES.includes(row.Names))
492
+ .map((row) => {
493
+ if (!CONTAINER_ID.test(row.ID)) throw new Error("canonical Butler list identity is invalid")
494
+ return { id: row.ID, name: row.Names }
495
+ })
496
+ }
497
+
498
+ function autostartNames() {
499
+ const content = readFileSync("/var/lib/docker/unraid-autostart", "utf8")
500
+ const names = content.split(/\r?\n/u).map((line) => line.trim().split(/\s+/u)[0]).filter((name) => CANONICAL_NAMES.includes(name))
501
+ if (new Set(names).size !== names.length) throw new Error("canonical autostart inventory is ambiguous")
502
+ return new Set(names)
503
+ }
504
+
505
+ async function captureCanonicalRecords(dependencies = {}) {
506
+ const listed = (dependencies.dockerTopology ?? dockerTopology)()
507
+ if (new Set(listed.map(({ name }) => name)).size !== listed.length || new Set(listed.map(({ id }) => id)).size !== listed.length) throw new Error("canonical Butler topology is ambiguous")
508
+ if (listed.length === 0) throw new Error("canonical Butler topology is empty")
509
+ const inspections = dependencies.inspectCanonical
510
+ ? dependencies.inspectCanonical(listed.map(({ id }) => id))
511
+ : JSON.parse(runDocker(["inspect", ...listed.map(({ id }) => id)]).trim())
512
+ if (!Array.isArray(inspections) || inspections.length !== listed.length) throw new Error("canonical Butler inspection is incomplete")
513
+ const starts = new Set((dependencies.autostartNames ?? (() => [...autostartNames()]))())
514
+ const graphqlRecords = await (dependencies.graphqlAutostartNames ?? queryGraphqlAutostart)()
515
+ if (!(graphqlRecords instanceof Map)) throw new Error("Unraid GraphQL autostart state is invalid")
516
+ const records = inspections.map((entry) => {
517
+ const name = String(entry.Name).replace(/^\//u, "")
518
+ const listedRecord = listed.find(({ id }) => id === entry.Id)
519
+ if (!listedRecord || listedRecord.name !== name) throw new Error("canonical Butler identity changed during atomic inspection")
520
+ return {
521
+ id: entry.Id,
522
+ names: [entry.Name],
523
+ imageId: entry.Image,
524
+ running: entry.State?.Running,
525
+ autoStart: starts.has(name),
526
+ restartPolicy: entry.HostConfig?.RestartPolicy?.Name,
527
+ pid: entry.State?.Pid,
528
+ networkMode: entry.HostConfig?.NetworkMode,
529
+ }
530
+ })
531
+ const recordNames = new Set(records.map(({ names }) => names[0].replace(/^\//u, "")))
532
+ if (!exactSet(recordNames, new Set(graphqlRecords.keys()))) throw new Error("Unraid GraphQL canonical topology presence disagrees with Docker")
533
+ const graphqlStarts = new Set([...graphqlRecords].filter(([, identity]) => identity?.autoStart === true).map(([name]) => name))
534
+ if (!exactSet(starts, graphqlStarts)) throw new Error("Unraid GraphQL and durable autostart state disagree")
535
+ for (const [name, identity] of graphqlRecords) {
536
+ const record = records.find((candidate) => candidate.names[0].replace(/^\//u, "") === name)
537
+ if (!record || record.id !== identity?.containerId || record.autoStart !== identity?.autoStart) throw new Error("Unraid GraphQL autostart identity disagrees with Docker")
538
+ }
539
+ return records
540
+ }
541
+
542
+ function parseProcNet(content, ipv6) {
543
+ return content.split(/\r?\n/u).slice(1).filter(Boolean).map((line) => line.trim().split(/\s+/u)).filter((fields) => fields[3] === "0A").map((fields) => {
544
+ const [address, portHex] = fields[1].split(":")
545
+ const localAddress = ipv6 ? address : [6, 4, 2, 0].map((offset) => Number.parseInt(address.slice(offset, offset + 2), 16)).join(".")
546
+ return { inode: fields[9], localAddress, port: Number.parseInt(portHex, 16) }
547
+ })
548
+ }
549
+
550
+ function parseProcUdp(content, ipv6) {
551
+ return content.split(/\r?\n/u).slice(1).filter(Boolean).map((line) => line.trim().split(/\s+/u)).map((fields) => {
552
+ const [address, portHex] = fields[1].split(":")
553
+ const localAddress = ipv6 ? address : [6, 4, 2, 0].map((offset) => Number.parseInt(address.slice(offset, offset + 2), 16)).join(".")
554
+ return { inode: fields[9], localAddress, port: Number.parseInt(portHex, 16) }
555
+ }).filter(({ port }) => port !== 0)
556
+ }
557
+
558
+ function cgroupProcessIds(rootPid, containerId, dependencies = {}) {
559
+ if (!Number.isSafeInteger(rootPid) || rootPid <= 0 || !CONTAINER_ID.test(containerId)) throw new Error("target cgroup identity is invalid")
560
+ const readCgroup = dependencies.readCgroup ?? ((pid) => readFileSync(`/proc/${pid}/cgroup`, "utf8"))
561
+ const readCgroupMembership = dependencies.readCgroupMembership ?? ((file) => readFileSync(file, "utf8"))
562
+ const lines = readCgroup(rootPid).split(/\r?\n/u).filter(Boolean)
563
+ const expectedPath = `/docker/${containerId}`
564
+ if (lines.length !== 1 || lines[0] !== `0::${expectedPath}`) throw new Error("target cgroup is not the exact Docker cgroup-v2 identity")
565
+ const membership = (file, label) => {
566
+ const raw = readCgroupMembership(`/sys/fs/cgroup${expectedPath}/${file}`).trim()
567
+ const ids = raw ? raw.split(/\s+/u).map(Number) : []
568
+ if (ids.length === 0 || ids.some((pid) => !Number.isSafeInteger(pid) || pid <= 0) || new Set(ids).size !== ids.length) throw new Error(`target cgroup ${label} membership is invalid`)
569
+ return ids.sort((left, right) => left - right)
570
+ }
571
+ const processIds = membership("cgroup.procs", "process")
572
+ const threadIds = membership("cgroup.threads", "thread")
573
+ if (!processIds.includes(rootPid)) throw new Error("target root PID is absent from its cgroup")
574
+ if (!processIds.every((pid) => threadIds.includes(pid))) throw new Error("target cgroup thread membership is incomplete")
575
+ return { path: expectedPath, processIds, threadIds }
576
+ }
577
+
578
+ function ownedSocketInodes(threadIds, dependencies = {}) {
579
+ const listFileDescriptors = dependencies.listFileDescriptors ?? ((tid) => readdirSync(`/proc/${tid}/fd`))
580
+ const readDescriptorLink = dependencies.readDescriptorLink ?? ((tid, fd) => readlinkSync(`/proc/${tid}/fd/${fd}`))
581
+ const inodes = []
582
+ for (const tid of threadIds) for (const fd of listFileDescriptors(tid)) {
583
+ const match = /^socket:\[([0-9]+)\]$/u.exec(readDescriptorLink(tid, fd))
584
+ if (match) inodes.push(match[1])
585
+ }
586
+ return inodes
587
+ }
588
+
589
+
590
+ function parseProcUnix(content) {
591
+ return content.split(/\r?\n/u).slice(1).filter(Boolean).map((line) => line.trim().split(/\s+/u))
592
+ .filter((fields) => /^[0-9]+$/u.test(fields[6] ?? ""))
593
+ .map((fields) => ({ inode: fields[6], path: fields.slice(7).join(" "), flags: fields[3], type: fields[4], state: fields[5] }))
594
+ }
595
+
596
+ function sortedUnique(values) {
597
+ return [...new Set(values)].sort((left, right) => String(left).localeCompare(String(right)))
598
+ }
599
+
600
+ function sortedRecords(values) {
601
+ return [...values].sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)))
602
+ }
603
+
604
+ function sameMembership(left, right) {
605
+ return left.path === right.path
606
+ && exactSet(new Set(left.processIds), new Set(right.processIds))
607
+ && exactSet(new Set(left.threadIds), new Set(right.threadIds))
608
+ }
609
+
610
+ function completeContainmentSnapshot(provisional, readers) {
611
+ const netnsBefore = readers.readNetns(provisional.targetPid)
612
+ const membershipBefore = readers.readMembership(provisional.targetPid, provisional.targetContainerId)
613
+ const socketInodesBefore = sortedUnique(readers.readSockets(membershipBefore.threadIds))
614
+ const tcpListenersRaw = readers.readTcp(provisional.targetPid)
615
+ const udpListenersRaw = readers.readUdp(provisional.targetPid)
616
+ const unixSocketsRaw = readers.readUnix(provisional.targetPid)
617
+ const membershipAfter = readers.readMembership(provisional.targetPid, provisional.targetContainerId)
618
+ const socketInodesAfter = sortedUnique(readers.readSockets(membershipAfter.threadIds))
619
+ const netnsAfter = readers.readNetns(provisional.targetPid)
620
+ if (netnsBefore !== netnsAfter || !sameMembership(membershipBefore, membershipAfter)
621
+ || !exactSet(new Set(socketInodesBefore), new Set(socketInodesAfter))) return null
622
+ const owned = new Set(socketInodesAfter)
623
+ return {
624
+ netns: netnsAfter,
625
+ membership: membershipAfter,
626
+ socketInodes: socketInodesAfter,
627
+ tcpListeners: sortedRecords(tcpListenersRaw.filter((listener) => owned.has(listener.inode))),
628
+ udpListeners: sortedRecords(udpListenersRaw.filter((listener) => owned.has(listener.inode))),
629
+ unixSockets: sortedRecords(unixSocketsRaw.filter((socket) => owned.has(socket.inode))),
630
+ }
631
+ }
632
+
633
+ function convergedTerminalContainment(provisional, readers) {
634
+ let previous = null
635
+ for (let attempt = 0; attempt < TERMINAL_CONTAINMENT_MAX_SAMPLES; attempt += 1) {
636
+ const current = completeContainmentSnapshot(provisional, readers)
637
+ if (current && previous && JSON.stringify(current) === JSON.stringify(previous)) return current
638
+ previous = current
639
+ }
640
+ throw new Error("terminal containment snapshot did not converge")
641
+ }
642
+
643
+ async function runDeploymentTargetAudit(profileName, expectedImageId, dependencies = {}) {
644
+ const capture = dependencies.captureCanonicalRecords ?? captureCanonicalRecords
645
+ const before = await capture()
646
+ const provisional = attestDeploymentTarget({ profile: profileName, expectedImageId, topologyBefore: before, inspected: before, topologyAfter: before })
647
+ const readNetns = dependencies.readNetns ?? ((pid) => readlinkSync(`/proc/${pid}/ns/net`))
648
+ const readMembership = dependencies.cgroupProcessIds ?? cgroupProcessIds
649
+ const readSockets = dependencies.ownedSocketInodes ?? ownedSocketInodes
650
+ const readTcp = dependencies.readTcpListeners ?? ((pid) => [...parseProcNet(readFileSync(`/proc/${pid}/net/tcp`, "utf8"), false), ...parseProcNet(readFileSync(`/proc/${pid}/net/tcp6`, "utf8"), true)])
651
+ const readUdp = dependencies.readUdpListeners ?? ((pid) => [...parseProcUdp(readFileSync(`/proc/${pid}/net/udp`, "utf8"), false), ...parseProcUdp(readFileSync(`/proc/${pid}/net/udp6`, "utf8"), true)])
652
+ const readUnix = dependencies.readUnixSockets ?? ((pid) => parseProcUnix(readFileSync(`/proc/${pid}/net/unix`, "utf8")))
653
+ const quiesceTarget = dependencies.quiesceTarget ?? withPausedTarget
654
+ const containment = quiesceTarget(provisional, () => {
655
+ const netnsBefore = readNetns(provisional.targetPid)
656
+ const membershipBefore = readMembership(provisional.targetPid, provisional.targetContainerId)
657
+ const processIdsBefore = membershipBefore.processIds
658
+ const socketInodesBefore = readSockets(membershipBefore.threadIds)
659
+ const tcpListenersBefore = readTcp(provisional.targetPid)
660
+ const udpListenersBefore = readUdp(provisional.targetPid)
661
+ const unixSocketsBefore = readUnix(provisional.targetPid)
662
+ const membershipAfter = readMembership(provisional.targetPid, provisional.targetContainerId)
663
+ if (membershipBefore.path !== membershipAfter.path) throw new Error("target cgroup changed")
664
+ if (JSON.stringify(processIdsBefore) !== JSON.stringify(membershipAfter.processIds)) throw new Error("target cgroup process membership changed")
665
+ if (JSON.stringify(membershipBefore.threadIds) !== JSON.stringify(membershipAfter.threadIds)) throw new Error("target cgroup thread membership changed")
666
+ const processIdsAfter = membershipAfter.processIds
667
+ const socketInodesAfter = readSockets(membershipAfter.threadIds)
668
+ const tcpListenersAfter = readTcp(provisional.targetPid)
669
+ const udpListenersAfter = readUdp(provisional.targetPid)
670
+ const unixSocketsAfter = readUnix(provisional.targetPid)
671
+ const netnsAfter = readNetns(provisional.targetPid)
672
+ const terminal = convergedTerminalContainment(provisional, { readNetns, readMembership, readSockets, readTcp, readUdp, readUnix })
673
+ return { membershipBefore, processIdsBefore, socketInodesBefore, tcpListenersBefore, udpListenersBefore, unixSocketsBefore, membershipAfter, processIdsAfter, socketInodesAfter, tcpListenersAfter, udpListenersAfter, unixSocketsAfter, netnsBefore, netnsAfter, terminal }
674
+ })
675
+ const after = await capture()
676
+ const { membershipBefore, processIdsBefore, socketInodesBefore, tcpListenersBefore, udpListenersBefore, unixSocketsBefore, membershipAfter, processIdsAfter, socketInodesAfter, tcpListenersAfter, udpListenersAfter, unixSocketsAfter, netnsBefore, netnsAfter, terminal } = containment
677
+ const membershipTerminal = terminal.membership
678
+ const socketInodesTerminal = terminal.socketInodes
679
+ const tcpListenersTerminal = terminal.tcpListeners
680
+ const udpListenersTerminal = terminal.udpListeners
681
+ const unixSocketsTerminal = terminal.unixSockets
682
+ const sameIds = (left, right) => exactSet(new Set(left), new Set(right))
683
+ if (netnsBefore !== netnsAfter || netnsAfter !== terminal.netns) throw new Error("target network namespace changed")
684
+ if (membershipBefore.path !== membershipTerminal.path || membershipAfter.path !== membershipTerminal.path) throw new Error("target cgroup changed")
685
+ if (!sameIds(processIdsBefore, membershipTerminal.processIds) || !sameIds(processIdsAfter, membershipTerminal.processIds)) throw new Error("target cgroup process membership changed")
686
+ if (!sameIds(membershipBefore.threadIds, membershipTerminal.threadIds) || !sameIds(membershipAfter.threadIds, membershipTerminal.threadIds)) throw new Error("target cgroup thread membership changed")
687
+ if (!exactSet(new Set(socketInodesBefore), new Set(socketInodesTerminal)) || !exactSet(new Set(socketInodesAfter), new Set(socketInodesTerminal))) throw new Error("target socket ownership changed")
688
+ const deployment = attestDeploymentTarget({ profile: profileName, expectedImageId, topologyBefore: before, inspected: before, topologyAfter: after })
689
+ const listeners = { ...attestOwnedListeners({ rootPid: deployment.targetPid, netnsBefore, netnsAfter, processIdsBefore, processIdsAfter, socketInodesBefore, socketInodesAfter, socketInodesTerminal, tcpListenersBefore, tcpListenersAfter, tcpListenersTerminal, udpListenersBefore, udpListenersAfter, udpListenersTerminal, unixSocketsBefore, unixSocketsAfter, unixSocketsTerminal }), cgroupPath: membershipTerminal.path, threadCount: membershipTerminal.threadIds.length }
690
+ return { schemaVersion: "sanctuary-effective-deployment-v1", deployment, listeners }
691
+ }
692
+
693
+ if (process.argv[1]?.endsWith("sanctuary-deployment-target.mjs") && process.argv[2] === "--thaw-watchdog") {
694
+ const [targetContainerId, rawTargetPid, rawParentPid, parentBootId, parentStarttime, root] = process.argv.slice(3)
695
+ try { await runThawWatchdog(targetContainerId, Number(rawTargetPid), Number(rawParentPid), parentBootId, parentStarttime, root) } catch (error) {
696
+ if (/^\/run\/ouro-thaw-watchdog\.[0-9]+\.[0-9]+$/u.test(root ?? "") && !existsSync(`${root}/watchdog-terminal.json`)) {
697
+ try { writeFileSync(`${root}/watchdog-terminal.json`, `${JSON.stringify({ status: "failed", containerId: targetContainerId, pid: Number(rawTargetPid), parentBootId, parentStarttime, error: error instanceof Error ? error.message.slice(0, 240) : "unknown" })}\n`, { flag: "wx", mode: 0o600 }) } catch {}
698
+ }
699
+ process.exitCode = 1
700
+ }
701
+ } else if (process.argv[1]?.endsWith("sanctuary-deployment-target.mjs")) {
702
+ const [profile, expectedImageId] = process.argv.slice(2)
703
+ if (process.argv.length !== 4) process.exitCode = 2
704
+ else {
705
+ try { process.stdout.write(`${JSON.stringify(await runDeploymentTargetAudit(profile, expectedImageId))}\n`) }
706
+ catch { process.exitCode = 1 }
707
+ }
708
+ }
709
+
710
+ export { attestDeploymentTarget, attestOwnedListeners, armThawWatchdog, captureCanonicalRecords, cgroupProcessIds, dockerTopology, ownedSocketInodes, parseProcNet, parseProcStatIdentity, parseProcUdp, parseProcUnix, queryGraphqlAutostart, runDeploymentTargetAudit, runKillableCommand, runThawWatchdog, targetProfile, withPausedTarget }