@ouro.bot/cli 0.1.0-alpha.81 → 0.1.0-alpha.810

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 (623) 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/changelog.json +5177 -16
  15. package/deploy/unraid/Dockerfile +48 -0
  16. package/deploy/unraid/README.txt +3032 -0
  17. package/deploy/unraid/audit-container-spec.sh +4 -0
  18. package/deploy/unraid/container-runtime.json +1 -0
  19. package/deploy/unraid/docker-man-template-transaction.mjs +526 -0
  20. package/deploy/unraid/docker-man-template-xml.cjs +105 -0
  21. package/deploy/unraid/migrate-sanctuary-bundle.mjs +60 -0
  22. package/deploy/unraid/ouro-events/bootstrap-spool.sh +92 -0
  23. package/deploy/unraid/ouro-events/emit-event.mjs +310 -0
  24. package/deploy/unraid/ouro-events/emit-usenet-event.sh +54 -0
  25. package/deploy/unraid/ouro-events/install-usenet-guard.sh +435 -0
  26. package/deploy/unraid/ouro-events/usenet-health.sh +176 -0
  27. package/deploy/unraid/sanctuary-acceptance-adapter.sh +43 -0
  28. package/deploy/unraid/sanctuary-acceptance-contract.json +198 -0
  29. package/deploy/unraid/sanctuary-acceptance-harness.sh +23 -0
  30. package/deploy/unraid/sanctuary-deployment-target.mjs +709 -0
  31. package/deploy/unraid/sanctuary-safe-rename.py +24 -0
  32. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +2030 -0
  33. package/deploy/unraid/sanctuary-unit16-run.sh +560 -0
  34. package/deploy/unraid/sanctuary-unit18-target-audit.sh +27 -0
  35. package/deploy/unraid/sanctuary.ouro/agent.json +24 -0
  36. package/deploy/unraid/sanctuary.ouro/arc/README.md +3 -0
  37. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +5 -0
  38. package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +8 -0
  39. package/deploy/unraid/sanctuary.ouro/provider-readiness.json +11 -0
  40. package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +5 -0
  41. package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +5 -0
  42. package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +11 -0
  43. package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +25 -0
  44. package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +11 -0
  45. package/deploy/unraid/sanctuary.ouro/state/policy/steward.json +7 -0
  46. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +23 -0
  47. package/deploy/unraid/sanctuary.xml +27 -0
  48. package/dist/a2a/card.js +57 -0
  49. package/dist/a2a/client.js +180 -0
  50. package/dist/a2a/config.js +50 -0
  51. package/dist/a2a/delegation-stores.js +56 -0
  52. package/dist/a2a/did-resolution.js +93 -0
  53. package/dist/a2a/identity.js +170 -0
  54. package/dist/a2a/inbound-share.js +107 -0
  55. package/dist/a2a/mission-result-wire.js +196 -0
  56. package/dist/a2a/onboarding.js +115 -0
  57. package/dist/a2a/pin-store.js +132 -0
  58. package/dist/a2a/seen-ledger.js +120 -0
  59. package/dist/a2a/server.js +604 -0
  60. package/dist/a2a/task-store.js +69 -0
  61. package/dist/a2a/transport.js +45 -0
  62. package/dist/a2a/types.js +3 -0
  63. package/dist/arc/attention-types.js +8 -0
  64. package/dist/arc/cares.js +360 -0
  65. package/dist/arc/episodes.js +118 -0
  66. package/dist/arc/evolution.js +490 -0
  67. package/dist/arc/flight-recorder.js +1034 -0
  68. package/dist/arc/intentions.js +134 -0
  69. package/dist/arc/json-store.js +117 -0
  70. package/dist/arc/obligations.js +386 -0
  71. package/dist/arc/packets.js +336 -0
  72. package/dist/arc/presence.js +185 -0
  73. package/dist/arc/task-lifecycle.js +57 -0
  74. package/dist/commerce/store.js +755 -0
  75. package/dist/commerce/types.js +3 -0
  76. package/dist/heart/active-work.js +820 -29
  77. package/dist/heart/agent-entry.js +252 -3
  78. package/dist/heart/approval-files.js +113 -0
  79. package/dist/heart/approval-store.js +950 -0
  80. package/dist/heart/attachments/image-normalize.js +194 -0
  81. package/dist/heart/attachments/materialize.js +97 -0
  82. package/dist/heart/attachments/originals.js +88 -0
  83. package/dist/heart/attachments/render.js +29 -0
  84. package/dist/heart/attachments/sources/bluebubbles.js +156 -0
  85. package/dist/heart/attachments/sources/cli-local-file.js +78 -0
  86. package/dist/heart/attachments/sources/index.js +18 -0
  87. package/dist/heart/attachments/sources/telegram.js +71 -0
  88. package/dist/heart/attachments/store.js +132 -0
  89. package/dist/heart/attachments/types.js +93 -0
  90. package/dist/heart/auth/auth-flow.js +495 -0
  91. package/dist/heart/autonomy-budget.js +478 -0
  92. package/dist/heart/awaiting/await-alert.js +148 -0
  93. package/dist/heart/awaiting/await-expiry.js +143 -0
  94. package/dist/heart/awaiting/await-loader.js +91 -0
  95. package/dist/heart/awaiting/await-parser.js +149 -0
  96. package/dist/heart/awaiting/await-runtime-state.js +100 -0
  97. package/dist/heart/awaiting/await-scheduler.js +382 -0
  98. package/dist/heart/background-operations.js +281 -0
  99. package/dist/heart/bridges/manager.js +137 -17
  100. package/dist/heart/bridges/store.js +14 -2
  101. package/dist/heart/bundle-state.js +168 -0
  102. package/dist/heart/commitments.js +64 -18
  103. package/dist/heart/config-registry.js +331 -0
  104. package/dist/heart/config.js +142 -129
  105. package/dist/heart/context-loss-gauntlet.js +387 -0
  106. package/dist/heart/context-loss-sentinel.js +1157 -0
  107. package/dist/heart/core.js +2105 -574
  108. package/dist/heart/cross-chat-delivery.js +9 -22
  109. package/dist/heart/daemon/agent-config-check.js +451 -0
  110. package/dist/heart/daemon/agent-discovery.js +185 -3
  111. package/dist/heart/daemon/agent-service.js +542 -0
  112. package/dist/heart/daemon/agentic-repair.js +547 -0
  113. package/dist/heart/daemon/await-private-wake.js +59 -0
  114. package/dist/heart/daemon/bluebubbles-cli-integration.js +71 -0
  115. package/dist/heart/daemon/bluebubbles-health-diagnostics.js +122 -0
  116. package/dist/heart/daemon/bluebubbles-host-protocol.js +433 -0
  117. package/dist/heart/daemon/bluebubbles-host.js +273 -0
  118. package/dist/heart/daemon/boot-sync-probe.js +197 -0
  119. package/dist/heart/daemon/cadence.js +180 -0
  120. package/dist/heart/daemon/cli-defaults.js +854 -0
  121. package/dist/heart/daemon/cli-desk.js +322 -0
  122. package/dist/heart/daemon/cli-exec.js +8822 -0
  123. package/dist/heart/daemon/cli-help.js +716 -0
  124. package/dist/heart/daemon/cli-parse.js +2771 -0
  125. package/dist/heart/daemon/cli-render-doctor.js +57 -0
  126. package/dist/heart/daemon/cli-render.js +777 -0
  127. package/dist/heart/daemon/cli-types.js +8 -0
  128. package/dist/heart/daemon/connect-bay.js +323 -0
  129. package/dist/heart/daemon/container-credential-bootstrap.js +273 -0
  130. package/dist/heart/daemon/container-healthcheck.js +112 -0
  131. package/dist/heart/daemon/container-runtime.js +97 -0
  132. package/dist/heart/daemon/container-spec-auditor-main.js +170 -0
  133. package/dist/heart/daemon/container-spec-auditor.js +236 -0
  134. package/dist/heart/daemon/daemon-bootstrap-startup.js +115 -0
  135. package/dist/heart/daemon/daemon-cli.js +29 -2317
  136. package/dist/heart/daemon/daemon-entry.js +921 -11
  137. package/dist/heart/daemon/daemon-health.js +186 -0
  138. package/dist/heart/daemon/daemon-rollup.js +57 -0
  139. package/dist/heart/daemon/daemon-runtime-sync.js +88 -13
  140. package/dist/heart/daemon/daemon-tombstone.js +236 -0
  141. package/dist/heart/daemon/daemon.js +2073 -104
  142. package/dist/heart/daemon/dns-workflow.js +394 -0
  143. package/dist/heart/daemon/doctor-types.js +8 -0
  144. package/dist/heart/daemon/doctor.js +1755 -0
  145. package/dist/heart/daemon/freshness.js +345 -0
  146. package/dist/heart/daemon/habit-private-wake.js +61 -0
  147. package/dist/heart/daemon/health-monitor.js +142 -11
  148. package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
  149. package/dist/heart/daemon/hooks/bundle-meta.js +135 -1
  150. package/dist/heart/daemon/http-health-probe.js +80 -0
  151. package/dist/heart/daemon/human-command-screens.js +234 -0
  152. package/dist/heart/daemon/human-readiness.js +114 -0
  153. package/dist/heart/daemon/inner-status.js +78 -0
  154. package/dist/heart/daemon/interactive-repair.js +394 -0
  155. package/dist/heart/daemon/launchd.js +37 -8
  156. package/dist/heart/daemon/log-tailer.js +79 -10
  157. package/dist/heart/daemon/logs-prune.js +132 -0
  158. package/dist/heart/daemon/mcp-canary.js +524 -0
  159. package/dist/heart/daemon/message-router.js +65 -5
  160. package/dist/heart/daemon/migrate-to-desk.js +848 -0
  161. package/dist/heart/daemon/os-cron-deps.js +135 -0
  162. package/dist/heart/daemon/os-cron.js +22 -14
  163. package/dist/heart/daemon/ouro-bot-entry.js +4 -2
  164. package/dist/heart/daemon/ouro-entry.js +3 -1
  165. package/dist/heart/daemon/plugin-cli.js +432 -0
  166. package/dist/heart/daemon/process-manager.js +733 -61
  167. package/dist/heart/daemon/provider-discovery.js +137 -0
  168. package/dist/heart/daemon/provider-ping-progress.js +83 -0
  169. package/dist/heart/daemon/prunable-bundle.js +144 -0
  170. package/dist/heart/daemon/pulse.js +511 -0
  171. package/dist/heart/daemon/readiness-repair.js +365 -0
  172. package/dist/heart/daemon/run-hooks.js +2 -0
  173. package/dist/heart/daemon/runtime-logging.js +47 -14
  174. package/dist/heart/daemon/runtime-metadata.js +2 -30
  175. package/dist/heart/daemon/runtime-mode.js +13 -2
  176. package/dist/heart/daemon/safe-mode.js +161 -0
  177. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +2609 -0
  178. package/dist/heart/daemon/sanctuary-acceptance-harness.js +1866 -0
  179. package/dist/heart/daemon/sanctuary-acceptance-marker.js +267 -0
  180. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +976 -0
  181. package/dist/heart/daemon/sanctuary-bundle-migration.js +728 -0
  182. package/dist/heart/daemon/sanctuary-package-management.js +103 -0
  183. package/dist/heart/daemon/sanctuary-scheduler-liveness.js +227 -0
  184. package/dist/heart/daemon/sanctuary-scheduler-origin.js +132 -0
  185. package/dist/heart/daemon/sense-manager.js +689 -48
  186. package/dist/heart/daemon/session-id-resolver.js +131 -0
  187. package/dist/heart/daemon/skill-management-installer.js +1 -1
  188. package/dist/heart/daemon/socket-client.js +180 -12
  189. package/dist/heart/daemon/stale-bundle-prune.js +113 -0
  190. package/dist/heart/daemon/startup-tui.js +339 -0
  191. package/dist/heart/daemon/supercronic-supervisor.js +282 -0
  192. package/dist/heart/daemon/task-scheduler.js +117 -39
  193. package/dist/heart/daemon/terminal-ui.js +499 -0
  194. package/dist/heart/daemon/thoughts.js +138 -54
  195. package/dist/heart/daemon/up-progress.js +366 -0
  196. package/dist/heart/daemon/vault-items.js +56 -0
  197. package/dist/heart/delegation.js +1 -4
  198. package/dist/heart/external-events/router.js +1414 -0
  199. package/dist/heart/frontend-approval-runtime.js +432 -0
  200. package/dist/heart/frontend-journal.js +215 -0
  201. package/dist/heart/frontend-session-service.js +237 -0
  202. package/dist/heart/frontend-socket-client.js +154 -0
  203. package/dist/heart/frontend-socket.js +400 -0
  204. package/dist/heart/habits/habit-cancel.js +810 -0
  205. package/dist/heart/habits/habit-lifecycle.js +1327 -0
  206. package/dist/heart/habits/habit-migration.js +194 -0
  207. package/dist/heart/habits/habit-parser.js +278 -0
  208. package/dist/heart/habits/habit-runtime-state.js +93 -0
  209. package/dist/heart/habits/habit-scheduler.js +653 -0
  210. package/dist/heart/habits/habit-session-summary.js +318 -0
  211. package/dist/heart/habits/habit-session.js +636 -0
  212. package/dist/heart/{daemon → hatch}/hatch-flow.js +55 -62
  213. package/dist/heart/{daemon → hatch}/hatch-specialist.js +6 -8
  214. package/dist/heart/{daemon → hatch}/specialist-prompt.js +12 -9
  215. package/dist/heart/{daemon → hatch}/specialist-tools.js +53 -18
  216. package/dist/heart/identity.js +184 -59
  217. package/dist/heart/kept-notes.js +289 -0
  218. package/dist/heart/kicks.js +1 -1
  219. package/dist/heart/machine-identity.js +161 -0
  220. package/dist/heart/mail-import-discovery.js +356 -0
  221. package/dist/heart/mailbox/mailbox-http-hooks.js +98 -0
  222. package/dist/heart/mailbox/mailbox-http-response.js +7 -0
  223. package/dist/heart/mailbox/mailbox-http-routes.js +398 -0
  224. package/dist/heart/mailbox/mailbox-http-static.js +103 -0
  225. package/dist/heart/mailbox/mailbox-http-transport.js +129 -0
  226. package/dist/heart/mailbox/mailbox-http.js +99 -0
  227. package/dist/heart/mailbox/mailbox-read.js +39 -0
  228. package/dist/heart/mailbox/mailbox-types.js +27 -0
  229. package/dist/heart/mailbox/mailbox-view.js +197 -0
  230. package/dist/heart/mailbox/readers/agent-machine.js +428 -0
  231. package/dist/heart/mailbox/readers/continuity-readers.js +329 -0
  232. package/dist/heart/mailbox/readers/mail.js +375 -0
  233. package/dist/heart/mailbox/readers/runtime-readers.js +823 -0
  234. package/dist/heart/mailbox/readers/sessions.js +231 -0
  235. package/dist/heart/mailbox/readers/shared.js +111 -0
  236. package/dist/heart/mcp/mcp-server.js +696 -0
  237. package/dist/heart/migrate-config.js +100 -0
  238. package/dist/heart/model-capabilities.js +11 -0
  239. package/dist/heart/orientation-frame.js +345 -0
  240. package/dist/heart/platform.js +81 -0
  241. package/dist/heart/primitives.js +1 -1
  242. package/dist/heart/private-runtime/decision-renderer.js +158 -0
  243. package/dist/heart/private-runtime/index.js +18 -0
  244. package/dist/heart/private-runtime/ledger.js +221 -0
  245. package/dist/heart/private-runtime/policy.js +252 -0
  246. package/dist/heart/private-runtime/types.js +2 -0
  247. package/dist/heart/progress-story.js +1 -1
  248. package/dist/heart/provider-attempt.js +137 -0
  249. package/dist/heart/provider-binding-resolver.js +273 -0
  250. package/dist/heart/provider-credentials.js +449 -0
  251. package/dist/heart/provider-failover.js +311 -0
  252. package/dist/heart/provider-models.js +96 -0
  253. package/dist/heart/provider-ping.js +265 -0
  254. package/dist/heart/provider-readiness-cache.js +119 -0
  255. package/dist/heart/provider-visibility.js +188 -0
  256. package/dist/heart/providers/anthropic-token.js +131 -0
  257. package/dist/heart/providers/anthropic.js +173 -60
  258. package/dist/heart/providers/azure.js +23 -11
  259. package/dist/heart/providers/error-classification.js +127 -0
  260. package/dist/heart/providers/github-copilot.js +34 -38
  261. package/dist/heart/providers/minimax-vlm.js +189 -0
  262. package/dist/heart/providers/minimax.js +26 -8
  263. package/dist/heart/providers/openai-codex-token.js +349 -0
  264. package/dist/heart/providers/openai-codex.js +55 -40
  265. package/dist/heart/providers/openai-compatible.js +177 -0
  266. package/dist/heart/run-ledger.js +247 -0
  267. package/dist/heart/runtime-capability-check.js +170 -0
  268. package/dist/heart/runtime-credentials.js +607 -0
  269. package/dist/heart/runtime-cwd.js +87 -0
  270. package/dist/heart/sense-truth.js +22 -4
  271. package/dist/heart/session-activity.js +92 -24
  272. package/dist/heart/session-events.js +1433 -0
  273. package/dist/heart/session-playback-cli-main.js +5 -0
  274. package/dist/heart/session-playback-cli.js +36 -0
  275. package/dist/heart/session-playback.js +231 -0
  276. package/dist/heart/session-redaction-repair-cli-main.js +31 -0
  277. package/dist/heart/session-redaction-repair.js +543 -0
  278. package/dist/heart/session-stats-cli-main.js +5 -0
  279. package/dist/heart/session-stats.js +182 -0
  280. package/dist/heart/session-transcript.js +133 -0
  281. package/dist/heart/start-of-turn-packet.js +372 -0
  282. package/dist/heart/steward-policy.js +302 -0
  283. package/dist/heart/streaming.js +604 -207
  284. package/dist/heart/structured-output.js +196 -0
  285. package/dist/heart/sync-classification.js +176 -0
  286. package/dist/heart/sync.js +449 -0
  287. package/dist/heart/target-resolution.js +11 -7
  288. package/dist/heart/tempo.js +93 -0
  289. package/dist/heart/temporal-view.js +41 -0
  290. package/dist/heart/timeouts.js +101 -0
  291. package/dist/heart/tool-activity-callbacks.js +59 -0
  292. package/dist/heart/tool-approval.js +342 -0
  293. package/dist/heart/tool-description.js +155 -0
  294. package/dist/heart/tool-friction.js +55 -0
  295. package/dist/heart/tool-loop.js +200 -0
  296. package/dist/heart/turn-context.js +482 -0
  297. package/dist/heart/turn-execution-lease.js +30 -0
  298. package/dist/heart/{daemon → versioning}/ouro-bot-global-installer.js +6 -5
  299. package/dist/heart/{daemon → versioning}/ouro-bot-wrapper.js +1 -1
  300. package/dist/heart/versioning/ouro-path-installer.js +432 -0
  301. package/dist/heart/versioning/ouro-recovery-launcher.js +76 -0
  302. package/dist/heart/versioning/ouro-version-manager.js +409 -0
  303. package/dist/heart/{daemon → versioning}/staged-restart.js +40 -8
  304. package/dist/heart/{daemon → versioning}/update-checker.js +6 -1
  305. package/dist/heart/versioning/update-hooks.js +154 -0
  306. package/dist/heart/versioning/version-intent.js +114 -0
  307. package/dist/heart/work-card.js +380 -0
  308. package/dist/mailbox-ui/assets/index-Du_9G9WO.css +1 -0
  309. package/dist/mailbox-ui/assets/index-J8zDwHmE.js +1 -0
  310. package/dist/mailbox-ui/assets/vendor-CcN1XpQ9.js +61 -0
  311. package/dist/mailbox-ui/index.html +16 -0
  312. package/dist/mailroom/attention.js +167 -0
  313. package/dist/mailroom/autonomy.js +209 -0
  314. package/dist/mailroom/blob-store.js +889 -0
  315. package/dist/mailroom/body-cache.js +61 -0
  316. package/dist/mailroom/cache-sync-cli.js +58 -0
  317. package/dist/mailroom/core.js +803 -0
  318. package/dist/mailroom/entry.js +160 -0
  319. package/dist/mailroom/file-store.js +568 -0
  320. package/dist/mailroom/hosted-cache-sync.js +384 -0
  321. package/dist/mailroom/mbox-import.js +393 -0
  322. package/dist/mailroom/migration.js +164 -0
  323. package/dist/mailroom/outbound.js +380 -0
  324. package/dist/mailroom/policy.js +263 -0
  325. package/dist/mailroom/reader.js +292 -0
  326. package/dist/mailroom/search-cache.js +513 -0
  327. package/dist/mailroom/search-relevance.js +319 -0
  328. package/dist/mailroom/smtp-ingress.js +176 -0
  329. package/dist/mailroom/source-state.js +176 -0
  330. package/dist/mailroom/thread.js +109 -0
  331. package/dist/mailroom/travel-extract.js +89 -0
  332. package/dist/mind/bundle-manifest.js +24 -2
  333. package/dist/mind/context.js +279 -111
  334. package/dist/mind/desk-section.js +362 -0
  335. package/dist/mind/diary-integrity.js +60 -0
  336. package/dist/mind/{memory.js → diary.js} +68 -77
  337. package/dist/mind/embedding-provider.js +60 -0
  338. package/dist/mind/file-state.js +179 -0
  339. package/dist/mind/{associative-recall.js → note-search.js} +47 -58
  340. package/dist/mind/obligation-steering.js +227 -0
  341. package/dist/mind/pending.js +34 -8
  342. package/dist/mind/prompt-budget.js +479 -0
  343. package/dist/mind/prompt-refresh.js +3 -2
  344. package/dist/mind/prompt.js +1059 -187
  345. package/dist/mind/provenance-trust.js +26 -0
  346. package/dist/mind/record-paths.js +312 -0
  347. package/dist/mind/scrutiny.js +173 -0
  348. package/dist/mind/session-transaction.js +564 -0
  349. package/dist/nerves/cli-logging.js +7 -1
  350. package/dist/nerves/coverage/audit-rules.js +15 -6
  351. package/dist/nerves/coverage/audit.js +28 -2
  352. package/dist/nerves/coverage/cli.js +1 -1
  353. package/dist/nerves/coverage/contract.js +5 -5
  354. package/dist/nerves/coverage/file-completeness.js +139 -5
  355. package/dist/nerves/coverage/run-artifacts.js +55 -35
  356. package/dist/nerves/coverage/source-scanner.js +4 -4
  357. package/dist/nerves/event-buffer.js +111 -0
  358. package/dist/nerves/index.js +293 -9
  359. package/dist/nerves/observation.js +20 -0
  360. package/dist/nerves/redact.js +79 -0
  361. package/dist/nerves/review/cli-main.js +5 -0
  362. package/dist/nerves/review/cli.js +156 -0
  363. package/dist/nerves/review/core.js +159 -0
  364. package/dist/nerves/runtime.js +17 -1
  365. package/dist/repertoire/ado-client.js +15 -56
  366. package/dist/repertoire/ado-semantic.js +16 -10
  367. package/dist/repertoire/api-client.js +97 -0
  368. package/dist/repertoire/bitwarden-store.js +1045 -0
  369. package/dist/repertoire/bundle-templates.js +71 -0
  370. package/dist/repertoire/bw-installer.js +180 -0
  371. package/dist/repertoire/coding/codex-jsonl.js +64 -0
  372. package/dist/repertoire/coding/context-pack.js +331 -0
  373. package/dist/repertoire/coding/feedback.js +218 -30
  374. package/dist/repertoire/coding/index.js +15 -3
  375. package/dist/repertoire/coding/manager.js +166 -10
  376. package/dist/repertoire/coding/spawner.js +58 -11
  377. package/dist/repertoire/coding/tools.js +242 -14
  378. package/dist/repertoire/coding/workbench-client.js +272 -0
  379. package/dist/repertoire/coding/workbench-manager.js +346 -0
  380. package/dist/repertoire/commerce-errors.js +109 -0
  381. package/dist/repertoire/commerce-self-test.js +156 -0
  382. package/dist/repertoire/credential-access.js +178 -0
  383. package/dist/repertoire/desk/classifier.js +362 -0
  384. package/dist/repertoire/duffel-client.js +185 -0
  385. package/dist/repertoire/github-client.js +14 -55
  386. package/dist/repertoire/graph-client.js +11 -52
  387. package/dist/repertoire/guardrails.js +811 -18
  388. package/dist/repertoire/mcp-client.js +44 -3
  389. package/dist/repertoire/mcp-manager.js +331 -27
  390. package/dist/repertoire/mcp-tools.js +83 -0
  391. package/dist/repertoire/plugin-mcp.js +175 -0
  392. package/dist/repertoire/plugins.js +253 -0
  393. package/dist/repertoire/relationship-authorization.js +194 -0
  394. package/dist/repertoire/shell-sessions.js +133 -0
  395. package/dist/repertoire/skills.js +48 -4
  396. package/dist/repertoire/stripe-client.js +131 -0
  397. package/dist/repertoire/tool-arguments.js +148 -0
  398. package/dist/repertoire/tool-results.js +29 -0
  399. package/dist/repertoire/tools-a2a.js +699 -0
  400. package/dist/repertoire/tools-attachments.js +322 -0
  401. package/dist/repertoire/tools-awaiting.js +575 -0
  402. package/dist/repertoire/tools-base.js +82 -1139
  403. package/dist/repertoire/tools-bluebubbles.js +2 -0
  404. package/dist/repertoire/tools-bridge.js +144 -0
  405. package/dist/repertoire/tools-bundle.js +993 -0
  406. package/dist/repertoire/tools-commerce.js +253 -0
  407. package/dist/repertoire/tools-config.js +186 -0
  408. package/dist/repertoire/tools-continuity.js +575 -0
  409. package/dist/repertoire/tools-credential.js +383 -0
  410. package/dist/repertoire/tools-evolution.js +527 -0
  411. package/dist/repertoire/tools-files.js +344 -0
  412. package/dist/repertoire/tools-flight.js +290 -0
  413. package/dist/repertoire/tools-flow.js +119 -0
  414. package/dist/repertoire/tools-github.js +3 -8
  415. package/dist/repertoire/tools-habits.js +103 -0
  416. package/dist/repertoire/tools-mail.js +1828 -0
  417. package/dist/repertoire/tools-notes.js +485 -0
  418. package/dist/repertoire/tools-obligations.js +182 -0
  419. package/dist/repertoire/tools-orientation.js +31 -0
  420. package/dist/repertoire/tools-record.js +482 -0
  421. package/dist/repertoire/tools-rsvp.js +139 -0
  422. package/dist/repertoire/tools-runtime.js +150 -0
  423. package/dist/repertoire/tools-session.js +1108 -0
  424. package/dist/repertoire/tools-shell.js +133 -0
  425. package/dist/repertoire/tools-steward-policy.js +95 -0
  426. package/dist/repertoire/tools-stripe.js +224 -0
  427. package/dist/repertoire/tools-surface.js +369 -0
  428. package/dist/repertoire/tools-teams.js +12 -39
  429. package/dist/repertoire/tools-telegram-contacts.js +37 -0
  430. package/dist/repertoire/tools-travel.js +125 -0
  431. package/dist/repertoire/tools-trip.js +982 -0
  432. package/dist/repertoire/tools-unraid.js +484 -0
  433. package/dist/repertoire/tools-user-profile.js +146 -0
  434. package/dist/repertoire/tools-vault.js +40 -0
  435. package/dist/repertoire/tools-voice.js +145 -0
  436. package/dist/repertoire/tools.js +377 -80
  437. package/dist/repertoire/travel-api-client.js +360 -0
  438. package/dist/repertoire/unraid-client.js +155 -0
  439. package/dist/repertoire/unraid-restart.js +212 -0
  440. package/dist/repertoire/user-profile.js +131 -0
  441. package/dist/repertoire/vault-setup.js +246 -0
  442. package/dist/repertoire/vault-unlock.js +628 -0
  443. package/dist/rsvp/aisleplanner-client.js +354 -0
  444. package/dist/rsvp/cli.js +926 -0
  445. package/dist/rsvp/config.js +327 -0
  446. package/dist/rsvp/cutover.js +433 -0
  447. package/dist/rsvp/diagnostics.js +232 -0
  448. package/dist/rsvp/diff-renderer.js +176 -0
  449. package/dist/rsvp/habit-policy.js +110 -0
  450. package/dist/rsvp/habit-stage.js +123 -0
  451. package/dist/rsvp/incident-bundle.js +97 -0
  452. package/dist/rsvp/migration.js +204 -0
  453. package/dist/rsvp/native-habit-runner.js +630 -0
  454. package/dist/rsvp/outbound-state.js +628 -0
  455. package/dist/rsvp/query.js +118 -0
  456. package/dist/rsvp/replay.js +233 -0
  457. package/dist/rsvp/snapshot.js +232 -0
  458. package/dist/rsvp/spend-ledger.js +175 -0
  459. package/dist/scripts/claude-code-hook.js +41 -0
  460. package/dist/scripts/claude-code-stop-hook.js +47 -0
  461. package/dist/senses/a2a-entry.js +115 -0
  462. package/dist/senses/acp-server.js +386 -0
  463. package/dist/senses/attention-queue.js +186 -0
  464. package/dist/senses/await-turn-message.js +58 -0
  465. package/dist/senses/bluebubbles/active-turns.js +216 -0
  466. package/dist/senses/bluebubbles/attachment-cache.js +53 -0
  467. package/dist/senses/bluebubbles/attachment-download.js +137 -0
  468. package/dist/senses/{bluebubbles-client.js → bluebubbles/client.js} +355 -40
  469. package/dist/senses/bluebubbles/context-packet.js +207 -0
  470. package/dist/senses/bluebubbles/context-smoke.js +144 -0
  471. package/dist/senses/bluebubbles/entry.js +90 -0
  472. package/dist/senses/{bluebubbles-inbound-log.js → bluebubbles/inbound-log.js} +21 -3
  473. package/dist/senses/bluebubbles/index.js +3839 -0
  474. package/dist/senses/bluebubbles/latest-turn.js +311 -0
  475. package/dist/senses/{bluebubbles-media.js → bluebubbles/media.js} +121 -71
  476. package/dist/senses/{bluebubbles-model.js → bluebubbles/model.js} +159 -28
  477. package/dist/senses/{bluebubbles-mutation-log.js → bluebubbles/mutation-log.js} +24 -13
  478. package/dist/senses/bluebubbles/outbound-state.js +308 -0
  479. package/dist/senses/bluebubbles/processed-log.js +133 -0
  480. package/dist/senses/bluebubbles/reaction-policy.js +47 -0
  481. package/dist/senses/bluebubbles/replay.js +284 -0
  482. package/dist/senses/{bluebubbles-runtime-state.js → bluebubbles/runtime-state.js} +30 -2
  483. package/dist/senses/bluebubbles/semantic-receipts.js +1986 -0
  484. package/dist/senses/{bluebubbles-session-cleanup.js → bluebubbles/session-cleanup.js} +1 -1
  485. package/dist/senses/bluebubbles/webhook-registration.js +228 -0
  486. package/dist/senses/bluebubbles-meta-guard.js +39 -0
  487. package/dist/senses/cli/bracketed-paste.js +82 -0
  488. package/dist/senses/cli/image-paste.js +287 -0
  489. package/dist/senses/cli/image-ref-navigation.js +75 -0
  490. package/dist/senses/cli/ink-app.js +156 -0
  491. package/dist/senses/cli/inline-diff.js +64 -0
  492. package/dist/senses/cli/input-keys.js +174 -0
  493. package/dist/senses/cli/kill-ring.js +86 -0
  494. package/dist/senses/cli/message-list.js +51 -0
  495. package/dist/senses/cli/ouro-tui.js +607 -0
  496. package/dist/senses/cli/spinner-imperative.js +135 -0
  497. package/dist/senses/cli/spinner.js +101 -0
  498. package/dist/senses/cli/status-line.js +60 -0
  499. package/dist/senses/cli/streaming-markdown.js +526 -0
  500. package/dist/senses/cli/tool-display.js +85 -0
  501. package/dist/senses/cli/tool-render.js +85 -0
  502. package/dist/senses/cli/tui-store.js +241 -0
  503. package/dist/senses/cli/virtual-list.js +35 -0
  504. package/dist/senses/cli-entry.js +60 -8
  505. package/dist/senses/cli.js +600 -317
  506. package/dist/senses/commands.js +66 -3
  507. package/dist/senses/context-packet-ledger.js +371 -0
  508. package/dist/senses/context-packets.js +154 -0
  509. package/dist/senses/habit-lifecycle-guard.js +24 -0
  510. package/dist/senses/habit-turn-message.js +160 -0
  511. package/dist/senses/ingress-evidence.js +2 -0
  512. package/dist/senses/mail-entry.js +66 -0
  513. package/dist/senses/mail.js +524 -0
  514. package/dist/senses/pipeline.js +1115 -204
  515. package/dist/senses/private-runtime-worker.js +859 -0
  516. package/dist/senses/private-runtime.js +1496 -0
  517. package/dist/senses/proactive-content-guard.js +51 -0
  518. package/dist/senses/sanctuary-download-credit-presentation.js +31 -0
  519. package/dist/senses/sanctuary-full-visibility-contract.js +302 -0
  520. package/dist/senses/sanctuary-grounding.js +118 -0
  521. package/dist/senses/sanctuary-health-acceptance-probe-entry.js +12 -0
  522. package/dist/senses/sanctuary-health-acceptance-probe.js +774 -0
  523. package/dist/senses/sanctuary-health-runner.js +101 -0
  524. package/dist/senses/sanctuary-health.js +408 -0
  525. package/dist/senses/sanctuary-install-state-contract.js +126 -0
  526. package/dist/senses/sanctuary-interactive-control.js +306 -0
  527. package/dist/senses/sanctuary-media-catalog-contract.js +235 -0
  528. package/dist/senses/sanctuary-media-optimization.js +514 -0
  529. package/dist/senses/sanctuary-runtime.js +338 -0
  530. package/dist/senses/sanctuary-sab.js +85 -0
  531. package/dist/senses/sanctuary-storage-optimization-contract.js +32 -0
  532. package/dist/senses/shared-turn.js +756 -0
  533. package/dist/senses/surface-tool.js +109 -0
  534. package/dist/senses/teams-entry.js +60 -8
  535. package/dist/senses/teams.js +515 -217
  536. package/dist/senses/telegram-admission.js +711 -0
  537. package/dist/senses/telegram-approval-runtime.js +566 -0
  538. package/dist/senses/telegram-attachments.js +182 -0
  539. package/dist/senses/telegram-audit-ledger.js +220 -0
  540. package/dist/senses/telegram-client.js +1677 -0
  541. package/dist/senses/telegram-effect-adapter.js +859 -0
  542. package/dist/senses/telegram-entry.js +83 -0
  543. package/dist/senses/telegram.js +2045 -0
  544. package/dist/senses/trust-gate.js +104 -9
  545. package/dist/senses/voice/audio-playback.js +237 -0
  546. package/dist/senses/voice/audio-routing.js +119 -0
  547. package/dist/senses/voice/elevenlabs.js +202 -0
  548. package/dist/senses/voice/floor-control.js +431 -0
  549. package/dist/senses/voice/floor-controller.js +115 -0
  550. package/dist/senses/voice/golden-path.js +116 -0
  551. package/dist/senses/voice/index.js +29 -0
  552. package/dist/senses/voice/meeting.js +113 -0
  553. package/dist/senses/voice/outbound.js +190 -0
  554. package/dist/senses/voice/phone.js +33 -0
  555. package/dist/senses/voice/playback.js +139 -0
  556. package/dist/senses/voice/realtime-eval.js +496 -0
  557. package/dist/senses/voice/realtime-trace.js +531 -0
  558. package/dist/senses/voice/transcript.js +70 -0
  559. package/dist/senses/voice/turn.js +191 -0
  560. package/dist/senses/voice/twilio-phone-runtime.js +807 -0
  561. package/dist/senses/voice/twilio-phone.js +5081 -0
  562. package/dist/senses/voice/types.js +2 -0
  563. package/dist/senses/voice/whisper.js +161 -0
  564. package/dist/senses/voice-entry.js +81 -0
  565. package/dist/senses/voice-realtime-eval-command.js +99 -0
  566. package/dist/senses/voice-realtime-eval-entry.js +21 -0
  567. package/dist/senses/voice-twilio-entry.js +87 -0
  568. package/dist/trips/core.js +138 -0
  569. package/dist/trips/store.js +265 -0
  570. package/dist/util/frontmatter.js +69 -0
  571. package/npm-shrinkwrap.json +8138 -0
  572. package/package.json +72 -13
  573. package/skills/agent-commerce.md +113 -0
  574. package/skills/browser-navigation.md +117 -0
  575. package/skills/commerce-setup-guide.md +116 -0
  576. package/skills/commerce-setup.md +84 -0
  577. package/skills/configure-dev-tools.md +99 -0
  578. package/skills/travel-planning.md +138 -0
  579. package/dist/heart/daemon/auth-flow.js +0 -430
  580. package/dist/heart/daemon/ouro-path-installer.js +0 -260
  581. package/dist/heart/daemon/ouro-version-manager.js +0 -164
  582. package/dist/heart/daemon/update-hooks.js +0 -138
  583. package/dist/heart/obligations.js +0 -141
  584. package/dist/heart/safe-workspace.js +0 -228
  585. package/dist/heart/session-recall.js +0 -116
  586. package/dist/mind/friends/channel.js +0 -84
  587. package/dist/mind/friends/group-context.js +0 -144
  588. package/dist/mind/friends/resolver.js +0 -84
  589. package/dist/mind/friends/store-file.js +0 -190
  590. package/dist/mind/friends/store.js +0 -4
  591. package/dist/mind/friends/tokens.js +0 -26
  592. package/dist/mind/friends/trust-explanation.js +0 -74
  593. package/dist/mind/friends/types.js +0 -29
  594. package/dist/repertoire/tasks/board.js +0 -134
  595. package/dist/repertoire/tasks/index.js +0 -224
  596. package/dist/repertoire/tasks/lifecycle.js +0 -80
  597. package/dist/repertoire/tasks/middleware.js +0 -65
  598. package/dist/repertoire/tasks/parser.js +0 -173
  599. package/dist/repertoire/tasks/scanner.js +0 -132
  600. package/dist/repertoire/tasks/transitions.js +0 -144
  601. package/dist/senses/bluebubbles-entry.js +0 -13
  602. package/dist/senses/bluebubbles.js +0 -1181
  603. package/dist/senses/debug-activity.js +0 -154
  604. package/dist/senses/inner-dialog-worker.js +0 -90
  605. package/dist/senses/inner-dialog.js +0 -525
  606. package/subagents/README.md +0 -7
  607. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/basilisk.md +0 -0
  608. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jafar.md +0 -0
  609. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jormungandr.md +0 -0
  610. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/kaa.md +0 -0
  611. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/medusa.md +0 -0
  612. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +0 -0
  613. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/nagini.md +0 -0
  614. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/ouroboros.md +0 -0
  615. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/python.md +0 -0
  616. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/quetzalcoatl.md +0 -0
  617. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/sir-hiss.md +0 -0
  618. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
  619. /package/dist/{repertoire/tasks/types.js → heart/attachments/sources/adapter.js} +0 -0
  620. /package/dist/heart/{daemon → hatch}/hatch-animation.js +0 -0
  621. /package/dist/heart/{daemon → hatch}/specialist-orchestrator.js +0 -0
  622. /package/dist/heart/{daemon → versioning}/ouro-uti.js +0 -0
  623. /package/dist/heart/{daemon → versioning}/wrapper-publish-guard.js +0 -0
@@ -0,0 +1,709 @@
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
+ "/home/ouro/AgentBundles/sanctuary.ouro/state/acceptance/telegram-control.sock",
20
+ ]
21
+ const DOCUMENTED_LOOPBACK_TCP_CONTROLS = new Set([6876])
22
+ const TERMINAL_CONTAINMENT_MAX_SAMPLES = 8
23
+ const TARGET_THAW_MAX_ATTEMPTS = 3
24
+ const WATCHDOG_POLL_MS = 100
25
+ const WATCHDOG_PARENT_DEATH_ENFORCEMENT_MS = 25_000
26
+ const RO_PERMISSIONS = ["ARRAY", "DASHBOARD", "DISK", "DOCKER", "INFO", "LOGS", "NOTIFICATIONS", "SHARE", "VARS"]
27
+ .map((resource) => `${resource}:READ_ANY`).sort()
28
+
29
+ function object(value, label) {
30
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`)
31
+ return value
32
+ }
33
+
34
+ function targetProfile(name) {
35
+ const profile = PROFILES[name]
36
+ if (!profile) throw new Error("deployment target profile is invalid")
37
+ return profile
38
+ }
39
+
40
+ function exactSet(left, right) {
41
+ return left.size === right.size && [...left].every((entry) => right.has(entry))
42
+ }
43
+
44
+ function readCanonicalReadDescriptor() {
45
+ const root = statSync(KEY_ROOT)
46
+ if (!root.isDirectory() || root.uid !== 0 || (root.mode & 0o077) !== 0) throw new Error("Unraid key directory metadata is invalid")
47
+ const matches = []
48
+ for (const entry of readdirSync(KEY_ROOT, { withFileTypes: true })) {
49
+ if (!entry.isFile() || !entry.name.endsWith(".json")) throw new Error("unexpected Unraid key directory entry")
50
+ const fd = openSync(`${KEY_ROOT}/${entry.name}`, constants.O_RDONLY | constants.O_NOFOLLOW)
51
+ let record
52
+ try {
53
+ const metadata = fstatSync(fd)
54
+ if (!metadata.isFile() || metadata.uid !== 0 || (metadata.mode & 0o777) !== 0o600) throw new Error("Unraid key file metadata is invalid")
55
+ record = object(JSON.parse(readFileSync(fd, "utf8")), "Unraid key record")
56
+ } finally { closeSync(fd) }
57
+ const permissions = Array.isArray(record.permissions)
58
+ ? record.permissions.flatMap((raw) => {
59
+ const permission = object(raw, "Unraid key permission")
60
+ return Array.isArray(permission.actions) ? permission.actions.map((action) => `${permission.resource}:${action}`) : []
61
+ }).sort()
62
+ : []
63
+ 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)
64
+ }
65
+ if (matches.length !== 1) throw new Error("canonical read-only Unraid key is absent or ambiguous")
66
+ return matches[0]
67
+ }
68
+
69
+ async function queryGraphqlAutostart(fetchImpl = fetch, readDescriptor = readCanonicalReadDescriptor) {
70
+ const response = await fetchImpl(GRAPHQL_ENDPOINT, {
71
+ method: "POST",
72
+ headers: { "content-type": "application/json", "x-api-key": readDescriptor() },
73
+ body: JSON.stringify({ query: "query AcceptanceContainerTopology { vars { id } docker { containers(skipCache: true) { id names autoStart } } }", variables: {} }),
74
+ signal: AbortSignal.timeout(10_000),
75
+ })
76
+ if (!response.ok) throw new Error("Unraid container topology query failed")
77
+ const envelope = object(await response.json(), "Unraid container topology response")
78
+ if (envelope.errors || !envelope.data) throw new Error("Unraid container topology query was rejected")
79
+ const data = object(envelope.data, "Unraid topology data")
80
+ const serverIdentity = /^([0-9a-f]{64}):vars$/u.exec(object(data.vars, "Unraid topology vars").id)
81
+ if (!serverIdentity) throw new Error("Unraid server identity is invalid")
82
+ const containers = object(data.docker, "Unraid topology docker").containers
83
+ if (!Array.isArray(containers)) throw new Error("Unraid container topology is invalid")
84
+ const result = new Map()
85
+ for (const raw of containers) {
86
+ const container = object(raw, "Unraid topology container")
87
+ if (!Array.isArray(container.names) || container.names.some((name) => typeof name !== "string")) throw new Error("Unraid container topology identity is invalid")
88
+ const canonical = container.names.map((name) => name.replace(/^\//u, "")).filter((name) => CANONICAL_NAMES.includes(name))
89
+ const identity = typeof container.id === "string" ? /^([0-9a-f]{64}):([0-9a-f]{64})$/u.exec(container.id) : null
90
+ 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")
91
+ if (canonical.length === 1 && typeof container.autoStart !== "boolean") throw new Error("Unraid container autostart state is invalid")
92
+ if (canonical.length === 1) {
93
+ if (result.has(canonical[0])) throw new Error("Unraid autostart topology is ambiguous")
94
+ result.set(canonical[0], { containerId: identity[2], autoStart: container.autoStart })
95
+ }
96
+ }
97
+ return result
98
+ }
99
+
100
+ function normalizeRecord(raw, label) {
101
+ const value = object(raw, label)
102
+ 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`)
103
+ const name = value.names[0].replace(/^\//u, "")
104
+ if (!CANONICAL_NAMES.includes(name)) throw new Error(`${label} name is not canonical`)
105
+ 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`)
106
+ if (!Number.isSafeInteger(value.pid) || value.pid < 0 || value.running !== (value.pid > 0)) throw new Error(`${label} PID state is invalid`)
107
+ return { id: value.id, name, imageId: value.imageId, running: value.running, autoStart: value.autoStart, restartPolicy: value.restartPolicy, networkMode: value.networkMode, pid: value.pid }
108
+ }
109
+
110
+ function records(value, label) {
111
+ if (!Array.isArray(value)) throw new Error(`${label} must be an array`)
112
+ const normalized = value.map((entry, index) => normalizeRecord(entry, `${label}[${index}]`))
113
+ 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`)
114
+ return normalized.sort((left, right) => left.name.localeCompare(right.name))
115
+ }
116
+
117
+ function signature(value) {
118
+ return JSON.stringify(value.map(({ id, name, imageId, running, autoStart, restartPolicy, networkMode, pid }) => ({ id, name, imageId, running, autoStart, restartPolicy, networkMode, pid })))
119
+ }
120
+
121
+ function attestDeploymentTarget(input) {
122
+ const value = object(input, "deployment target input")
123
+ const profile = targetProfile(value.profile)
124
+ if (!IMAGE_ID.test(value.expectedImageId)) throw new Error("expected image ID is invalid")
125
+ const before = records(value.topologyBefore, "topology before")
126
+ const inspected = records(value.inspected, "canonical inspections")
127
+ const after = records(value.topologyAfter, "topology after")
128
+ if (signature(before) !== signature(after) || signature(before) !== signature(inspected)) throw new Error("canonical deployment topology changed or was incompletely inspected")
129
+ const byName = new Map(inspected.map((entry) => [entry.name, entry]))
130
+ const target = byName.get(profile.containerName)
131
+ if (!target || target.imageId !== value.expectedImageId || !target.running || !target.autoStart) throw new Error("deployment target identity, image, state, or autostart is invalid")
132
+ for (const name of profile.requiredStopped) {
133
+ const stopped = byName.get(name)
134
+ if (!stopped || stopped.running || stopped.autoStart) throw new Error("required rollback target state is invalid")
135
+ }
136
+ for (const name of profile.optionalStopped) {
137
+ const stopped = byName.get(name)
138
+ if (stopped && (stopped.running || stopped.autoStart)) throw new Error("optional rollback target state is invalid")
139
+ }
140
+ for (const name of profile.forbidden) if (byName.has(name)) throw new Error("mixed deployment target state is invalid")
141
+ const activeRunningCardinality = inspected.filter(({ running }) => running).length
142
+ if (activeRunningCardinality !== 1) throw new Error("active Butler running cardinality must equal one")
143
+ return { schemaVersion: "sanctuary-deployment-target-v1", profile: profile.name, targetContainerName: profile.containerName, targetContainerId: target.id, targetImageId: target.imageId, targetPid: target.pid, activeRunningCardinality }
144
+ }
145
+
146
+ function attestOwnedListeners(input) {
147
+ const value = object(input, "listener ownership input")
148
+ 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")
149
+ const processIdsBefore = value.processIdsBefore
150
+ const processIdsAfter = value.processIdsAfter
151
+ 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
152
+ 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")
153
+ const socketSet = (raw, label) => {
154
+ if (!Array.isArray(raw) || raw.some((inode) => !/^[0-9]+$/u.test(inode))) throw new Error(`${label} is invalid`)
155
+ return new Set(raw)
156
+ }
157
+ const inventory = (suffix) => {
158
+ const owned = socketSet(value[`socketInodes${suffix}`], `socket ownership ${suffix.toLowerCase()}`)
159
+ const select = (kind, fallback) => value[kind] ?? value[`${fallback}${suffix}`]
160
+ const tcpRaw = select("tcpListeners", "tcpListeners")
161
+ const udpRaw = select("udpListeners", "udpListeners")
162
+ const unixRaw = select("unixSockets", "unixSockets")
163
+ if (!Array.isArray(tcpRaw) || !Array.isArray(udpRaw) || !Array.isArray(unixRaw)) throw new Error("listener inventory is invalid")
164
+ const tcp = tcpRaw.filter((raw) => owned.has(object(raw, "TCP listener").inode))
165
+ const udp = udpRaw.filter((raw) => owned.has(object(raw, "UDP listener").inode))
166
+ const unix = unixRaw.filter((raw) => owned.has(object(raw, "Unix socket").inode))
167
+ const sortRecords = (records) => [...records].sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)))
168
+ const signature = JSON.stringify({ tcp: sortRecords(tcp), udp: sortRecords(udp), unix: sortRecords(unix) })
169
+ return { owned, tcp, udp, unix, signature }
170
+ }
171
+ const before = inventory("Before")
172
+ const after = inventory("After")
173
+ const terminalFields = ["socketInodesTerminal", "tcpListenersTerminal", "udpListenersTerminal", "unixSocketsTerminal"]
174
+ const providedTerminalFields = terminalFields.filter((field) => value[field] !== undefined)
175
+ if (providedTerminalFields.length !== 0 && providedTerminalFields.length !== terminalFields.length) throw new Error("terminal listener inventory is incomplete")
176
+ const terminal = providedTerminalFields.length === terminalFields.length ? inventory("Terminal") : after
177
+ if (!exactSet(before.owned, after.owned) || !exactSet(after.owned, terminal.owned)) throw new Error("target socket ownership changed")
178
+ if (before.signature !== after.signature || after.signature !== terminal.signature) throw new Error("target listener inventory changed")
179
+ const { owned, tcp, udp, unix } = terminal
180
+ for (const listener of tcp) {
181
+ if (listener.localAddress !== "127.0.0.1" || !DOCUMENTED_LOOPBACK_TCP_CONTROLS.has(listener.port)) throw new Error("target runtime owns an inbound TCP listener")
182
+ }
183
+ if (udp.length !== 0) throw new Error("target runtime owns an inbound UDP listener")
184
+ let unixControlSocketCount = 0
185
+ for (const socket of unix) {
186
+ if (socket.path === "" && socket.flags !== "00010000") continue
187
+ 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")
188
+ unixControlSocketCount += 1
189
+ }
190
+ 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 }
191
+ }
192
+
193
+ function runDocker(args, timeoutMs = 20_000) {
194
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 20_000) throw new Error("Docker command timeout is invalid")
195
+ 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"] })
196
+ if (result.error || result.status !== 0) throw new Error("bounded deployment target inspection failed")
197
+ return result.stdout
198
+ }
199
+
200
+ function pausedTargetState(target, run, timeoutMs = 20_000) {
201
+ 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}}}'
202
+ const state = object(JSON.parse(run(["inspect", "--format", template, target.targetContainerId], timeoutMs)), "target pause state")
203
+ if (!CONTAINER_ID.test(state.containerId) || typeof state.running !== "boolean" || typeof state.paused !== "boolean"
204
+ || typeof state.restarting !== "boolean" || typeof state.dead !== "boolean" || !Number.isSafeInteger(state.pid) || state.pid < 0) {
205
+ throw new Error("target pause state is invalid")
206
+ }
207
+ if (state.containerId !== target.targetContainerId || state.pid !== target.targetPid) throw new Error("target pause identity changed")
208
+ return state
209
+ }
210
+
211
+ function restorePausedTarget(target, run) {
212
+ let lastError
213
+ for (let attempt = 0; attempt < TARGET_THAW_MAX_ATTEMPTS; attempt += 1) {
214
+ try { run(["unpause", target.targetContainerId]) } catch (error) { lastError = error }
215
+ try {
216
+ const resumed = pausedTargetState(target, run)
217
+ if (resumed.running && !resumed.paused && !resumed.restarting && !resumed.dead) return
218
+ lastError = new Error("target resumed state is invalid")
219
+ } catch (error) {
220
+ lastError = error
221
+ }
222
+ }
223
+ throw new Error("target did not resume after bounded thaw attempts", { cause: lastError })
224
+ }
225
+
226
+ function syncWait(milliseconds) {
227
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds)
228
+ }
229
+
230
+ function watchdogReceipt(root, expected, dependencies = {}) {
231
+ const fileExists = dependencies.existsSync ?? existsSync
232
+ const readFile = dependencies.readFileSync ?? readFileSync
233
+ const wait = dependencies.syncWait ?? syncWait
234
+ const file = `${root}/watchdog-terminal.json`
235
+ for (let attempt = 0; attempt < 100 && !fileExists(file); attempt += 1) wait(50)
236
+ const receipt = object(JSON.parse(readFile(file, "utf8")), "thaw watchdog terminal receipt")
237
+ if (receipt.status !== expected.status || receipt.containerId !== expected.containerId || receipt.pid !== expected.pid
238
+ || receipt.parentBootId !== expected.parentBootId || receipt.parentStarttime !== expected.parentStarttime
239
+ || expected.watchdogPid !== undefined && (receipt.watchdogPid !== expected.watchdogPid || receipt.watchdogBootId !== expected.watchdogBootId
240
+ || receipt.watchdogStarttime !== expected.watchdogStarttime)) throw new Error("thaw watchdog terminal receipt is invalid")
241
+ return receipt
242
+ }
243
+
244
+ function parseProcStatIdentity(content, expectedPid) {
245
+ if (typeof content !== "string" || content.includes("\0") || !Number.isSafeInteger(expectedPid) || expectedPid <= 0) throw new Error("parent process identity is invalid")
246
+ const prefix = `${expectedPid} (`
247
+ const close = content.lastIndexOf(") ")
248
+ if (!content.startsWith(prefix) || close < prefix.length || close + 4 > content.length) throw new Error("parent process identity is invalid")
249
+ const state = content[close + 2]
250
+ if (!/^[A-Za-z]$/u.test(state) || content[close + 3] !== " ") throw new Error("parent process identity is invalid")
251
+ const fields = content.slice(close + 4).trim().split(/\s+/u)
252
+ const starttime = fields[18]
253
+ if (!starttime || !/^[0-9]+$/u.test(starttime) || starttime === "0") throw new Error("parent process identity is invalid")
254
+ return { state, starttime }
255
+ }
256
+
257
+ function terminalProcState(state) {
258
+ return state === "Z" || state === "X" || state === "x"
259
+ }
260
+
261
+ function monotonicMilliseconds() {
262
+ return Number(process.hrtime.bigint() / 1_000_000n)
263
+ }
264
+
265
+ function readParentIdentity(pid) {
266
+ const bootId = readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim()
267
+ 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")
268
+ return { bootId, ...parseProcStatIdentity(readFileSync(`/proc/${pid}/stat`, "utf8"), pid) }
269
+ }
270
+
271
+ function armThawWatchdog(target, dependencies = {}) {
272
+ const now = dependencies.now ?? Date.now
273
+ const readIdentity = dependencies.readParentIdentity ?? readParentIdentity
274
+ const makeDirectory = dependencies.mkdirSync ?? mkdirSync
275
+ const spawnChild = dependencies.spawn ?? spawn
276
+ const fileExists = dependencies.existsSync ?? existsSync
277
+ const wait = dependencies.syncWait ?? syncWait
278
+ const writeFile = dependencies.writeFileSync ?? writeFileSync
279
+ const readFile = dependencies.readFileSync ?? readFileSync
280
+ const parentIdentity = readIdentity(process.pid)
281
+ if (terminalProcState(parentIdentity.state)) throw new Error("thaw watchdog parent is not alive")
282
+ const root = `/run/ouro-thaw-watchdog.${process.pid}.${now()}`
283
+ makeDirectory(root, { mode: 0o700 })
284
+ const child = spawnChild(process.execPath, [fileURLToPath(import.meta.url), "--thaw-watchdog", target.targetContainerId, String(target.targetPid), String(process.pid), parentIdentity.bootId, parentIdentity.starttime, root], {
285
+ cwd: "/", detached: true, stdio: "ignore",
286
+ })
287
+ child.unref()
288
+ for (let attempt = 0; attempt < 100 && !fileExists(`${root}/ready`); attempt += 1) wait(50)
289
+ if (!fileExists(`${root}/ready`)) throw new Error("thaw watchdog did not arm")
290
+ if (!Number.isSafeInteger(child.pid) || child.pid <= 0) {
291
+ if (!dependencies.spawn) throw new Error("thaw watchdog process identity is invalid")
292
+ return { assertLive() {}, disarm() { writeFile(`${root}/disarm`, "\n", { flag: "wx", mode: 0o600 }); return { status: "test-disarmed" } } }
293
+ }
294
+ const ready = object(JSON.parse(readFile(`${root}/ready`, "utf8")), "thaw watchdog ready receipt")
295
+ const watchdogIdentity = { pid: child.pid, bootId: ready.watchdogBootId, starttime: ready.watchdogStarttime }
296
+ if (ready.watchdogPid !== watchdogIdentity.pid || ready.readyAt === undefined || !Number.isSafeInteger(ready.readyAt)
297
+ || watchdogIdentity.bootId !== parentIdentity.bootId || !/^[0-9]+$/u.test(watchdogIdentity.starttime) || watchdogIdentity.starttime === "0") {
298
+ throw new Error("thaw watchdog ready identity is invalid")
299
+ }
300
+ const assertLive = () => {
301
+ let observed
302
+ try { observed = readIdentity(watchdogIdentity.pid) } catch (error) { throw new Error("thaw watchdog child exited", { cause: error }) }
303
+ if (observed.bootId !== watchdogIdentity.bootId || observed.starttime !== watchdogIdentity.starttime || terminalProcState(observed.state)) throw new Error("thaw watchdog child identity is not alive")
304
+ }
305
+ assertLive()
306
+ return {
307
+ assertLive,
308
+ disarm() {
309
+ assertLive()
310
+ writeFile(`${root}/disarm`, "\n", { flag: "wx", mode: 0o600 })
311
+ 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)
312
+ },
313
+ }
314
+ }
315
+
316
+ function runKillableCommand(executable, args, timeoutMs, dependencies = {}) {
317
+ if (typeof executable !== "string" || !executable.startsWith("/") || !Array.isArray(args) || args.some((arg) => typeof arg !== "string")
318
+ || !Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 20_000) return Promise.reject(new Error("killable command deadline is invalid"))
319
+ const spawnChild = dependencies.spawn ?? spawn
320
+ const killGroup = dependencies.killProcessGroup ?? ((pid, signal) => {
321
+ try { process.kill(-pid, signal) } catch {
322
+ try { process.kill(pid, signal) } catch {}
323
+ }
324
+ })
325
+ return new Promise((resolve, reject) => {
326
+ let settled = false
327
+ let stdout = ""
328
+ const child = spawnChild(executable, args, {
329
+ cwd: "/", detached: true, stdio: ["ignore", "pipe", "ignore"],
330
+ env: { PATH: "/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin" },
331
+ })
332
+ const finish = (error, value) => {
333
+ if (settled) return
334
+ settled = true
335
+ clearTimeout(termTimer)
336
+ clearTimeout(timer)
337
+ if (error) reject(error)
338
+ else resolve(value)
339
+ }
340
+ child.stdout?.on("data", (chunk) => {
341
+ stdout += String(chunk)
342
+ if (Buffer.byteLength(stdout, "utf8") > 1024 * 1024) {
343
+ killGroup(child.pid, "SIGKILL")
344
+ finish(new Error("killable command output exceeded its bound"))
345
+ }
346
+ })
347
+ child.once("error", () => finish(new Error("killable command failed to launch")))
348
+ child.once("close", (status, signal) => {
349
+ if (status === 0 && signal === null) finish(undefined, stdout)
350
+ else finish(new Error("killable command failed"))
351
+ })
352
+ const graceMs = Math.min(100, Math.max(1, Math.floor(timeoutMs / 4)))
353
+ const termTimer = setTimeout(() => { killGroup(child.pid, "SIGTERM") }, timeoutMs - graceMs)
354
+ const timer = setTimeout(() => {
355
+ killGroup(child.pid, "SIGKILL")
356
+ finish(new Error("killable command hard deadline timed out"))
357
+ }, timeoutMs)
358
+ })
359
+ }
360
+
361
+ async function runDockerWatchdog(args, timeoutMs) {
362
+ return await runKillableCommand(DOCKER, args, timeoutMs)
363
+ }
364
+
365
+ async function pausedTargetStateAsync(target, run, timeoutMs) {
366
+ 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}}}'
367
+ const state = object(JSON.parse(await run(["inspect", "--format", template, target.targetContainerId], timeoutMs)), "target pause state")
368
+ if (!CONTAINER_ID.test(state.containerId) || typeof state.running !== "boolean" || typeof state.paused !== "boolean"
369
+ || typeof state.restarting !== "boolean" || typeof state.dead !== "boolean" || !Number.isSafeInteger(state.pid) || state.pid < 0) throw new Error("target pause state is invalid")
370
+ if (state.containerId !== target.targetContainerId || state.pid !== target.targetPid) throw new Error("target pause identity changed")
371
+ return state
372
+ }
373
+
374
+ async function runThawWatchdog(targetContainerId, targetPid, parentPid, parentBootId, parentStarttime, root, dependencies = {}) {
375
+ const target = { targetContainerId, targetPid }
376
+ const wallNow = dependencies.now ?? Date.now
377
+ const monotonicNow = dependencies.monotonicNow ?? monotonicMilliseconds
378
+ const sleep = dependencies.sleep ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)))
379
+ const fileExists = dependencies.existsSync ?? existsSync
380
+ const writeFile = dependencies.writeFileSync ?? writeFileSync
381
+ const readIdentity = dependencies.readParentIdentity ?? readParentIdentity
382
+ const run = dependencies.runDocker ?? runDockerWatchdog
383
+ const enforcementMs = dependencies.enforcementMs ?? WATCHDOG_PARENT_DEATH_ENFORCEMENT_MS
384
+ const recoveryPollMs = dependencies.recoveryPollMs ?? 250
385
+ const rootIdentity = /^\/run\/ouro-thaw-watchdog\.([0-9]+)\.([0-9]+)$/u.exec(root)
386
+ if (!CONTAINER_ID.test(targetContainerId) || !Number.isSafeInteger(targetPid) || targetPid <= 0
387
+ || !Number.isSafeInteger(parentPid) || parentPid <= 0
388
+ || !/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/u.test(parentBootId)
389
+ || !/^[0-9]+$/u.test(parentStarttime) || parentStarttime === "0"
390
+ || !rootIdentity || Number(rootIdentity[1]) !== parentPid
391
+ || !Number.isSafeInteger(enforcementMs) || enforcementMs <= 0 || !Number.isSafeInteger(recoveryPollMs) || recoveryPollMs <= 0) throw new Error("thaw watchdog lease is invalid")
392
+ const initial = await pausedTargetStateAsync(target, run, 20_000)
393
+ if (!initial.running || initial.paused || initial.restarting || initial.dead) throw new Error("thaw watchdog initial state is invalid")
394
+ const selfIdentity = dependencies.readSelfIdentity?.() ?? (dependencies.readParentIdentity
395
+ ? { bootId: parentBootId, state: "S", starttime: "1" }
396
+ : readParentIdentity(process.pid))
397
+ 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")
398
+ const readyAt = wallNow()
399
+ writeFile(`${root}/ready`, `${JSON.stringify({ watchdogPid: process.pid, watchdogBootId: selfIdentity.bootId, watchdogStarttime: selfIdentity.starttime, readyAt })}\n`, { flag: "wx", mode: 0o600 })
400
+ 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 })
401
+ let transientIdentityFailures = 0
402
+ for (;;) {
403
+ if (fileExists(`${root}/disarm`)) {
404
+ const restored = await pausedTargetStateAsync(target, run, 20_000)
405
+ if (!restored.running || restored.paused || restored.restarting || restored.dead) throw new Error("thaw watchdog disarm state is invalid")
406
+ terminalReceipt("disarmed")
407
+ return
408
+ }
409
+ let parentAlive = false
410
+ try {
411
+ const observed = readIdentity(parentPid)
412
+ parentAlive = observed.bootId === parentBootId && observed.starttime === parentStarttime && !terminalProcState(observed.state)
413
+ transientIdentityFailures = 0
414
+ } catch (error) {
415
+ const code = error && typeof error === "object" && "code" in error ? error.code : undefined
416
+ const confirmedMissing = code === "ENOENT" || code === "ESRCH" || error instanceof Error && /^(?:ENOENT|ESRCH)$/u.test(error.message)
417
+ if (confirmedMissing) break
418
+ transientIdentityFailures += 1
419
+ if (transientIdentityFailures >= 3) throw new Error("thaw watchdog parent identity remained unreadable", { cause: error })
420
+ await sleep(WATCHDOG_POLL_MS)
421
+ continue
422
+ }
423
+ if (!parentAlive) break
424
+ await sleep(WATCHDOG_POLL_MS)
425
+ }
426
+ const deadline = monotonicNow() + enforcementMs
427
+ const remaining = () => {
428
+ const milliseconds = deadline - monotonicNow()
429
+ if (!Number.isSafeInteger(milliseconds) || milliseconds <= 0) throw new Error("thaw watchdog recovery deadline expired")
430
+ return Math.min(20_000, milliseconds)
431
+ }
432
+ for (;;) {
433
+ const state = await pausedTargetStateAsync(target, run, remaining())
434
+ remaining()
435
+ if (!state.running || state.restarting || state.dead) throw new Error("thaw watchdog target state drifted")
436
+ if (state.paused) {
437
+ await run(["unpause", targetContainerId], remaining())
438
+ remaining()
439
+ }
440
+ else if (deadline - monotonicNow() <= recoveryPollMs) {
441
+ remaining()
442
+ terminalReceipt("parent-death-recovered")
443
+ return
444
+ }
445
+ const sleepMs = Math.min(recoveryPollMs, deadline - monotonicNow())
446
+ if (sleepMs <= 0) throw new Error("thaw watchdog recovery deadline expired")
447
+ await sleep(sleepMs)
448
+ }
449
+ }
450
+
451
+ function withPausedTarget(target, operation, dependencies = {}) {
452
+ const run = dependencies.runDocker ?? runDocker
453
+ const armWatchdog = dependencies.armWatchdog ?? (dependencies.runDocker ? () => ({ disarm: () => ({ status: "test-disarmed" }) }) : armThawWatchdog)
454
+ const original = pausedTargetState(target, run)
455
+ if (!original.running || original.paused || original.restarting || original.dead) throw new Error("target original running state is invalid")
456
+ const watchdog = armWatchdog(target)
457
+ watchdog.assertLive?.()
458
+ let result
459
+ let operationError
460
+ let restoreError
461
+ try {
462
+ run(["pause", target.targetContainerId])
463
+ watchdog.assertLive?.()
464
+ const paused = pausedTargetState(target, run)
465
+ if (!paused.running || !paused.paused || paused.restarting || paused.dead) throw new Error("target paused state is invalid")
466
+ watchdog.assertLive?.()
467
+ result = operation()
468
+ watchdog.assertLive?.()
469
+ } catch (error) {
470
+ operationError = error
471
+ } finally {
472
+ try {
473
+ restorePausedTarget(target, run)
474
+ } catch (error) {
475
+ restoreError = error
476
+ }
477
+ }
478
+ if (!restoreError) {
479
+ try { watchdog.disarm() } catch (error) { restoreError = error }
480
+ }
481
+ if (restoreError && operationError) throw new AggregateError([operationError, restoreError], "target scan and restoration both failed")
482
+ if (restoreError) throw new Error("target failed to restore its original running state", { cause: restoreError })
483
+ if (operationError) throw operationError
484
+ return result
485
+ }
486
+
487
+ function dockerTopology(run = runDocker) {
488
+ const rows = run(["container", "ls", "-a", "--no-trunc", "--format", '{"ID":{{json .ID}},"Names":{{json .Names}}}'])
489
+ return rows.split(/\r?\n/u).filter(Boolean).map((line) => JSON.parse(line))
490
+ .filter((row) => typeof row.Names === "string" && CANONICAL_NAMES.includes(row.Names))
491
+ .map((row) => {
492
+ if (!CONTAINER_ID.test(row.ID)) throw new Error("canonical Butler list identity is invalid")
493
+ return { id: row.ID, name: row.Names }
494
+ })
495
+ }
496
+
497
+ function autostartNames() {
498
+ const content = readFileSync("/var/lib/docker/unraid-autostart", "utf8")
499
+ const names = content.split(/\r?\n/u).map((line) => line.trim().split(/\s+/u)[0]).filter((name) => CANONICAL_NAMES.includes(name))
500
+ if (new Set(names).size !== names.length) throw new Error("canonical autostart inventory is ambiguous")
501
+ return new Set(names)
502
+ }
503
+
504
+ async function captureCanonicalRecords(dependencies = {}) {
505
+ const listed = (dependencies.dockerTopology ?? dockerTopology)()
506
+ 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")
507
+ if (listed.length === 0) throw new Error("canonical Butler topology is empty")
508
+ const inspections = dependencies.inspectCanonical
509
+ ? dependencies.inspectCanonical(listed.map(({ id }) => id))
510
+ : JSON.parse(runDocker(["inspect", ...listed.map(({ id }) => id)]).trim())
511
+ if (!Array.isArray(inspections) || inspections.length !== listed.length) throw new Error("canonical Butler inspection is incomplete")
512
+ const starts = new Set((dependencies.autostartNames ?? (() => [...autostartNames()]))())
513
+ const graphqlRecords = await (dependencies.graphqlAutostartNames ?? queryGraphqlAutostart)()
514
+ if (!(graphqlRecords instanceof Map)) throw new Error("Unraid GraphQL autostart state is invalid")
515
+ const records = inspections.map((entry) => {
516
+ const name = String(entry.Name).replace(/^\//u, "")
517
+ const listedRecord = listed.find(({ id }) => id === entry.Id)
518
+ if (!listedRecord || listedRecord.name !== name) throw new Error("canonical Butler identity changed during atomic inspection")
519
+ return {
520
+ id: entry.Id,
521
+ names: [entry.Name],
522
+ imageId: entry.Image,
523
+ running: entry.State?.Running,
524
+ autoStart: starts.has(name),
525
+ restartPolicy: entry.HostConfig?.RestartPolicy?.Name,
526
+ pid: entry.State?.Pid,
527
+ networkMode: entry.HostConfig?.NetworkMode,
528
+ }
529
+ })
530
+ const recordNames = new Set(records.map(({ names }) => names[0].replace(/^\//u, "")))
531
+ if (!exactSet(recordNames, new Set(graphqlRecords.keys()))) throw new Error("Unraid GraphQL canonical topology presence disagrees with Docker")
532
+ const graphqlStarts = new Set([...graphqlRecords].filter(([, identity]) => identity?.autoStart === true).map(([name]) => name))
533
+ if (!exactSet(starts, graphqlStarts)) throw new Error("Unraid GraphQL and durable autostart state disagree")
534
+ for (const [name, identity] of graphqlRecords) {
535
+ const record = records.find((candidate) => candidate.names[0].replace(/^\//u, "") === name)
536
+ if (!record || record.id !== identity?.containerId || record.autoStart !== identity?.autoStart) throw new Error("Unraid GraphQL autostart identity disagrees with Docker")
537
+ }
538
+ return records
539
+ }
540
+
541
+ function parseProcNet(content, ipv6) {
542
+ return content.split(/\r?\n/u).slice(1).filter(Boolean).map((line) => line.trim().split(/\s+/u)).filter((fields) => fields[3] === "0A").map((fields) => {
543
+ const [address, portHex] = fields[1].split(":")
544
+ const localAddress = ipv6 ? address : [6, 4, 2, 0].map((offset) => Number.parseInt(address.slice(offset, offset + 2), 16)).join(".")
545
+ return { inode: fields[9], localAddress, port: Number.parseInt(portHex, 16) }
546
+ })
547
+ }
548
+
549
+ function parseProcUdp(content, ipv6) {
550
+ return content.split(/\r?\n/u).slice(1).filter(Boolean).map((line) => line.trim().split(/\s+/u)).map((fields) => {
551
+ const [address, portHex] = fields[1].split(":")
552
+ const localAddress = ipv6 ? address : [6, 4, 2, 0].map((offset) => Number.parseInt(address.slice(offset, offset + 2), 16)).join(".")
553
+ return { inode: fields[9], localAddress, port: Number.parseInt(portHex, 16) }
554
+ }).filter(({ port }) => port !== 0)
555
+ }
556
+
557
+ function cgroupProcessIds(rootPid, containerId, dependencies = {}) {
558
+ if (!Number.isSafeInteger(rootPid) || rootPid <= 0 || !CONTAINER_ID.test(containerId)) throw new Error("target cgroup identity is invalid")
559
+ const readCgroup = dependencies.readCgroup ?? ((pid) => readFileSync(`/proc/${pid}/cgroup`, "utf8"))
560
+ const readCgroupMembership = dependencies.readCgroupMembership ?? ((file) => readFileSync(file, "utf8"))
561
+ const lines = readCgroup(rootPid).split(/\r?\n/u).filter(Boolean)
562
+ const expectedPath = `/docker/${containerId}`
563
+ if (lines.length !== 1 || lines[0] !== `0::${expectedPath}`) throw new Error("target cgroup is not the exact Docker cgroup-v2 identity")
564
+ const membership = (file, label) => {
565
+ const raw = readCgroupMembership(`/sys/fs/cgroup${expectedPath}/${file}`).trim()
566
+ const ids = raw ? raw.split(/\s+/u).map(Number) : []
567
+ 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`)
568
+ return ids.sort((left, right) => left - right)
569
+ }
570
+ const processIds = membership("cgroup.procs", "process")
571
+ const threadIds = membership("cgroup.threads", "thread")
572
+ if (!processIds.includes(rootPid)) throw new Error("target root PID is absent from its cgroup")
573
+ if (!processIds.every((pid) => threadIds.includes(pid))) throw new Error("target cgroup thread membership is incomplete")
574
+ return { path: expectedPath, processIds, threadIds }
575
+ }
576
+
577
+ function ownedSocketInodes(threadIds, dependencies = {}) {
578
+ const listFileDescriptors = dependencies.listFileDescriptors ?? ((tid) => readdirSync(`/proc/${tid}/fd`))
579
+ const readDescriptorLink = dependencies.readDescriptorLink ?? ((tid, fd) => readlinkSync(`/proc/${tid}/fd/${fd}`))
580
+ const inodes = []
581
+ for (const tid of threadIds) for (const fd of listFileDescriptors(tid)) {
582
+ const match = /^socket:\[([0-9]+)\]$/u.exec(readDescriptorLink(tid, fd))
583
+ if (match) inodes.push(match[1])
584
+ }
585
+ return inodes
586
+ }
587
+
588
+
589
+ function parseProcUnix(content) {
590
+ return content.split(/\r?\n/u).slice(1).filter(Boolean).map((line) => line.trim().split(/\s+/u))
591
+ .filter((fields) => /^[0-9]+$/u.test(fields[6] ?? ""))
592
+ .map((fields) => ({ inode: fields[6], path: fields.slice(7).join(" "), flags: fields[3], type: fields[4], state: fields[5] }))
593
+ }
594
+
595
+ function sortedUnique(values) {
596
+ return [...new Set(values)].sort((left, right) => String(left).localeCompare(String(right)))
597
+ }
598
+
599
+ function sortedRecords(values) {
600
+ return [...values].sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)))
601
+ }
602
+
603
+ function sameMembership(left, right) {
604
+ return left.path === right.path
605
+ && exactSet(new Set(left.processIds), new Set(right.processIds))
606
+ && exactSet(new Set(left.threadIds), new Set(right.threadIds))
607
+ }
608
+
609
+ function completeContainmentSnapshot(provisional, readers) {
610
+ const netnsBefore = readers.readNetns(provisional.targetPid)
611
+ const membershipBefore = readers.readMembership(provisional.targetPid, provisional.targetContainerId)
612
+ const socketInodesBefore = sortedUnique(readers.readSockets(membershipBefore.threadIds))
613
+ const tcpListenersRaw = readers.readTcp(provisional.targetPid)
614
+ const udpListenersRaw = readers.readUdp(provisional.targetPid)
615
+ const unixSocketsRaw = readers.readUnix(provisional.targetPid)
616
+ const membershipAfter = readers.readMembership(provisional.targetPid, provisional.targetContainerId)
617
+ const socketInodesAfter = sortedUnique(readers.readSockets(membershipAfter.threadIds))
618
+ const netnsAfter = readers.readNetns(provisional.targetPid)
619
+ if (netnsBefore !== netnsAfter || !sameMembership(membershipBefore, membershipAfter)
620
+ || !exactSet(new Set(socketInodesBefore), new Set(socketInodesAfter))) return null
621
+ const owned = new Set(socketInodesAfter)
622
+ return {
623
+ netns: netnsAfter,
624
+ membership: membershipAfter,
625
+ socketInodes: socketInodesAfter,
626
+ tcpListeners: sortedRecords(tcpListenersRaw.filter((listener) => owned.has(listener.inode))),
627
+ udpListeners: sortedRecords(udpListenersRaw.filter((listener) => owned.has(listener.inode))),
628
+ unixSockets: sortedRecords(unixSocketsRaw.filter((socket) => owned.has(socket.inode))),
629
+ }
630
+ }
631
+
632
+ function convergedTerminalContainment(provisional, readers) {
633
+ let previous = null
634
+ for (let attempt = 0; attempt < TERMINAL_CONTAINMENT_MAX_SAMPLES; attempt += 1) {
635
+ const current = completeContainmentSnapshot(provisional, readers)
636
+ if (current && previous && JSON.stringify(current) === JSON.stringify(previous)) return current
637
+ previous = current
638
+ }
639
+ throw new Error("terminal containment snapshot did not converge")
640
+ }
641
+
642
+ async function runDeploymentTargetAudit(profileName, expectedImageId, dependencies = {}) {
643
+ const capture = dependencies.captureCanonicalRecords ?? captureCanonicalRecords
644
+ const before = await capture()
645
+ const provisional = attestDeploymentTarget({ profile: profileName, expectedImageId, topologyBefore: before, inspected: before, topologyAfter: before })
646
+ const readNetns = dependencies.readNetns ?? ((pid) => readlinkSync(`/proc/${pid}/ns/net`))
647
+ const readMembership = dependencies.cgroupProcessIds ?? cgroupProcessIds
648
+ const readSockets = dependencies.ownedSocketInodes ?? ownedSocketInodes
649
+ const readTcp = dependencies.readTcpListeners ?? ((pid) => [...parseProcNet(readFileSync(`/proc/${pid}/net/tcp`, "utf8"), false), ...parseProcNet(readFileSync(`/proc/${pid}/net/tcp6`, "utf8"), true)])
650
+ const readUdp = dependencies.readUdpListeners ?? ((pid) => [...parseProcUdp(readFileSync(`/proc/${pid}/net/udp`, "utf8"), false), ...parseProcUdp(readFileSync(`/proc/${pid}/net/udp6`, "utf8"), true)])
651
+ const readUnix = dependencies.readUnixSockets ?? ((pid) => parseProcUnix(readFileSync(`/proc/${pid}/net/unix`, "utf8")))
652
+ const quiesceTarget = dependencies.quiesceTarget ?? withPausedTarget
653
+ const containment = quiesceTarget(provisional, () => {
654
+ const netnsBefore = readNetns(provisional.targetPid)
655
+ const membershipBefore = readMembership(provisional.targetPid, provisional.targetContainerId)
656
+ const processIdsBefore = membershipBefore.processIds
657
+ const socketInodesBefore = readSockets(membershipBefore.threadIds)
658
+ const tcpListenersBefore = readTcp(provisional.targetPid)
659
+ const udpListenersBefore = readUdp(provisional.targetPid)
660
+ const unixSocketsBefore = readUnix(provisional.targetPid)
661
+ const membershipAfter = readMembership(provisional.targetPid, provisional.targetContainerId)
662
+ if (membershipBefore.path !== membershipAfter.path) throw new Error("target cgroup changed")
663
+ if (JSON.stringify(processIdsBefore) !== JSON.stringify(membershipAfter.processIds)) throw new Error("target cgroup process membership changed")
664
+ if (JSON.stringify(membershipBefore.threadIds) !== JSON.stringify(membershipAfter.threadIds)) throw new Error("target cgroup thread membership changed")
665
+ const processIdsAfter = membershipAfter.processIds
666
+ const socketInodesAfter = readSockets(membershipAfter.threadIds)
667
+ const tcpListenersAfter = readTcp(provisional.targetPid)
668
+ const udpListenersAfter = readUdp(provisional.targetPid)
669
+ const unixSocketsAfter = readUnix(provisional.targetPid)
670
+ const netnsAfter = readNetns(provisional.targetPid)
671
+ const terminal = convergedTerminalContainment(provisional, { readNetns, readMembership, readSockets, readTcp, readUdp, readUnix })
672
+ return { membershipBefore, processIdsBefore, socketInodesBefore, tcpListenersBefore, udpListenersBefore, unixSocketsBefore, membershipAfter, processIdsAfter, socketInodesAfter, tcpListenersAfter, udpListenersAfter, unixSocketsAfter, netnsBefore, netnsAfter, terminal }
673
+ })
674
+ const after = await capture()
675
+ const { membershipBefore, processIdsBefore, socketInodesBefore, tcpListenersBefore, udpListenersBefore, unixSocketsBefore, membershipAfter, processIdsAfter, socketInodesAfter, tcpListenersAfter, udpListenersAfter, unixSocketsAfter, netnsBefore, netnsAfter, terminal } = containment
676
+ const membershipTerminal = terminal.membership
677
+ const socketInodesTerminal = terminal.socketInodes
678
+ const tcpListenersTerminal = terminal.tcpListeners
679
+ const udpListenersTerminal = terminal.udpListeners
680
+ const unixSocketsTerminal = terminal.unixSockets
681
+ const sameIds = (left, right) => exactSet(new Set(left), new Set(right))
682
+ if (netnsBefore !== netnsAfter || netnsAfter !== terminal.netns) throw new Error("target network namespace changed")
683
+ if (membershipBefore.path !== membershipTerminal.path || membershipAfter.path !== membershipTerminal.path) throw new Error("target cgroup changed")
684
+ if (!sameIds(processIdsBefore, membershipTerminal.processIds) || !sameIds(processIdsAfter, membershipTerminal.processIds)) throw new Error("target cgroup process membership changed")
685
+ if (!sameIds(membershipBefore.threadIds, membershipTerminal.threadIds) || !sameIds(membershipAfter.threadIds, membershipTerminal.threadIds)) throw new Error("target cgroup thread membership changed")
686
+ if (!exactSet(new Set(socketInodesBefore), new Set(socketInodesTerminal)) || !exactSet(new Set(socketInodesAfter), new Set(socketInodesTerminal))) throw new Error("target socket ownership changed")
687
+ const deployment = attestDeploymentTarget({ profile: profileName, expectedImageId, topologyBefore: before, inspected: before, topologyAfter: after })
688
+ 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 }
689
+ return { schemaVersion: "sanctuary-effective-deployment-v1", deployment, listeners }
690
+ }
691
+
692
+ if (process.argv[1]?.endsWith("sanctuary-deployment-target.mjs") && process.argv[2] === "--thaw-watchdog") {
693
+ const [targetContainerId, rawTargetPid, rawParentPid, parentBootId, parentStarttime, root] = process.argv.slice(3)
694
+ try { await runThawWatchdog(targetContainerId, Number(rawTargetPid), Number(rawParentPid), parentBootId, parentStarttime, root) } catch (error) {
695
+ if (/^\/run\/ouro-thaw-watchdog\.[0-9]+\.[0-9]+$/u.test(root ?? "") && !existsSync(`${root}/watchdog-terminal.json`)) {
696
+ 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 {}
697
+ }
698
+ process.exitCode = 1
699
+ }
700
+ } else if (process.argv[1]?.endsWith("sanctuary-deployment-target.mjs")) {
701
+ const [profile, expectedImageId] = process.argv.slice(2)
702
+ if (process.argv.length !== 4) process.exitCode = 2
703
+ else {
704
+ try { process.stdout.write(`${JSON.stringify(await runDeploymentTargetAudit(profile, expectedImageId))}\n`) }
705
+ catch { process.exitCode = 1 }
706
+ }
707
+ }
708
+
709
+ export { attestDeploymentTarget, attestOwnedListeners, armThawWatchdog, captureCanonicalRecords, cgroupProcessIds, dockerTopology, ownedSocketInodes, parseProcNet, parseProcStatIdentity, parseProcUdp, parseProcUnix, queryGraphqlAutostart, runDeploymentTargetAudit, runKillableCommand, runThawWatchdog, targetProfile, withPausedTarget }