@ouro.bot/cli 0.1.0-alpha.80 → 0.1.0-alpha.800

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