@ouro.bot/cli 0.1.0-alpha.99 → 0.1.0-alpha.991

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 (777) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +171 -23
  3. package/RepairGuide.ouro/agent.json +5 -0
  4. package/RepairGuide.ouro/psyche/IDENTITY.md +19 -0
  5. package/RepairGuide.ouro/psyche/SOUL.md +55 -0
  6. package/RepairGuide.ouro/skills/diagnose-broken-remote.md +63 -0
  7. package/RepairGuide.ouro/skills/diagnose-stacked-typed-issues.md +35 -0
  8. package/RepairGuide.ouro/skills/diagnose-sync-blocked.md +54 -0
  9. package/RepairGuide.ouro/skills/diagnose-vault-expired.md +60 -0
  10. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/agent.json +4 -2
  11. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/SOUL.md +2 -2
  12. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-serpent.md +1 -1
  13. package/assets/bluebubbles-host +264 -0
  14. package/assets/sanctuary-host-launcher.sh +16 -0
  15. package/changelog.json +6257 -16
  16. package/deploy/unraid/Dockerfile +71 -0
  17. package/deploy/unraid/INSTALL.md +64 -0
  18. package/deploy/unraid/README.txt +3032 -0
  19. package/deploy/unraid/audit-container-spec.sh +4 -0
  20. package/deploy/unraid/container-runtime.json +1 -0
  21. package/deploy/unraid/credit-gateway.md +95 -0
  22. package/deploy/unraid/docker-man-template-transaction.mjs +727 -0
  23. package/deploy/unraid/docker-man-template-xml.cjs +105 -0
  24. package/deploy/unraid/github_known_hosts +3 -0
  25. package/deploy/unraid/install.sh +274 -0
  26. package/deploy/unraid/migrate-sanctuary-bundle.mjs +63 -0
  27. package/deploy/unraid/ouro-events/bootstrap-spool.sh +92 -0
  28. package/deploy/unraid/ouro-events/emit-event.mjs +310 -0
  29. package/deploy/unraid/ouro-events/emit-usenet-event.sh +54 -0
  30. package/deploy/unraid/ouro-events/install-usenet-guard.sh +435 -0
  31. package/deploy/unraid/ouro-events/usenet-health.sh +176 -0
  32. package/deploy/unraid/ouro-sync-ssh +98 -0
  33. package/deploy/unraid/ouro-sync.sudoers +3 -0
  34. package/deploy/unraid/sanctuary-acceptance-adapter.sh +43 -0
  35. package/deploy/unraid/sanctuary-acceptance-contract.json +198 -0
  36. package/deploy/unraid/sanctuary-acceptance-harness.sh +23 -0
  37. package/deploy/unraid/sanctuary-authority-installation.json +36 -0
  38. package/deploy/unraid/sanctuary-authority-service.sh +30 -0
  39. package/deploy/unraid/sanctuary-butler-upgrade.mjs +1523 -0
  40. package/deploy/unraid/sanctuary-deployment-target.mjs +710 -0
  41. package/deploy/unraid/sanctuary-replay-gate.mjs +2467 -0
  42. package/deploy/unraid/sanctuary-safe-rename.py +24 -0
  43. package/deploy/unraid/sanctuary-sync-cutover.sh +1039 -0
  44. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +2088 -0
  45. package/deploy/unraid/sanctuary-unit16-run.sh +570 -0
  46. package/deploy/unraid/sanctuary-unit18-target-audit.sh +27 -0
  47. package/deploy/unraid/sanctuary.ouro/agent.json +67 -0
  48. package/deploy/unraid/sanctuary.ouro/arc/README.md +3 -0
  49. package/deploy/unraid/sanctuary.ouro/awaiting/house-care-sweep.md +17 -0
  50. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +5 -0
  51. package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +8 -0
  52. package/deploy/unraid/sanctuary.ouro/mcp/media-mcp.mjs +2255 -0
  53. package/deploy/unraid/sanctuary.ouro/product/PRODUCT.md +13 -0
  54. package/deploy/unraid/sanctuary.ouro/provider-readiness.json +11 -0
  55. package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +5 -0
  56. package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +7 -0
  57. package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +7 -0
  58. package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +15 -0
  59. package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +3 -0
  60. package/deploy/unraid/sanctuary.ouro/state/policy/steward.json +7 -0
  61. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +234 -0
  62. package/deploy/unraid/sanctuary.xml +30 -0
  63. package/dist/a2a/card.js +57 -0
  64. package/dist/a2a/client.js +290 -0
  65. package/dist/a2a/config.js +50 -0
  66. package/dist/a2a/delegated-command-grants.js +93 -0
  67. package/dist/a2a/delegated-command.js +197 -0
  68. package/dist/a2a/delegation-stores.js +56 -0
  69. package/dist/a2a/did-resolution.js +205 -0
  70. package/dist/a2a/escalation-grants.js +84 -0
  71. package/dist/a2a/escalation-wiring.js +60 -0
  72. package/dist/a2a/identity.js +258 -0
  73. package/dist/a2a/inbound-share.js +131 -0
  74. package/dist/a2a/mission-result-wire.js +265 -0
  75. package/dist/a2a/onboarding.js +133 -0
  76. package/dist/a2a/operator-trust.js +399 -0
  77. package/dist/a2a/outbox-client.js +59 -0
  78. package/dist/a2a/outbox-store.js +188 -0
  79. package/dist/a2a/outbox-wire.js +106 -0
  80. package/dist/a2a/pin-store.js +167 -0
  81. package/dist/a2a/refusals.js +53 -0
  82. package/dist/a2a/relay-client.js +106 -0
  83. package/dist/a2a/relay-config-vault.js +64 -0
  84. package/dist/a2a/relay-contacts.js +120 -0
  85. package/dist/a2a/relay-inbox.js +360 -0
  86. package/dist/a2a/relay-retirements.js +89 -0
  87. package/dist/a2a/replay-grant-holders.js +33 -0
  88. package/dist/a2a/replay-harness.js +188 -0
  89. package/dist/a2a/resolution-proof.js +53 -0
  90. package/dist/a2a/sealed-chat.js +114 -0
  91. package/dist/a2a/seen-ledger.js +214 -0
  92. package/dist/a2a/server.js +808 -0
  93. package/dist/a2a/task-store.js +69 -0
  94. package/dist/a2a/transport.js +100 -0
  95. package/dist/a2a/trust-commands.js +203 -0
  96. package/dist/a2a/trusted-files.js +185 -0
  97. package/dist/a2a/types.js +3 -0
  98. package/dist/arc/attention-types.js +8 -0
  99. package/dist/arc/cares.js +360 -0
  100. package/dist/arc/episodes.js +118 -0
  101. package/dist/arc/evolution.js +490 -0
  102. package/dist/arc/flight-recorder.js +1034 -0
  103. package/dist/arc/intentions.js +134 -0
  104. package/dist/arc/json-store.js +117 -0
  105. package/dist/arc/obligations.js +386 -0
  106. package/dist/arc/packets.js +336 -0
  107. package/dist/arc/presence.js +185 -0
  108. package/dist/arc/task-lifecycle.js +57 -0
  109. package/dist/commerce/store.js +755 -0
  110. package/dist/commerce/types.js +3 -0
  111. package/dist/gateway/keys.js +117 -0
  112. package/dist/gateway/server.js +429 -0
  113. package/dist/heart/active-work.js +798 -40
  114. package/dist/heart/agent-entry.js +252 -3
  115. package/dist/heart/approval-files.js +113 -0
  116. package/dist/heart/approval-store.js +960 -0
  117. package/dist/heart/attachments/image-normalize.js +201 -0
  118. package/dist/heart/attachments/materialize.js +97 -0
  119. package/dist/heart/attachments/originals.js +88 -0
  120. package/dist/heart/attachments/render.js +29 -0
  121. package/dist/heart/attachments/sources/bluebubbles.js +156 -0
  122. package/dist/heart/attachments/sources/cli-local-file.js +78 -0
  123. package/dist/heart/attachments/sources/index.js +18 -0
  124. package/dist/heart/attachments/sources/telegram.js +71 -0
  125. package/dist/heart/attachments/store.js +132 -0
  126. package/dist/heart/attachments/types.js +93 -0
  127. package/dist/heart/auth/auth-flow.js +511 -0
  128. package/dist/heart/autonomy-budget.js +478 -0
  129. package/dist/heart/awaiting/a2a-await-delivery.js +109 -0
  130. package/dist/heart/awaiting/await-alert.js +152 -0
  131. package/dist/heart/awaiting/await-expiry.js +143 -0
  132. package/dist/heart/awaiting/await-loader.js +91 -0
  133. package/dist/heart/awaiting/await-parser.js +152 -0
  134. package/dist/heart/awaiting/await-runtime-state.js +130 -0
  135. package/dist/heart/awaiting/await-scheduler.js +391 -0
  136. package/dist/heart/background-operations.js +281 -0
  137. package/dist/heart/bluebubbles-host-account.js +78 -0
  138. package/dist/heart/bridges/manager.js +137 -17
  139. package/dist/heart/bridges/store.js +14 -2
  140. package/dist/heart/bundle-state.js +168 -0
  141. package/dist/heart/commitments.js +44 -18
  142. package/dist/heart/config-registry.js +331 -0
  143. package/dist/heart/config.js +179 -131
  144. package/dist/heart/context-loss-gauntlet.js +387 -0
  145. package/dist/heart/context-loss-sentinel.js +1204 -0
  146. package/dist/heart/core.js +2378 -586
  147. package/dist/heart/cross-chat-delivery.js +9 -22
  148. package/dist/heart/daemon/agent-config-check.js +474 -0
  149. package/dist/heart/daemon/agent-discovery.js +287 -3
  150. package/dist/heart/daemon/agent-home.js +355 -0
  151. package/dist/heart/daemon/agent-service.js +542 -0
  152. package/dist/heart/daemon/agentic-repair.js +547 -0
  153. package/dist/heart/daemon/await-private-wake.js +59 -0
  154. package/dist/heart/daemon/bluebubbles-cli-integration.js +71 -0
  155. package/dist/heart/daemon/bluebubbles-health-diagnostics.js +122 -0
  156. package/dist/heart/daemon/bluebubbles-host-protocol.js +433 -0
  157. package/dist/heart/daemon/bluebubbles-host.js +273 -0
  158. package/dist/heart/daemon/boot-sync-probe.js +223 -0
  159. package/dist/heart/daemon/cadence.js +180 -0
  160. package/dist/heart/daemon/cli-arg-redaction.js +26 -0
  161. package/dist/heart/daemon/cli-defaults.js +881 -0
  162. package/dist/heart/daemon/cli-desk.js +331 -0
  163. package/dist/heart/daemon/cli-exec.js +9339 -0
  164. package/dist/heart/daemon/cli-help.js +766 -0
  165. package/dist/heart/daemon/cli-parse.js +3343 -0
  166. package/dist/heart/daemon/cli-render-doctor.js +57 -0
  167. package/dist/heart/daemon/cli-render.js +819 -0
  168. package/dist/heart/daemon/cli-types.js +8 -0
  169. package/dist/heart/daemon/connect-bay.js +323 -0
  170. package/dist/heart/daemon/container-credential-bootstrap.js +273 -0
  171. package/dist/heart/daemon/container-healthcheck.js +112 -0
  172. package/dist/heart/daemon/container-runtime.js +97 -0
  173. package/dist/heart/daemon/container-spec-auditor-main.js +187 -0
  174. package/dist/heart/daemon/container-spec-auditor.js +257 -0
  175. package/dist/heart/daemon/daemon-bootstrap-startup.js +115 -0
  176. package/dist/heart/daemon/daemon-cli.js +29 -2373
  177. package/dist/heart/daemon/daemon-entry.js +975 -11
  178. package/dist/heart/daemon/daemon-health.js +186 -0
  179. package/dist/heart/daemon/daemon-rollup.js +57 -0
  180. package/dist/heart/daemon/daemon-runtime-sync.js +88 -13
  181. package/dist/heart/daemon/daemon-tombstone.js +236 -0
  182. package/dist/heart/daemon/daemon.js +2110 -104
  183. package/dist/heart/daemon/desk-admin.js +420 -0
  184. package/dist/heart/daemon/dns-workflow.js +394 -0
  185. package/dist/heart/daemon/doctor-types.js +8 -0
  186. package/dist/heart/daemon/doctor.js +1766 -0
  187. package/dist/heart/daemon/freshness.js +345 -0
  188. package/dist/heart/daemon/habit-private-wake.js +61 -0
  189. package/dist/heart/daemon/health-monitor.js +154 -11
  190. package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
  191. package/dist/heart/daemon/hooks/bundle-meta.js +135 -1
  192. package/dist/heart/daemon/http-health-probe.js +80 -0
  193. package/dist/heart/daemon/human-command-screens.js +234 -0
  194. package/dist/heart/daemon/human-readiness.js +114 -0
  195. package/dist/heart/daemon/inner-status.js +78 -0
  196. package/dist/heart/daemon/interactive-repair.js +394 -0
  197. package/dist/heart/daemon/launchd.js +37 -8
  198. package/dist/heart/daemon/log-tailer.js +79 -10
  199. package/dist/heart/daemon/logs-prune.js +132 -0
  200. package/dist/heart/daemon/mcp-canary.js +524 -0
  201. package/dist/heart/daemon/message-router.js +65 -5
  202. package/dist/heart/daemon/migrate-to-desk.js +848 -0
  203. package/dist/heart/daemon/os-cron-deps.js +135 -0
  204. package/dist/heart/daemon/os-cron.js +22 -14
  205. package/dist/heart/daemon/ouro-bot-entry.js +6 -3
  206. package/dist/heart/daemon/ouro-entry.js +9 -3
  207. package/dist/heart/daemon/plugin-cli.js +432 -0
  208. package/dist/heart/daemon/process-manager.js +760 -61
  209. package/dist/heart/daemon/provider-discovery.js +137 -0
  210. package/dist/heart/daemon/provider-ping-progress.js +83 -0
  211. package/dist/heart/daemon/prunable-bundle.js +144 -0
  212. package/dist/heart/daemon/pulse.js +511 -0
  213. package/dist/heart/daemon/readiness-repair.js +365 -0
  214. package/dist/heart/daemon/run-hooks.js +2 -0
  215. package/dist/heart/daemon/runtime-logging.js +47 -14
  216. package/dist/heart/daemon/runtime-metadata.js +2 -30
  217. package/dist/heart/daemon/runtime-mode.js +13 -2
  218. package/dist/heart/daemon/safe-mode.js +161 -0
  219. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +2662 -0
  220. package/dist/heart/daemon/sanctuary-acceptance-harness.js +1832 -0
  221. package/dist/heart/daemon/sanctuary-acceptance-marker.js +267 -0
  222. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +956 -0
  223. package/dist/heart/daemon/sanctuary-authority-codec.js +86 -0
  224. package/dist/heart/daemon/sanctuary-authority-epoch.js +291 -0
  225. package/dist/heart/daemon/sanctuary-authority-installation.js +161 -0
  226. package/dist/heart/daemon/sanctuary-authority-ledger.js +241 -0
  227. package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +1332 -0
  228. package/dist/heart/daemon/sanctuary-authority-vault-migration.js +79 -0
  229. package/dist/heart/daemon/sanctuary-bundle-migration.js +974 -0
  230. package/dist/heart/daemon/sanctuary-host-authority.js +1008 -0
  231. package/dist/heart/daemon/sanctuary-host-detached-supervisor.js +493 -0
  232. package/dist/heart/daemon/sanctuary-host-executor.js +670 -0
  233. package/dist/heart/daemon/sanctuary-host-linux-kernel.js +334 -0
  234. package/dist/heart/daemon/sanctuary-host-supervisor-entry.js +207 -0
  235. package/dist/heart/daemon/sanctuary-host-supervisor.js +95 -0
  236. package/dist/heart/daemon/sanctuary-package-management.js +113 -0
  237. package/dist/heart/daemon/sanctuary-scheduler-liveness.js +227 -0
  238. package/dist/heart/daemon/sanctuary-scheduler-origin.js +132 -0
  239. package/dist/heart/daemon/sanctuary-telegram-authority-entry.js +475 -0
  240. package/dist/heart/daemon/sanctuary-telegram-authority-gateway.js +585 -0
  241. package/dist/heart/daemon/sanctuary-telegram-authority-service.js +676 -0
  242. package/dist/heart/daemon/sense-manager.js +786 -48
  243. package/dist/heart/daemon/session-id-resolver.js +131 -0
  244. package/dist/heart/daemon/skill-management-installer.js +1 -1
  245. package/dist/heart/daemon/socket-client.js +180 -12
  246. package/dist/heart/daemon/stale-bundle-prune.js +113 -0
  247. package/dist/heart/daemon/startup-tui.js +339 -0
  248. package/dist/heart/daemon/supercronic-supervisor.js +282 -0
  249. package/dist/heart/daemon/task-scheduler.js +117 -39
  250. package/dist/heart/daemon/terminal-ui.js +503 -0
  251. package/dist/heart/daemon/thoughts.js +138 -54
  252. package/dist/heart/daemon/up-progress.js +366 -0
  253. package/dist/heart/daemon/vault-items.js +56 -0
  254. package/dist/heart/delegation.js +1 -4
  255. package/dist/heart/external-events/router.js +1531 -0
  256. package/dist/heart/failure-reports.js +336 -0
  257. package/dist/heart/file-lock.js +109 -0
  258. package/dist/heart/frontend-approval-runtime.js +506 -0
  259. package/dist/heart/frontend-journal.js +215 -0
  260. package/dist/heart/frontend-session-service.js +271 -0
  261. package/dist/heart/frontend-socket-client.js +173 -0
  262. package/dist/heart/frontend-socket.js +434 -0
  263. package/dist/heart/frontend-turn-errors.js +20 -0
  264. package/dist/heart/habits/habit-cancel.js +810 -0
  265. package/dist/heart/habits/habit-lifecycle.js +1327 -0
  266. package/dist/heart/habits/habit-migration.js +194 -0
  267. package/dist/heart/habits/habit-parser.js +278 -0
  268. package/dist/heart/habits/habit-runtime-state.js +93 -0
  269. package/dist/heart/habits/habit-scheduler.js +653 -0
  270. package/dist/heart/habits/habit-session-summary.js +318 -0
  271. package/dist/heart/habits/habit-session.js +636 -0
  272. package/dist/heart/{daemon → hatch}/hatch-flow.js +55 -62
  273. package/dist/heart/{daemon → hatch}/hatch-specialist.js +6 -8
  274. package/dist/heart/{daemon → hatch}/specialist-prompt.js +12 -9
  275. package/dist/heart/{daemon → hatch}/specialist-tools.js +53 -18
  276. package/dist/heart/identity.js +239 -59
  277. package/dist/heart/kept-notes.js +289 -0
  278. package/dist/heart/kicks.js +1 -1
  279. package/dist/heart/machine-identity.js +175 -0
  280. package/dist/heart/mail-import-discovery.js +356 -0
  281. package/dist/heart/mailbox/mailbox-http-hooks.js +99 -0
  282. package/dist/heart/mailbox/mailbox-http-response.js +7 -0
  283. package/dist/heart/mailbox/mailbox-http-routes.js +406 -0
  284. package/dist/heart/mailbox/mailbox-http-static.js +103 -0
  285. package/dist/heart/mailbox/mailbox-http-transport.js +272 -0
  286. package/dist/heart/mailbox/mailbox-http.js +99 -0
  287. package/dist/heart/mailbox/mailbox-read.js +41 -0
  288. package/dist/heart/mailbox/mailbox-supervision.js +131 -0
  289. package/dist/heart/mailbox/mailbox-types.js +27 -0
  290. package/dist/heart/mailbox/mailbox-view.js +197 -0
  291. package/dist/heart/mailbox/readers/agent-machine.js +428 -0
  292. package/dist/heart/mailbox/readers/continuity-readers.js +329 -0
  293. package/dist/heart/mailbox/readers/mail.js +375 -0
  294. package/dist/heart/mailbox/readers/runtime-readers.js +823 -0
  295. package/dist/heart/mailbox/readers/sessions.js +231 -0
  296. package/dist/heart/mailbox/readers/shared.js +111 -0
  297. package/dist/heart/mcp/mcp-server.js +760 -0
  298. package/dist/heart/migrate-config.js +100 -0
  299. package/dist/heart/model-capabilities.js +83 -16
  300. package/dist/heart/orientation-frame.js +376 -0
  301. package/dist/heart/platform.js +81 -0
  302. package/dist/heart/primitives.js +1 -1
  303. package/dist/heart/private-runtime/decision-renderer.js +158 -0
  304. package/dist/heart/private-runtime/index.js +18 -0
  305. package/dist/heart/private-runtime/ledger.js +221 -0
  306. package/dist/heart/private-runtime/policy.js +260 -0
  307. package/dist/heart/private-runtime/types.js +2 -0
  308. package/dist/heart/progress-story.js +1 -1
  309. package/dist/heart/provider-attempt.js +137 -0
  310. package/dist/heart/provider-binding-resolver.js +273 -0
  311. package/dist/heart/provider-credentials.js +449 -0
  312. package/dist/heart/provider-failover.js +311 -0
  313. package/dist/heart/provider-models.js +96 -0
  314. package/dist/heart/provider-ping.js +266 -0
  315. package/dist/heart/provider-readiness-cache.js +119 -0
  316. package/dist/heart/provider-visibility.js +188 -0
  317. package/dist/heart/providers/anthropic-token.js +131 -0
  318. package/dist/heart/providers/anthropic.js +248 -67
  319. package/dist/heart/providers/azure.js +23 -11
  320. package/dist/heart/providers/error-classification.js +129 -0
  321. package/dist/heart/providers/github-copilot.js +38 -39
  322. package/dist/heart/providers/minimax-vlm.js +189 -0
  323. package/dist/heart/providers/minimax.js +26 -8
  324. package/dist/heart/providers/openai-codex-token.js +349 -0
  325. package/dist/heart/providers/openai-codex.js +55 -40
  326. package/dist/heart/providers/openai-compatible.js +177 -0
  327. package/dist/heart/reach-channel.js +85 -0
  328. package/dist/heart/reply-style.js +52 -0
  329. package/dist/heart/run-ledger.js +247 -0
  330. package/dist/heart/runtime-capability-check.js +170 -0
  331. package/dist/heart/runtime-credentials.js +608 -0
  332. package/dist/heart/runtime-cwd.js +87 -0
  333. package/dist/heart/sense-truth.js +24 -4
  334. package/dist/heart/session-activity.js +94 -24
  335. package/dist/heart/session-events.js +1521 -0
  336. package/dist/heart/session-playback-cli-main.js +5 -0
  337. package/dist/heart/session-playback-cli.js +36 -0
  338. package/dist/heart/session-playback.js +231 -0
  339. package/dist/heart/session-redaction-repair-cli-main.js +31 -0
  340. package/dist/heart/session-redaction-repair.js +552 -0
  341. package/dist/heart/session-stats-cli-main.js +5 -0
  342. package/dist/heart/session-stats.js +183 -0
  343. package/dist/heart/session-transcript.js +133 -0
  344. package/dist/heart/shelf-claims.js +78 -0
  345. package/dist/heart/shepherd/custody-types.js +2 -0
  346. package/dist/heart/shepherd/mandates.js +352 -0
  347. package/dist/heart/shepherd/operation-commands.js +19 -0
  348. package/dist/heart/shepherd/operations.js +135 -0
  349. package/dist/heart/shepherd/supervision.js +114 -0
  350. package/dist/heart/shepherd/types.js +2 -0
  351. package/dist/heart/shepherd-config.js +123 -0
  352. package/dist/heart/source-grounding.js +313 -0
  353. package/dist/heart/start-of-turn-packet.js +372 -0
  354. package/dist/heart/steward-policy.js +656 -0
  355. package/dist/heart/streaming.js +631 -207
  356. package/dist/heart/structured-output.js +196 -0
  357. package/dist/heart/sync-classification.js +176 -0
  358. package/dist/heart/sync-health.js +152 -0
  359. package/dist/heart/sync-lock.js +431 -0
  360. package/dist/heart/sync.js +1579 -0
  361. package/dist/heart/target-resolution.js +11 -7
  362. package/dist/heart/tempo.js +93 -0
  363. package/dist/heart/temporal-view.js +41 -0
  364. package/dist/heart/timeouts.js +101 -0
  365. package/dist/heart/tool-activity-callbacks.js +59 -0
  366. package/dist/heart/tool-approval.js +382 -0
  367. package/dist/heart/tool-description.js +155 -0
  368. package/dist/heart/tool-friction.js +55 -0
  369. package/dist/heart/tool-loop.js +206 -0
  370. package/dist/heart/tool-output-budget.js +41 -0
  371. package/dist/heart/turn-context.js +496 -0
  372. package/dist/heart/turn-execution-lease.js +30 -0
  373. package/dist/heart/{daemon → versioning}/ouro-bot-global-installer.js +6 -5
  374. package/dist/heart/{daemon → versioning}/ouro-bot-wrapper.js +3 -2
  375. package/dist/heart/versioning/ouro-path-installer.js +432 -0
  376. package/dist/heart/versioning/ouro-recovery-launcher.js +76 -0
  377. package/dist/heart/versioning/ouro-version-manager.js +432 -0
  378. package/dist/heart/versioning/release-sources.js +173 -0
  379. package/dist/heart/versioning/self-update-defaults.js +294 -0
  380. package/dist/heart/versioning/self-update.js +196 -0
  381. package/dist/heart/{daemon → versioning}/staged-restart.js +40 -8
  382. package/dist/heart/{daemon → versioning}/update-checker.js +6 -1
  383. package/dist/heart/versioning/update-hooks.js +154 -0
  384. package/dist/heart/versioning/update-state.js +126 -0
  385. package/dist/heart/versioning/updater-launchd.js +150 -0
  386. package/dist/heart/versioning/version-intent.js +114 -0
  387. package/dist/heart/work-card.js +380 -0
  388. package/dist/mailbox-ui/assets/index-Dbska1tZ.js +1 -0
  389. package/dist/mailbox-ui/assets/index-DsiKaPyH.css +1 -0
  390. package/dist/mailbox-ui/assets/vendor-CcN1XpQ9.js +61 -0
  391. package/dist/mailbox-ui/index.html +16 -0
  392. package/dist/mailroom/attention.js +167 -0
  393. package/dist/mailroom/autonomy.js +209 -0
  394. package/dist/mailroom/blob-store.js +889 -0
  395. package/dist/mailroom/body-cache.js +61 -0
  396. package/dist/mailroom/cache-sync-cli.js +58 -0
  397. package/dist/mailroom/core.js +803 -0
  398. package/dist/mailroom/entry.js +160 -0
  399. package/dist/mailroom/file-store.js +568 -0
  400. package/dist/mailroom/hosted-cache-sync.js +384 -0
  401. package/dist/mailroom/mbox-import.js +393 -0
  402. package/dist/mailroom/migration.js +164 -0
  403. package/dist/mailroom/outbound.js +380 -0
  404. package/dist/mailroom/policy.js +263 -0
  405. package/dist/mailroom/reader.js +292 -0
  406. package/dist/mailroom/search-cache.js +513 -0
  407. package/dist/mailroom/search-relevance.js +319 -0
  408. package/dist/mailroom/smtp-ingress.js +176 -0
  409. package/dist/mailroom/source-state.js +176 -0
  410. package/dist/mailroom/thread.js +109 -0
  411. package/dist/mailroom/travel-extract.js +89 -0
  412. package/dist/mind/bundle-manifest.js +25 -2
  413. package/dist/mind/context.js +328 -139
  414. package/dist/mind/desk-section.js +362 -0
  415. package/dist/mind/diary-integrity.js +60 -0
  416. package/dist/mind/{memory.js → diary.js} +68 -77
  417. package/dist/mind/embedding-provider.js +60 -0
  418. package/dist/mind/file-state.js +179 -0
  419. package/dist/mind/friend-link-codes.js +209 -0
  420. package/dist/mind/friend-merge.js +336 -0
  421. package/dist/mind/house-names.js +43 -0
  422. package/dist/mind/{associative-recall.js → note-search.js} +47 -58
  423. package/dist/mind/obligation-steering.js +199 -3
  424. package/dist/mind/outbound-record.js +168 -0
  425. package/dist/mind/pending.js +36 -10
  426. package/dist/mind/prompt-budget.js +479 -0
  427. package/dist/mind/prompt-refresh.js +3 -2
  428. package/dist/mind/prompt.js +1222 -203
  429. package/dist/mind/provenance-trust.js +26 -0
  430. package/dist/mind/record-paths.js +312 -0
  431. package/dist/mind/scrutiny.js +173 -0
  432. package/dist/mind/session-transaction.js +622 -0
  433. package/dist/nerves/cli-logging.js +7 -1
  434. package/dist/nerves/coverage/audit-rules.js +15 -6
  435. package/dist/nerves/coverage/audit.js +28 -2
  436. package/dist/nerves/coverage/cli.js +1 -1
  437. package/dist/nerves/coverage/contract.js +5 -5
  438. package/dist/nerves/coverage/file-completeness.js +143 -5
  439. package/dist/nerves/coverage/run-artifacts.js +55 -35
  440. package/dist/nerves/coverage/source-scanner.js +4 -4
  441. package/dist/nerves/event-buffer.js +111 -0
  442. package/dist/nerves/index.js +293 -9
  443. package/dist/nerves/observation.js +20 -0
  444. package/dist/nerves/redact.js +79 -0
  445. package/dist/nerves/review/cli-main.js +5 -0
  446. package/dist/nerves/review/cli.js +156 -0
  447. package/dist/nerves/review/core.js +159 -0
  448. package/dist/nerves/runtime.js +17 -1
  449. package/dist/repertoire/ado-client.js +15 -56
  450. package/dist/repertoire/ado-semantic.js +16 -10
  451. package/dist/repertoire/api-client.js +97 -0
  452. package/dist/repertoire/bitwarden-store.js +1199 -0
  453. package/dist/repertoire/bundle-templates.js +71 -0
  454. package/dist/repertoire/bw-installer.js +231 -0
  455. package/dist/repertoire/coding/codex-jsonl.js +64 -0
  456. package/dist/repertoire/coding/context-pack.js +331 -0
  457. package/dist/repertoire/coding/feedback.js +146 -34
  458. package/dist/repertoire/coding/index.js +15 -3
  459. package/dist/repertoire/coding/manager.js +159 -10
  460. package/dist/repertoire/coding/spawner.js +40 -11
  461. package/dist/repertoire/coding/tools.js +182 -18
  462. package/dist/repertoire/coding/workbench-client.js +272 -0
  463. package/dist/repertoire/coding/workbench-manager.js +346 -0
  464. package/dist/repertoire/commerce-errors.js +109 -0
  465. package/dist/repertoire/commerce-self-test.js +156 -0
  466. package/dist/repertoire/credential-access.js +183 -0
  467. package/dist/repertoire/desk/classifier.js +362 -0
  468. package/dist/repertoire/duffel-client.js +185 -0
  469. package/dist/repertoire/github-client.js +14 -55
  470. package/dist/repertoire/graph-client.js +11 -52
  471. package/dist/repertoire/guardrails.js +1480 -22
  472. package/dist/repertoire/house-sweep.js +471 -0
  473. package/dist/repertoire/mcp-client.js +58 -5
  474. package/dist/repertoire/mcp-manager.js +427 -139
  475. package/dist/repertoire/mcp-tools.js +132 -0
  476. package/dist/repertoire/mcp-write-guard.js +46 -0
  477. package/dist/repertoire/plugin-mcp.js +175 -0
  478. package/dist/repertoire/plugins.js +253 -0
  479. package/dist/repertoire/relationship-authorization.js +322 -0
  480. package/dist/repertoire/sanctuary-container-policy.js +50 -0
  481. package/dist/repertoire/shell-sessions.js +134 -0
  482. package/dist/repertoire/skills.js +48 -4
  483. package/dist/repertoire/stripe-client.js +131 -0
  484. package/dist/repertoire/tool-arguments.js +182 -0
  485. package/dist/repertoire/tool-results.js +29 -0
  486. package/dist/repertoire/tools-a2a.js +742 -0
  487. package/dist/repertoire/tools-attachments.js +322 -0
  488. package/dist/repertoire/tools-awaiting.js +897 -0
  489. package/dist/repertoire/tools-base.js +83 -1168
  490. package/dist/repertoire/tools-bluebubbles.js +2 -0
  491. package/dist/repertoire/tools-bridge.js +144 -0
  492. package/dist/repertoire/tools-bundle.js +917 -0
  493. package/dist/repertoire/tools-commerce.js +253 -0
  494. package/dist/repertoire/tools-config.js +186 -0
  495. package/dist/repertoire/tools-continuity.js +588 -0
  496. package/dist/repertoire/tools-credential.js +383 -0
  497. package/dist/repertoire/tools-escalation.js +84 -0
  498. package/dist/repertoire/tools-evolution.js +527 -0
  499. package/dist/repertoire/tools-files.js +362 -0
  500. package/dist/repertoire/tools-flight.js +290 -0
  501. package/dist/repertoire/tools-flow.js +119 -0
  502. package/dist/repertoire/tools-friend-link.js +57 -0
  503. package/dist/repertoire/tools-github.js +3 -8
  504. package/dist/repertoire/tools-habits.js +103 -0
  505. package/dist/repertoire/tools-house-care.js +174 -0
  506. package/dist/repertoire/tools-mail.js +1828 -0
  507. package/dist/repertoire/tools-notes.js +501 -0
  508. package/dist/repertoire/tools-obligations.js +182 -0
  509. package/dist/repertoire/tools-orientation.js +31 -0
  510. package/dist/repertoire/tools-record.js +482 -0
  511. package/dist/repertoire/tools-rsvp.js +139 -0
  512. package/dist/repertoire/tools-runtime.js +238 -0
  513. package/dist/repertoire/tools-sanctuary-host.js +202 -0
  514. package/dist/repertoire/tools-session.js +1141 -0
  515. package/dist/repertoire/tools-shell.js +154 -0
  516. package/dist/repertoire/tools-shepherd-guidance.js +171 -0
  517. package/dist/repertoire/tools-shepherd.js +325 -0
  518. package/dist/repertoire/tools-steward-policy.js +147 -0
  519. package/dist/repertoire/tools-stripe.js +224 -0
  520. package/dist/repertoire/tools-surface.js +369 -0
  521. package/dist/repertoire/tools-teams.js +50 -39
  522. package/dist/repertoire/tools-telegram-contacts.js +37 -0
  523. package/dist/repertoire/tools-travel.js +125 -0
  524. package/dist/repertoire/tools-trip.js +982 -0
  525. package/dist/repertoire/tools-unraid.js +556 -0
  526. package/dist/repertoire/tools-user-profile.js +146 -0
  527. package/dist/repertoire/tools-vault.js +40 -0
  528. package/dist/repertoire/tools-voice-local.js +198 -0
  529. package/dist/repertoire/tools-voice.js +145 -0
  530. package/dist/repertoire/tools.js +523 -156
  531. package/dist/repertoire/travel-api-client.js +360 -0
  532. package/dist/repertoire/unraid-client.js +155 -0
  533. package/dist/repertoire/unraid-restart.js +339 -0
  534. package/dist/repertoire/user-profile.js +131 -0
  535. package/dist/repertoire/vault-setup.js +246 -0
  536. package/dist/repertoire/vault-unlock.js +686 -0
  537. package/dist/rsvp/aisleplanner-client.js +354 -0
  538. package/dist/rsvp/cli.js +936 -0
  539. package/dist/rsvp/config.js +327 -0
  540. package/dist/rsvp/cutover.js +453 -0
  541. package/dist/rsvp/diagnostics.js +232 -0
  542. package/dist/rsvp/diff-renderer.js +176 -0
  543. package/dist/rsvp/habit-policy.js +110 -0
  544. package/dist/rsvp/habit-stage.js +123 -0
  545. package/dist/rsvp/incident-bundle.js +97 -0
  546. package/dist/rsvp/migration.js +204 -0
  547. package/dist/rsvp/native-habit-runner.js +630 -0
  548. package/dist/rsvp/outbound-state.js +628 -0
  549. package/dist/rsvp/query.js +118 -0
  550. package/dist/rsvp/replay.js +233 -0
  551. package/dist/rsvp/snapshot.js +232 -0
  552. package/dist/rsvp/spend-ledger.js +175 -0
  553. package/dist/scripts/claude-code-hook.js +41 -0
  554. package/dist/scripts/claude-code-stop-hook.js +47 -0
  555. package/dist/senses/a2a-entry.js +140 -0
  556. package/dist/senses/acp-server.js +386 -0
  557. package/dist/senses/attention-queue.js +186 -0
  558. package/dist/senses/await-turn-message.js +65 -0
  559. package/dist/senses/bluebubbles/active-turns.js +216 -0
  560. package/dist/senses/bluebubbles/attachment-cache.js +53 -0
  561. package/dist/senses/bluebubbles/attachment-download.js +137 -0
  562. package/dist/senses/{bluebubbles-client.js → bluebubbles/client.js} +393 -43
  563. package/dist/senses/bluebubbles/context-packet.js +207 -0
  564. package/dist/senses/bluebubbles/context-smoke.js +144 -0
  565. package/dist/senses/bluebubbles/entry.js +90 -0
  566. package/dist/senses/bluebubbles/host-account-verifier.js +88 -0
  567. package/dist/senses/{bluebubbles-inbound-log.js → bluebubbles/inbound-log.js} +21 -3
  568. package/dist/senses/bluebubbles/index.js +4054 -0
  569. package/dist/senses/bluebubbles/latest-turn.js +311 -0
  570. package/dist/senses/{bluebubbles-media.js → bluebubbles/media.js} +121 -71
  571. package/dist/senses/{bluebubbles-model.js → bluebubbles/model.js} +159 -28
  572. package/dist/senses/{bluebubbles-mutation-log.js → bluebubbles/mutation-log.js} +24 -13
  573. package/dist/senses/bluebubbles/outbound-state.js +335 -0
  574. package/dist/senses/bluebubbles/processed-log.js +133 -0
  575. package/dist/senses/bluebubbles/reaction-policy.js +47 -0
  576. package/dist/senses/bluebubbles/replay.js +284 -0
  577. package/dist/senses/{bluebubbles-runtime-state.js → bluebubbles/runtime-state.js} +30 -2
  578. package/dist/senses/bluebubbles/semantic-receipts.js +1986 -0
  579. package/dist/senses/{bluebubbles-session-cleanup.js → bluebubbles/session-cleanup.js} +1 -1
  580. package/dist/senses/bluebubbles/webhook-registration.js +257 -0
  581. package/dist/senses/bluebubbles-meta-guard.js +39 -0
  582. package/dist/senses/cli/bracketed-paste.js +82 -0
  583. package/dist/senses/cli/image-paste.js +287 -0
  584. package/dist/senses/cli/image-ref-navigation.js +75 -0
  585. package/dist/senses/cli/ink-app.js +156 -0
  586. package/dist/senses/cli/inline-diff.js +64 -0
  587. package/dist/senses/cli/input-keys.js +174 -0
  588. package/dist/senses/cli/kill-ring.js +86 -0
  589. package/dist/senses/cli/message-list.js +51 -0
  590. package/dist/senses/cli/ouro-tui.js +607 -0
  591. package/dist/senses/cli/spinner-imperative.js +135 -0
  592. package/dist/senses/cli/spinner.js +101 -0
  593. package/dist/senses/cli/status-line.js +60 -0
  594. package/dist/senses/cli/streaming-markdown.js +526 -0
  595. package/dist/senses/cli/tool-display.js +85 -0
  596. package/dist/senses/cli/tool-render.js +85 -0
  597. package/dist/senses/cli/tui-store.js +241 -0
  598. package/dist/senses/cli/virtual-list.js +35 -0
  599. package/dist/senses/cli-entry.js +60 -8
  600. package/dist/senses/cli.js +570 -309
  601. package/dist/senses/commands.js +66 -3
  602. package/dist/senses/context-packet-ledger.js +371 -0
  603. package/dist/senses/context-packets.js +154 -0
  604. package/dist/senses/habit-lifecycle-guard.js +24 -0
  605. package/dist/senses/habit-turn-message.js +160 -0
  606. package/dist/senses/ingress-evidence.js +2 -0
  607. package/dist/senses/mail-entry.js +66 -0
  608. package/dist/senses/mail.js +524 -0
  609. package/dist/senses/pipeline.js +1192 -204
  610. package/dist/senses/private-runtime-worker.js +857 -0
  611. package/dist/senses/private-runtime.js +1622 -0
  612. package/dist/senses/proactive-content-guard.js +51 -0
  613. package/dist/senses/root-host-approval-port.js +345 -0
  614. package/dist/senses/root-host-approval-runtime.js +393 -0
  615. package/dist/senses/sanctuary-authority-resident.js +102 -0
  616. package/dist/senses/sanctuary-download-credit-presentation.js +31 -0
  617. package/dist/senses/sanctuary-full-visibility-contract.js +302 -0
  618. package/dist/senses/sanctuary-grounding.js +118 -0
  619. package/dist/senses/sanctuary-health-acceptance-probe-entry.js +12 -0
  620. package/dist/senses/sanctuary-health-acceptance-probe.js +786 -0
  621. package/dist/senses/sanctuary-health-runner.js +106 -0
  622. package/dist/senses/sanctuary-health.js +442 -0
  623. package/dist/senses/sanctuary-house.js +97 -0
  624. package/dist/senses/sanctuary-install-state-contract.js +169 -0
  625. package/dist/senses/sanctuary-interactive-control.js +410 -0
  626. package/dist/senses/sanctuary-media-catalog-contract.js +268 -0
  627. package/dist/senses/sanctuary-media-optimization.js +517 -0
  628. package/dist/senses/sanctuary-runtime.js +390 -0
  629. package/dist/senses/sanctuary-sab.js +85 -0
  630. package/dist/senses/sanctuary-storage-optimization-contract.js +32 -0
  631. package/dist/senses/shared-turn.js +832 -0
  632. package/dist/senses/shepherd/agent-presentation.js +88 -0
  633. package/dist/senses/shepherd/attention-page.js +166 -0
  634. package/dist/senses/shepherd/attention-sidebar.js +76 -0
  635. package/dist/senses/shepherd/attention.js +295 -0
  636. package/dist/senses/shepherd/checkin-config.js +76 -0
  637. package/dist/senses/shepherd/checkin-store.js +386 -0
  638. package/dist/senses/shepherd/client.js +196 -0
  639. package/dist/senses/shepherd/cmux.js +483 -0
  640. package/dist/senses/shepherd/composer.js +336 -0
  641. package/dist/senses/shepherd/copilot-app.js +648 -0
  642. package/dist/senses/shepherd/feedback-reply.js +108 -0
  643. package/dist/senses/shepherd/feedback-source.js +20 -0
  644. package/dist/senses/shepherd/feedback-store.js +331 -0
  645. package/dist/senses/shepherd/feedback-submit.js +182 -0
  646. package/dist/senses/shepherd/feedback-worker.js +208 -0
  647. package/dist/senses/shepherd/guidance.js +273 -0
  648. package/dist/senses/shepherd/herdr.js +427 -0
  649. package/dist/senses/shepherd/host.js +17 -0
  650. package/dist/senses/shepherd/human-decision-guard.js +29 -0
  651. package/dist/senses/shepherd/judge-evaluation-command.js +183 -0
  652. package/dist/senses/shepherd/judge-evaluation-entry.js +13 -0
  653. package/dist/senses/shepherd/judge-evaluation.js +155 -0
  654. package/dist/senses/shepherd/judge.js +210 -0
  655. package/dist/senses/shepherd/notice-outbox.js +262 -0
  656. package/dist/senses/shepherd/observation-publisher.js +320 -0
  657. package/dist/senses/shepherd/operations-defaults.js +86 -0
  658. package/dist/senses/shepherd/operations.js +159 -0
  659. package/dist/senses/shepherd/owner-notice.js +489 -0
  660. package/dist/senses/shepherd/prompt-state.js +60 -0
  661. package/dist/senses/shepherd/recovery.js +543 -0
  662. package/dist/senses/shepherd/redact.js +66 -0
  663. package/dist/senses/shepherd/returns.js +79 -0
  664. package/dist/senses/shepherd/sense.js +849 -0
  665. package/dist/senses/shepherd/shepherd-judge-corpus-v1.json +170 -0
  666. package/dist/senses/shepherd/surface.js +123 -0
  667. package/dist/senses/shepherd/target-binding.js +13 -0
  668. package/dist/senses/shepherd/terminal-process.js +147 -0
  669. package/dist/senses/shepherd-entry.js +98 -0
  670. package/dist/senses/surface-tool.js +109 -0
  671. package/dist/senses/teams-entry.js +68 -8
  672. package/dist/senses/teams-image.js +119 -0
  673. package/dist/senses/teams.js +998 -337
  674. package/dist/senses/telegram-admission.js +718 -0
  675. package/dist/senses/telegram-approval-runtime.js +793 -0
  676. package/dist/senses/telegram-attachments.js +186 -0
  677. package/dist/senses/telegram-audit-ledger.js +220 -0
  678. package/dist/senses/telegram-authority-transport.js +231 -0
  679. package/dist/senses/telegram-client.js +1755 -0
  680. package/dist/senses/telegram-effect-adapter.js +904 -0
  681. package/dist/senses/telegram-entry.js +83 -0
  682. package/dist/senses/telegram-handoff.js +154 -0
  683. package/dist/senses/telegram.js +2474 -0
  684. package/dist/senses/trust-gate.js +104 -9
  685. package/dist/senses/voice/audio-playback.js +237 -0
  686. package/dist/senses/voice/audio-routing.js +119 -0
  687. package/dist/senses/voice/call-auth.js +250 -0
  688. package/dist/senses/voice/elevenlabs.js +202 -0
  689. package/dist/senses/voice/floor-control.js +431 -0
  690. package/dist/senses/voice/floor-controller.js +115 -0
  691. package/dist/senses/voice/golden-path.js +116 -0
  692. package/dist/senses/voice/index.js +29 -0
  693. package/dist/senses/voice/local-audio-control.js +344 -0
  694. package/dist/senses/voice/local-audio-devices.js +516 -0
  695. package/dist/senses/voice/local-audio-disclosure.js +174 -0
  696. package/dist/senses/voice/local-audio-identity.js +103 -0
  697. package/dist/senses/voice/local-audio-join.js +325 -0
  698. package/dist/senses/voice/local-audio-mute.js +199 -0
  699. package/dist/senses/voice/local-audio-notify.js +124 -0
  700. package/dist/senses/voice/local-audio-prompts.js +116 -0
  701. package/dist/senses/voice/local-audio-transport.js +427 -0
  702. package/dist/senses/voice/local-audio.js +281 -0
  703. package/dist/senses/voice/local-media-stream.js +145 -0
  704. package/dist/senses/voice/meeting.js +113 -0
  705. package/dist/senses/voice/outbound.js +190 -0
  706. package/dist/senses/voice/phone.js +33 -0
  707. package/dist/senses/voice/playback.js +139 -0
  708. package/dist/senses/voice/process-guards.js +51 -0
  709. package/dist/senses/voice/realtime-eval.js +496 -0
  710. package/dist/senses/voice/realtime-tool-schema.js +195 -0
  711. package/dist/senses/voice/realtime-trace.js +531 -0
  712. package/dist/senses/voice/transcript.js +70 -0
  713. package/dist/senses/voice/turn.js +191 -0
  714. package/dist/senses/voice/twilio-phone-runtime.js +836 -0
  715. package/dist/senses/voice/twilio-phone.js +5454 -0
  716. package/dist/senses/voice/types.js +2 -0
  717. package/dist/senses/voice/whisper.js +161 -0
  718. package/dist/senses/voice-entry.js +83 -0
  719. package/dist/senses/voice-realtime-eval-command.js +99 -0
  720. package/dist/senses/voice-realtime-eval-entry.js +21 -0
  721. package/dist/senses/voice-twilio-entry.js +87 -0
  722. package/dist/trips/core.js +138 -0
  723. package/dist/trips/store.js +265 -0
  724. package/dist/util/frontmatter.js +69 -0
  725. package/npm-shrinkwrap.json +8342 -0
  726. package/package.json +76 -13
  727. package/skills/agent-commerce.md +113 -0
  728. package/skills/browser-navigation.md +117 -0
  729. package/skills/commerce-setup-guide.md +116 -0
  730. package/skills/commerce-setup.md +84 -0
  731. package/skills/configure-dev-tools.md +99 -0
  732. package/skills/travel-planning.md +138 -0
  733. package/dist/heart/daemon/auth-flow.js +0 -430
  734. package/dist/heart/daemon/ouro-path-installer.js +0 -260
  735. package/dist/heart/daemon/ouro-version-manager.js +0 -171
  736. package/dist/heart/daemon/update-hooks.js +0 -138
  737. package/dist/heart/obligations.js +0 -191
  738. package/dist/heart/safe-workspace.js +0 -381
  739. package/dist/heart/session-recall.js +0 -116
  740. package/dist/mind/friends/channel.js +0 -84
  741. package/dist/mind/friends/group-context.js +0 -144
  742. package/dist/mind/friends/resolver.js +0 -84
  743. package/dist/mind/friends/store-file.js +0 -190
  744. package/dist/mind/friends/store.js +0 -4
  745. package/dist/mind/friends/tokens.js +0 -26
  746. package/dist/mind/friends/trust-explanation.js +0 -74
  747. package/dist/mind/friends/types.js +0 -29
  748. package/dist/repertoire/tasks/board.js +0 -134
  749. package/dist/repertoire/tasks/index.js +0 -224
  750. package/dist/repertoire/tasks/lifecycle.js +0 -80
  751. package/dist/repertoire/tasks/middleware.js +0 -65
  752. package/dist/repertoire/tasks/parser.js +0 -173
  753. package/dist/repertoire/tasks/scanner.js +0 -132
  754. package/dist/repertoire/tasks/transitions.js +0 -144
  755. package/dist/senses/bluebubbles-entry.js +0 -13
  756. package/dist/senses/bluebubbles.js +0 -1181
  757. package/dist/senses/debug-activity.js +0 -154
  758. package/dist/senses/inner-dialog-worker.js +0 -90
  759. package/dist/senses/inner-dialog.js +0 -525
  760. package/subagents/README.md +0 -7
  761. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/basilisk.md +0 -0
  762. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jafar.md +0 -0
  763. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jormungandr.md +0 -0
  764. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/kaa.md +0 -0
  765. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/medusa.md +0 -0
  766. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +0 -0
  767. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/nagini.md +0 -0
  768. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/ouroboros.md +0 -0
  769. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/python.md +0 -0
  770. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/quetzalcoatl.md +0 -0
  771. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/sir-hiss.md +0 -0
  772. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
  773. /package/dist/{repertoire/tasks/types.js → heart/attachments/sources/adapter.js} +0 -0
  774. /package/dist/heart/{daemon → hatch}/hatch-animation.js +0 -0
  775. /package/dist/heart/{daemon → hatch}/specialist-orchestrator.js +0 -0
  776. /package/dist/heart/{daemon → versioning}/ouro-uti.js +0 -0
  777. /package/dist/heart/{daemon → versioning}/wrapper-publish-guard.js +0 -0
@@ -0,0 +1,1039 @@
1
+ #!/bin/bash
2
+ # Moves the Butler's bundle from the hourly host backup onto bundle sync. Runs as root on the Unraid host.
3
+ #
4
+ # sanctuary-sync-cutover.sh [--dry-run] read the before-state, print it and save it to bundle-backup/cutover-before-<time>.txt (the default)
5
+ # sanctuary-sync-cutover.sh --apply cut over; any failure rolls back to that saved before-state
6
+ # sanctuary-sync-cutover.sh --rollback put the hourly backup back, from the before-state the apply recorded
7
+ # sanctuary-sync-cutover.sh --verify-live after --apply: the sync path reaches GitHub, the key stays unreadable, a stalled fetch ends
8
+ #
9
+ # --apply and --rollback run detached (a new session, all output to bundle-backup/cutover-<time>.log). This process only
10
+ # follows that log and exits with the detached run's status, so a dropped ssh session or a closed terminal never stops a
11
+ # cutover or a rollback halfway. Once a rollback starts it ignores INT, TERM and HUP, runs every step, and when a step fails
12
+ # it writes bundle-backup/cutover-ROLLBACK-INCOMPLETE and says whether the Butler is running. --rollback can be run again.
13
+ #
14
+ # Every git call names its repository and work tree (`git --git-dir=... --work-tree=...`), so git never discovers the
15
+ # repository and never refuses it for dubious ownership after the move or the chown. Root's git calls also run no hooks,
16
+ # no fsmonitor and no global excludes file, and `git status` takes no optional locks, so root never rewrites the index
17
+ # behind the Butler.
18
+ #
19
+ # From step 13 the Butler owns .git, so its config, hooks and info/attributes may name any command (a clean filter, an
20
+ # [include], an ext:: URL). Root runs no git on it after that: --verify-live runs git only inside the container as the
21
+ # Butler, an apply refuses while .git is in the bundle, and --rollback first moves .git back, hands it to root and puts
22
+ # back config, hooks/ and info/attributes from the root-owned snapshot the apply took before step 1. It refuses a repo.git
23
+ # that holds any symbolic link, which could carry root's writes onto the host. Only on a restored repo.git that holds no
24
+ # links does root run git, and only then does the rollback bring back the hourly backup.sh cron, which runs git as root
25
+ # with hooks on. Otherwise the rollback ends in ROLLBACK-INCOMPLETE with the cron still off.
26
+ set -Eeuo pipefail
27
+ umask 022
28
+
29
+ # The bundle's GitHub repository. It is renamed to butler.ouro at R4.
30
+ BUNDLE_REPO_OWNER=arimendelow
31
+ BUNDLE_REPO_NAME=sanctuary.ouro
32
+ BUNDLE_REMOTE="git@github.com:${BUNDLE_REPO_OWNER}/${BUNDLE_REPO_NAME}.git"
33
+ BUNDLE_BRANCH=main
34
+ AGENT_NAME=sanctuary
35
+ # backup.sh's commit identity and the fetch refspec the moved repository must have.
36
+ BACKUP_COMMIT_NAME="Sanctuary backup"
37
+ BACKUP_COMMIT_EMAIL=16390116+arimendelow@users.noreply.github.com
38
+ DEFAULT_FETCH='+refs/heads/*:refs/remotes/origin/*'
39
+ PSYCHE_FILES="ASPIRATIONS.md IDENTITY.md LORE.md SOUL.md TACIT.md"
40
+
41
+ CONTAINER=ouro-butler
42
+ CONTAINER_BUNDLE=/home/ouro/AgentBundles/sanctuary.ouro
43
+ CLI_ENTRY=/opt/ouro/dist/heart/daemon/ouro-entry.js
44
+ GATE_ENTRY=/opt/ouro/deploy/unraid/sanctuary-replay-gate.mjs
45
+ SYNC_SSH_WRAPPER=/usr/local/libexec/ouro-sync-ssh
46
+ CONTAINER_KEY=/run/ouro-sync/deploy_key
47
+ # The image's ouro-sync user, which alone reads the key and runs sync's ssh. The stall probe drops its port-22 traffic.
48
+ SYNC_USER=ouro-sync
49
+ SYNC_CONTAINER_UID=10002
50
+ PROBE_COMMENT=sanctuary-sync-cutover-probe
51
+ AUDIT_ICON=https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png
52
+ # git status and git add -A --dry-run on the live bundle must each stay under this (M5).
53
+ TIMING_LIMIT_MS=300
54
+ # A stalled fetch must end on its own within this (the wrapper's ssh timeouts are about 20 s).
55
+ STALL_LIMIT_S=25
56
+
57
+ # Tests point every host path into a temp tree. Nothing else reads this.
58
+ TEST_ROOT=${OURO_CUTOVER_ROOT:-}
59
+ APPDATA=$TEST_ROOT/mnt/user/appdata/ouro-butler
60
+ BUNDLE=$APPDATA/agent/sanctuary.ouro
61
+ BACKUP=$APPDATA/bundle-backup
62
+ BACKUP_GIT=$BACKUP/repo.git
63
+ BACKUP_KEY=$BACKUP/deploy_key
64
+ KNOWN_HOSTS=$BACKUP/known_hosts
65
+ LOCK_FILE=$BACKUP/lock
66
+ CUTOVER_LOCK=$BACKUP/cutover.lock
67
+ MARKER=$BACKUP/cutover-done
68
+ APPLYING=$BACKUP/cutover-applying
69
+ INCOMPLETE=$BACKUP/cutover-ROLLBACK-INCOMPLETE
70
+ CRON_DISABLED=$BACKUP/sanctuary-bundle-backup.cron.disabled
71
+ # What of repo.git can make git run a command, saved as root before step 1 and put back first on rollback (C1).
72
+ GIT_SNAPSHOT_PREFIX=$BACKUP/cutover-git-snapshot
73
+ SYNC_DIR=$APPDATA/sync
74
+ SYNC_KEY=$SYNC_DIR/deploy_key
75
+ CRON_FILE=$TEST_ROOT/boot/config/plugins/dynamix/sanctuary-bundle-backup.cron
76
+ CRON_TABLE=$TEST_ROOT/etc/cron.d/root
77
+ HOST_NODE=$TEST_ROOT/usr/local/bin/node
78
+
79
+ # The Butler's uid (owns .git), ouro-sync's uid (owns the key) and root. A test that is not root owns all three.
80
+ BUTLER_OWNER=10001:10001
81
+ SYNC_OWNER=10002:10002
82
+ ROOT_OWNER=0:0
83
+ if [ -n "$TEST_ROOT" ] && [ -n "${OURO_CUTOVER_OWNER:-}" ]; then
84
+ BUTLER_OWNER=$OURO_CUTOVER_OWNER
85
+ SYNC_OWNER=$OURO_CUTOVER_OWNER
86
+ ROOT_OWNER=$OURO_CUTOVER_OWNER
87
+ fi
88
+ if [ -n "$TEST_ROOT" ]; then STALL_LIMIT_S=${OURO_CUTOVER_STALL_LIMIT_S:-$STALL_LIMIT_S}; fi
89
+ BUTLER_UID=${BUTLER_OWNER%%:*}
90
+ SYNC_UID=${SYNC_OWNER%%:*}
91
+ SYNC_GID=${SYNC_OWNER##*:}
92
+
93
+ # Secret-looking names. The gate's SECRET_PATH is the same pattern; a test pins the two together.
94
+ SECRET_NAME_PATTERN='credential|secret|token|\.key$|\.pem$|(^|/)\.env($|\.)'
95
+
96
+ say() { printf '%s\n' "$*"; }
97
+ die() { printf 'cutover: %s\n' "$*" >&2; exit 1; }
98
+
99
+ # Git on the backup repository (before step 5) and on the bundle's own .git (from step 5). No hooks, no fsmonitor, and no
100
+ # global excludes file, so root's view of what is ignored is the repository's own (M5).
101
+ GIT_SAFE="-c core.hooksPath=/dev/null -c core.fsmonitor=false -c core.excludesFile=/dev/null"
102
+ # shellcheck disable=SC2086
103
+ backup_git() { git $GIT_SAFE --git-dir="$BACKUP_GIT" --work-tree="$BUNDLE" "$@"; }
104
+ # shellcheck disable=SC2086
105
+ bundle_git() { git $GIT_SAFE --git-dir="$BUNDLE/.git" --work-tree="$BUNDLE" "$@"; }
106
+ # backup.sh's ssh, with the host key it already accepted into bundle-backup/known_hosts now required (I2).
107
+ BACKUP_SSH="ssh -i $BACKUP_KEY -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$KNOWN_HOSTS -o BatchMode=yes"
108
+
109
+ # A config value, or "(unset)" when git says it is unset (exit 1). Any other failure is a failure (M9).
110
+ config_value() {
111
+ local gitfn=$1 key=$2 value rc=0
112
+ value=$("$gitfn" config --get "$key") || rc=$?
113
+ case "$rc" in
114
+ 0) printf '%s' "$value" ;;
115
+ 1) printf '(unset)' ;;
116
+ *) die "git config --get $key failed (exit $rc)" ;;
117
+ esac
118
+ }
119
+
120
+ # Every value of a multi-valued key, joined with " ; ", or "(unset)".
121
+ config_all() {
122
+ local gitfn=$1 key=$2 value rc=0
123
+ value=$("$gitfn" config --get-all "$key") || rc=$?
124
+ case "$rc" in
125
+ 0) printf '%s' "$value" | awk 'NR > 1 { printf " ; " } { printf "%s", $0 }' ;;
126
+ 1) printf '(unset)' ;;
127
+ *) die "git config --get-all $key failed (exit $rc)" ;;
128
+ esac
129
+ }
130
+
131
+ # Sets a config value, or unsets it for "(unset)".
132
+ restore_config() {
133
+ local gitfn=$1 key=$2 value=$3
134
+ if [ "$value" = "(unset)" ]; then
135
+ "$gitfn" config --unset-all "$key" || [ $? -eq 5 ]
136
+ else
137
+ "$gitfn" config "$key" "$value"
138
+ fi
139
+ }
140
+
141
+ # One "key: value" line from a file.
142
+ file_value() {
143
+ awk -v k="$2: " 'index($0, k) == 1 { print substr($0, length(k) + 1); found = 1; exit } END { exit found ? 0 : 1 }' "$1"
144
+ }
145
+
146
+ # One saved value from the before-state.
147
+ before_value() {
148
+ file_value "$BEFORE" "$1" || die "$(basename "$BEFORE") has no \"$1\" line"
149
+ }
150
+
151
+ container_image() {
152
+ local image
153
+ image=$(docker inspect --format '{{.Image}}' "$CONTAINER")
154
+ case "$image" in
155
+ sha256:*) ;;
156
+ *) die "the container's image id is not a sha256 id: $image" ;;
157
+ esac
158
+ [ "${#image}" -eq 71 ] || die "the container's image id is not a sha256 id: $image"
159
+ printf '%s' "$image"
160
+ }
161
+
162
+ # "true", "false", or "unknown" when docker cannot say.
163
+ container_running() {
164
+ docker inspect --format '{{.State.Running}}' "$CONTAINER" 2>/dev/null || printf 'unknown'
165
+ }
166
+
167
+ # The exclude list from the release's own code, into $1.
168
+ print_exclude() {
169
+ local image=$1 out=$2
170
+ docker run --rm --pull=never --network none --entrypoint node "$image" "$CLI_ENTRY" sanctuary git-exclude --print >"$out.raw"
171
+ # One entry per line and one final newline, exactly as the boot writes it.
172
+ awk 'NF { print }' "$out.raw" >"$out"
173
+ rm -f -- "$out.raw"
174
+ grep -qx 'state/' "$out" || die "the release's exclude list has no state/ entry"
175
+ grep -qx '\*\*/\*\.key' "$out" || die "the release's exclude list has no **/*.key entry"
176
+ }
177
+
178
+ # Milliseconds for git status and git add -A --dry-run. Arguments: the git dir, the work tree and the new exclude list, all
179
+ # of which may be read-only. Both run on a private copy of the index (git add --dry-run still takes index.lock), and status
180
+ # takes no optional lock, so nothing under the repository is written (I1). No single quotes below: a test runs this text.
181
+ # shellcheck disable=SC2016
182
+ MEASURE_GIT_SCRIPT='
183
+ repo=$1 w=$2 exclude=$3
184
+ tmp=$(mktemp -d) || exit 1
185
+ cleanup() { rm -rf "$tmp"; }
186
+ trap cleanup EXIT
187
+ if [ -f "$repo/index" ]; then cp "$repo/index" "$tmp/index" || exit 1; fi
188
+ g() { GIT_OPTIONAL_LOCKS=0 GIT_INDEX_FILE="$tmp/index" git -c safe.directory="*" -c core.hooksPath=/dev/null -c core.fsmonitor=false -c core.excludesFile="$exclude" --git-dir="$repo" --work-tree="$w" "$@"; }
189
+ a=$(date +%s%N); g status --porcelain >/dev/null || exit 1
190
+ b=$(date +%s%N); g add -A --dry-run >/dev/null || exit 1
191
+ c=$(date +%s%N); echo "$(( (b - a) / 1000000 )) $(( (c - b) / 1000000 ))"
192
+ '
193
+
194
+ # The timings inside the release image as the Butler's uid, with the repository and the live bundle mounted read-only.
195
+ measure_git() {
196
+ local image=$1 exclude=$2
197
+ docker run --rm --pull=never --network none --user 10001:10001 --entrypoint sh \
198
+ --mount "type=bind,src=$BACKUP_GIT,dst=/cutover/repo.git,readonly" \
199
+ --mount "type=bind,src=$BUNDLE,dst=$CONTAINER_BUNDLE,readonly" \
200
+ --mount "type=bind,src=$exclude,dst=/cutover/exclude,readonly" \
201
+ "$image" -c "$MEASURE_GIT_SCRIPT" sh /cutover/repo.git "$CONTAINER_BUNDLE" /cutover/exclude
202
+ }
203
+
204
+ # Paths in the tracked list ($1, one per line) that look like secrets.
205
+ secret_paths() { grep -Ei "$SECRET_NAME_PATTERN" "$1" || true; }
206
+
207
+ # The paths in a `git status --porcelain --untracked-files=all` file that sync must never commit, from the replay gate's own
208
+ # scan inside the release image, so step 10 and the gate cannot drift (I3).
209
+ status_problems() {
210
+ local image=$1 status=$2
211
+ docker run --rm -i --pull=never --network none --entrypoint node "$image" "$GATE_ENTRY" status-scan <"$status"
212
+ }
213
+
214
+ # The owner ("uid:gid") and mode ("755") of a path, read without following a link.
215
+ owner_and_mode() {
216
+ "$HOST_NODE" -e '
217
+ const s = require("node:fs").lstatSync(process.argv[1]);
218
+ process.stdout.write(s.uid + ":" + s.gid + " " + (s.mode & 0o7777).toString(8));
219
+ ' "$1"
220
+ }
221
+
222
+ # agent.json's "sync" value as JSON, or "(absent)".
223
+ agent_sync_value() {
224
+ "$HOST_NODE" -e '
225
+ const fs = require("node:fs");
226
+ const fd = fs.openSync(process.argv[1], fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
227
+ if (!fs.fstatSync(fd).isFile()) throw new Error(process.argv[1] + " is not a regular file");
228
+ const agent = JSON.parse(fs.readFileSync(fd, "utf8"));
229
+ process.stdout.write(Object.prototype.hasOwnProperty.call(agent, "sync") ? JSON.stringify(agent.sync) : "(absent)");
230
+ ' "$BUNDLE/agent.json"
231
+ }
232
+
233
+ # Edits agent.json's "sync" through a temp file and a rename, keeping its owner and mode. "enable" turns sync on for the
234
+ # branch; "restore <json|(absent)>" puts back exactly what the before-state recorded.
235
+ edit_agent_sync() {
236
+ "$HOST_NODE" -e '
237
+ const fs = require("node:fs"), path = require("node:path");
238
+ const [file, action, value, branch] = process.argv.slice(1);
239
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
240
+ const stat = fs.fstatSync(fd);
241
+ if (!stat.isFile()) throw new Error(file + " is not a regular file");
242
+ const agent = JSON.parse(fs.readFileSync(fd, "utf8"));
243
+ fs.closeSync(fd);
244
+ if (action === "enable") {
245
+ const sync = agent.sync && typeof agent.sync === "object" ? agent.sync : {};
246
+ agent.sync = { ...sync, enabled: true, branch: sync.branch ?? branch };
247
+ } else if (value === "(absent)") {
248
+ delete agent.sync;
249
+ } else {
250
+ agent.sync = JSON.parse(value);
251
+ }
252
+ const temp = path.join(path.dirname(file), ".agent.json.cutover-" + process.pid);
253
+ const out = fs.openSync(temp, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);
254
+ try {
255
+ fs.writeFileSync(out, JSON.stringify(agent, null, 2) + "\n");
256
+ fs.fchownSync(out, stat.uid, stat.gid);
257
+ fs.fchmodSync(out, stat.mode & 0o7777);
258
+ fs.fsyncSync(out);
259
+ } finally { fs.closeSync(out); }
260
+ fs.renameSync(temp, file);
261
+ ' "$BUNDLE/agent.json" "$1" "${2:-}" "$BUNDLE_BRANCH"
262
+ }
263
+
264
+ # Runs `docker exec "$@"`, its stdout in $EXEC_OUT (default $WORK/exec.out) and its status in $EXEC_STATUS. Dies when the
265
+ # check itself could not run (exit 125, 126 or 127, or no such container), so that is never read as the result (M3).
266
+ exec_check() {
267
+ EXEC_STATUS=0
268
+ docker exec "$@" >"${EXEC_OUT:-$WORK/exec.out}" 2>"$WORK/exec.err" || EXEC_STATUS=$?
269
+ if { [ "$EXEC_STATUS" -ge 125 ] && [ "$EXEC_STATUS" -le 127 ]; } || grep -qiE 'no such container|is not running' "$WORK/exec.err"; then
270
+ die "the check could not run: docker exec $* exited $EXEC_STATUS: $(head -c 300 "$WORK/exec.err" | tr '\n' ' ')"
271
+ fi
272
+ }
273
+
274
+ # The check that the Butler's uid cannot read the key: `cat` and `test -r` must both exit 1.
275
+ assert_key_unreadable() {
276
+ EXEC_OUT=/dev/null exec_check -u 10001 "$CONTAINER" cat "$CONTAINER_KEY"
277
+ case "$EXEC_STATUS" in
278
+ 0) die "the Butler's uid can read $CONTAINER_KEY" ;;
279
+ 1) ;;
280
+ *) die "cat $CONTAINER_KEY as the Butler's uid exited $EXEC_STATUS, not 1" ;;
281
+ esac
282
+ exec_check -u 10001 "$CONTAINER" test -r "$CONTAINER_KEY"
283
+ case "$EXEC_STATUS" in
284
+ 0) die "the Butler's uid can read $CONTAINER_KEY" ;;
285
+ 1) ;;
286
+ *) die "test -r $CONTAINER_KEY as the Butler's uid exited $EXEC_STATUS, not 1" ;;
287
+ esac
288
+ }
289
+
290
+ # ---------- dry run ----------
291
+
292
+ # Refuses a state the cutover cannot start from. Changes nothing.
293
+ refuse_unless_ready() {
294
+ [ -d "$BACKUP_GIT" ] || die "$BACKUP_GIT is missing"
295
+ [ -d "$BUNDLE" ] || die "$BUNDLE is missing"
296
+ [ ! -e "$INCOMPLETE" ] || die "a rollback did not finish ($INCOMPLETE); run --rollback again"
297
+ [ ! -e "$APPLYING" ] || die "an apply is running or was interrupted ($APPLYING); run --rollback"
298
+ [ ! -e "$BUNDLE/.git" ] || die "$BUNDLE/.git already exists"
299
+ [ ! -e "$MARKER" ] || die "the cutover is already done ($MARKER)"
300
+ [ -f "$CRON_FILE" ] || die "the backup cron file $CRON_FILE is missing"
301
+ [ -f "$BACKUP_KEY" ] || die "the backup deploy key $BACKUP_KEY is missing"
302
+ [ -f "$KNOWN_HOSTS" ] || die "$KNOWN_HOSTS is missing; backup.sh's pushes fill it, and the cutover's ssh requires it"
303
+ }
304
+
305
+ # Reads the before-state, prints it and saves it to a new timestamped file, never overwriting one. Changes nothing else.
306
+ # Sets BEFORE, and leaves the exclude list in $WORK/exclude.
307
+ preflight() {
308
+ refuse_unless_ready
309
+ local image branch upstream remote merge stash tracked timings status_ms add_ms excluded secrets out
310
+ local origin_url fetch bare worktree sshcmd ucache manyfiles agent_sync repo_stat repo_owner repo_mode key_state
311
+ image=$(container_image)
312
+ print_exclude "$image" "$WORK/exclude"
313
+ chmod 0644 "$WORK/exclude"
314
+ timings=$(measure_git "$image" "$WORK/exclude")
315
+ read -r status_ms add_ms <<<"$timings"
316
+ case "$status_ms:$add_ms" in
317
+ *[!0-9:]* | :* | *:) die "the git timing inside the image printed \"$timings\"" ;;
318
+ esac
319
+ branch=$(backup_git symbolic-ref --short HEAD) || die "the backup repository's HEAD is not on a branch"
320
+ remote=$(config_value backup_git "branch.$branch.remote")
321
+ merge=$(config_value backup_git "branch.$branch.merge")
322
+ upstream=$(backup_git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null) || upstream="(none)"
323
+ stash=$(backup_git rev-list --walk-reflogs --count refs/stash 2>/dev/null) || stash=0
324
+ origin_url=$(config_value backup_git remote.origin.url)
325
+ fetch=$(config_all backup_git remote.origin.fetch)
326
+ bare=$(config_value backup_git core.bare)
327
+ worktree=$(config_value backup_git core.worktree)
328
+ sshcmd=$(config_value backup_git core.sshCommand)
329
+ ucache=$(config_value backup_git core.untrackedCache)
330
+ manyfiles=$(config_value backup_git feature.manyFiles)
331
+ agent_sync=$(agent_sync_value)
332
+ repo_stat=$(owner_and_mode "$BACKUP_GIT")
333
+ repo_owner=${repo_stat% *}
334
+ repo_mode=${repo_stat#* }
335
+ if [ -e "$SYNC_KEY" ]; then key_state=present; else key_state=absent; fi
336
+ tracked=$WORK/tracked
337
+ backup_git ls-files >"$tracked"
338
+ excluded=$(backup_git ls-files -ci --exclude-per-directory=.gitignore --exclude-from="$WORK/exclude")
339
+ secrets=$(secret_paths "$tracked")
340
+ out=$WORK/before
341
+ {
342
+ say "# Bundle sync cutover: the before-state, read $(date -u +%Y-%m-%dT%H:%M:%SZ)"
343
+ say "origin.url: $origin_url"
344
+ say "target.url: $BUNDLE_REMOTE"
345
+ say "remote.origin.fetch: $fetch"
346
+ say "image: $image"
347
+ say "branch: $branch"
348
+ say "upstream: $upstream"
349
+ say "branch.remote: $remote"
350
+ say "branch.merge: $merge"
351
+ say "core.bare: $bare"
352
+ say "core.worktree: $worktree"
353
+ say "core.sshCommand: $sshcmd"
354
+ say "core.untrackedCache: $ucache"
355
+ say "feature.manyFiles: $manyfiles"
356
+ say "repo.git owner: $repo_owner"
357
+ say "repo.git mode: $repo_mode"
358
+ say "agent.json sync: $agent_sync"
359
+ say "sync key: $key_state"
360
+ say "known_hosts: $KNOWN_HOSTS"
361
+ say "stash count: $stash"
362
+ say "git status ms: $status_ms"
363
+ say "git add -A --dry-run ms: $add_ms"
364
+ say "tracked files: $(wc -l <"$tracked" | tr -d ' ')"
365
+ say "tracked paths the new exclude matches: $(printf '%s' "$excluded" | grep -c . || true)"
366
+ [ -z "$excluded" ] || printf '%s\n' "$excluded" | sed 's/^/ /'
367
+ say "secret-named tracked paths: $(printf '%s' "$secrets" | grep -c . || true)"
368
+ [ -z "$secrets" ] || printf '%s\n' "$secrets" | sed 's/^/ /'
369
+ say "cron file: $CRON_FILE"
370
+ sed 's/^/ /' "$CRON_FILE"
371
+ if [ -f "$BACKUP_GIT/info/exclude" ]; then
372
+ say "info/exclude:"
373
+ sed 's/^/ | /' "$BACKUP_GIT/info/exclude"
374
+ say "info/exclude.base64: $(base64 <"$BACKUP_GIT/info/exclude" | tr -d '\n')"
375
+ else
376
+ say "info/exclude: (absent)"
377
+ say "info/exclude.base64: (absent)"
378
+ fi
379
+ } >"$out"
380
+ cat "$out"
381
+ # A new file each time; noclobber refuses to replace one that exists (M10).
382
+ BEFORE=$BACKUP/cutover-before-$(date -u +%Y%m%dT%H%M%SZ)-$$.txt
383
+ (set -C; cat "$out" >"$BEFORE") || die "could not write $BEFORE"
384
+ local problems=0
385
+ if [ "$status_ms" -gt "$TIMING_LIMIT_MS" ] || [ "$add_ms" -gt "$TIMING_LIMIT_MS" ]; then
386
+ say "FAIL: git status took $status_ms ms and git add -A --dry-run $add_ms ms; each must be at most $TIMING_LIMIT_MS ms" >&2
387
+ problems=1
388
+ fi
389
+ if [ -n "$secrets" ]; then
390
+ say "FAIL: tracked paths look like secrets; untrack them and rotate what they hold before the cutover" >&2
391
+ problems=1
392
+ fi
393
+ [ "$problems" -eq 0 ] || die "the dry run found problems; nothing was changed"
394
+ say "dry run ok; saved $BEFORE"
395
+ }
396
+
397
+ # ---------- rollback ----------
398
+
399
+ ROLLBACK_FAILED=""
400
+
401
+ # Runs one rollback step and records its result. A failed step never stops the steps after it.
402
+ rollback_step() {
403
+ local name=$1
404
+ shift
405
+ if "$@"; then
406
+ say "rollback: $name: ok"
407
+ else
408
+ say "rollback: $name: FAILED"
409
+ ROLLBACK_FAILED="${ROLLBACK_FAILED:+$ROLLBACK_FAILED, }$name"
410
+ fi
411
+ }
412
+
413
+ # Undoes the cutover from the before-state in $BEFORE. Every step reads the state it finds, so a rollback can run again
414
+ # after one that was cut short. $1 is the step the apply reached (15 for --rollback): the container is stopped only when
415
+ # the apply started it, and started again only when the apply stopped it.
416
+ rollback_from() {
417
+ local reached=$1 running
418
+ # Nothing interrupts a rollback: a second Ctrl-C, a TERM or a lost terminal is ignored, here and in every child (C1).
419
+ trap '' INT TERM HUP PIPE
420
+ set +e
421
+ ROLLBACK_FAILED=""
422
+ # Read now: the marker goes, and the INCOMPLETE record is rewritten, before the rollback ends.
423
+ ROLLBACK_SNAPSHOT=$(recorded_value "git snapshot") || ROLLBACK_SNAPSHOT=""
424
+ ROLLBACK_SNAPSHOT_HOOKS=$(recorded_value "git snapshot hooks") || ROLLBACK_SNAPSHOT_HOOKS=""
425
+ ROLLBACK_SNAPSHOT_ATTRIBUTES=$(recorded_value "git snapshot attributes") || ROLLBACK_SNAPSHOT_ATTRIBUTES=""
426
+ say "rolling back from step $reached, from $BEFORE"
427
+ if [ "$reached" -ge 15 ]; then rollback_step "stop the Butler" docker stop "$CONTAINER"; fi
428
+ rollback_step "restore agent.json's sync value" restore_agent_sync
429
+ rollback_step "move .git back to repo.git" move_git_back
430
+ # Before any git: root owns repo.git again and nothing the Butler wrote into it can run (C1).
431
+ rollback_step "give repo.git back to root and put back its config, hooks and attributes" secure_backup_git
432
+ rollback_step "clear a stale index.lock" clear_stale_index_lock
433
+ rollback_step "restore repo.git's config, branch and exclude" git_step restore_backup_repo
434
+ rollback_step "drop an apply commit GitHub never got" git_step drop_unpushed_commit
435
+ rollback_step "restore repo.git's owner and mode" restore_repo_owner
436
+ rollback_step "remove the sync key the apply installed" remove_installed_key
437
+ # The hourly backup runs git as root, so it comes back only on a repo.git secure_backup_git put back.
438
+ rollback_step "restore the backup cron" git_step restore_cron
439
+ rollback_step "remove the cutover marker" rm -f -- "$MARKER"
440
+ rollback_step "check the hourly backup can push" git_step check_backup_can_push
441
+ if [ "$reached" -ge 1 ]; then
442
+ rollback_step "start the Butler" docker start "$CONTAINER"
443
+ rollback_step "check the Butler is running" butler_is_running
444
+ fi
445
+ running=$(container_running)
446
+ case "$running" in
447
+ true) running="running" ;;
448
+ false) running="STOPPED" ;;
449
+ *) running="in an unknown state (docker inspect failed)" ;;
450
+ esac
451
+ if [ -n "$ROLLBACK_FAILED" ]; then
452
+ {
453
+ say "before: $BEFORE"
454
+ [ -z "$ROLLBACK_SNAPSHOT" ] || say "git snapshot: $ROLLBACK_SNAPSHOT"
455
+ [ -z "$ROLLBACK_SNAPSHOT_HOOKS" ] || say "git snapshot hooks: $ROLLBACK_SNAPSHOT_HOOKS"
456
+ [ -z "$ROLLBACK_SNAPSHOT_ATTRIBUTES" ] || say "git snapshot attributes: $ROLLBACK_SNAPSHOT_ATTRIBUTES"
457
+ say "failed: $ROLLBACK_FAILED"
458
+ say "at: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
459
+ say "butler: $running"
460
+ } >"$INCOMPLETE" || say "could not write $INCOMPLETE"
461
+ say "ROLLBACK INCOMPLETE: these steps failed: $ROLLBACK_FAILED. The Butler is $running. See $INCOMPLETE; fix the cause, then run --rollback again."
462
+ return 1
463
+ fi
464
+ rm -f -- "$INCOMPLETE" "$APPLYING"
465
+ say "rolled back: the hourly backup owns the bundle again; commits already pushed stay on GitHub. The Butler is $running."
466
+ [ "$reached" -lt 1 ] || [ "$running" = running ]
467
+ }
468
+
469
+ butler_is_running() { [ "$(container_running)" = true ]; }
470
+
471
+ # Before step 1, while root still owns repo.git: a root-owned 0600 tar of what in it can make git run a command, which
472
+ # the Butler can change once it owns .git: config, hooks/ and info/attributes. Sets SNAPSHOT and SNAPSHOT_LINES, the lines
473
+ # the apply records with its state so a rollback finds the snapshot.
474
+ take_git_snapshot() {
475
+ local hooks=absent attributes=absent
476
+ local -a members=(config)
477
+ [ ! -L "$BACKUP_GIT" ] && [ -d "$BACKUP_GIT" ] || die "$BACKUP_GIT is not a real directory"
478
+ [ ! -L "$BACKUP_GIT/config" ] && [ -f "$BACKUP_GIT/config" ] || die "$BACKUP_GIT/config is not a regular file"
479
+ if [ -d "$BACKUP_GIT/hooks" ] && [ ! -L "$BACKUP_GIT/hooks" ]; then hooks=present; members+=(hooks); fi
480
+ if [ -f "$BACKUP_GIT/info/attributes" ] && [ ! -L "$BACKUP_GIT/info/attributes" ]; then attributes=present; members+=(info/attributes); fi
481
+ SNAPSHOT=$GIT_SNAPSHOT_PREFIX-$(date -u +%Y%m%dT%H%M%SZ)-$$.tar
482
+ (set -C; umask 077; : >"$SNAPSHOT") || die "could not create $SNAPSHOT"
483
+ tar -C "$BACKUP_GIT" -cf "$SNAPSHOT" -- "${members[@]}" || die "could not save $BACKUP_GIT's config and hooks to $SNAPSHOT"
484
+ if ! { chown "$ROOT_OWNER" "$SNAPSHOT" && chmod 0600 "$SNAPSHOT"; }; then die "could not make $SNAPSHOT root-only"; fi
485
+ SNAPSHOT_LINES=$(printf 'git snapshot: %s\ngit snapshot hooks: %s\ngit snapshot attributes: %s' "$SNAPSHOT" "$hooks" "$attributes")
486
+ }
487
+
488
+ # A value the apply recorded: in the in-progress file, the unfinished rollback's file, or the marker.
489
+ recorded_value() {
490
+ local file value
491
+ for file in "$APPLYING" "$INCOMPLETE" "$MARKER"; do
492
+ if [ -f "$file" ] && value=$(file_value "$file" "$1"); then printf '%s' "$value"; return 0; fi
493
+ done
494
+ return 1
495
+ }
496
+
497
+ # Moves a path aside as <path>.disabled-<time>-<pid>, never deleting it.
498
+ disable_path() {
499
+ local target
500
+ target="$1.disabled-$(date -u +%Y%m%dT%H%M%SZ)-$$"
501
+ mv -- "$1" "$target" && say "moved aside $1 (now $target)"
502
+ }
503
+
504
+ # The first thing a rollback does to repo.git, before any git runs on it: root owns it again, config is the snapshot's
505
+ # byte for byte (so no [include], filter, alias, sshCommand, insteadOf, hooksPath, fsmonitor or extension the Butler
506
+ # added survives), config.worktree and commondir are gone, hooks/ holds exactly the snapshot's hooks (anything else moved
507
+ # aside, never deleted), and info/attributes is the snapshot's or absent. GIT_READY says whether git may run on it.
508
+ GIT_READY=0
509
+ secure_backup_git() {
510
+ local snapshot hooks attributes owner mode tmp entry name links rc=0
511
+ GIT_READY=0
512
+ snapshot=$ROLLBACK_SNAPSHOT hooks=$ROLLBACK_SNAPSHOT_HOOKS attributes=$ROLLBACK_SNAPSHOT_ATTRIBUTES
513
+ [ -n "$snapshot" ] && [ -n "$hooks" ] && [ -n "$attributes" ] || { say "no git snapshot is recorded, so no git runs on $BACKUP_GIT"; return 1; }
514
+ [ -f "$snapshot" ] && [ ! -L "$snapshot" ] || { say "the git snapshot $snapshot is missing, so no git runs on $BACKUP_GIT"; return 1; }
515
+ [ -d "$BACKUP_GIT" ] && [ ! -L "$BACKUP_GIT" ] || { say "$BACKUP_GIT is not a real directory"; return 1; }
516
+ owner=$(before_value "repo.git owner") || return 1
517
+ mode=$(before_value "repo.git mode") || return 1
518
+ chown -R -h "$owner" "$BACKUP_GIT" && chmod "$mode" "$BACKUP_GIT" || return 1
519
+ # A real repo.git holds no symbolic links. One the Butler planted (objects/ab -> /etc, logs/HEAD -> a host file) would
520
+ # have root's git write through it, and a link in objects/ means the object store is no longer one root can trust (N1).
521
+ links=$(find "$BACKUP_GIT" -type l -print) || { say "could not look for symbolic links in $BACKUP_GIT, so no git runs on it"; return 1; }
522
+ if [ -n "$links" ]; then
523
+ say "$BACKUP_GIT holds a symbolic link, ${links%%$'\n'*}: move it aside (a real repo.git holds no links), then run --rollback again"
524
+ return 1
525
+ fi
526
+ tmp=$(umask 077; mktemp -d "$BACKUP/.cutover-git-restore.XXXXXX") || return 1
527
+ # In a subshell, so the work directory is removed whichever step fails.
528
+ (
529
+ tar -C "$tmp" -xf "$snapshot" || exit 1
530
+ # Each copy goes to a fresh name first (rm never follows a link the Butler may have planted there), then over the old file.
531
+ rm -f -- "$BACKUP_GIT/.config.cutover-restore" && cp -p -- "$tmp/config" "$BACKUP_GIT/.config.cutover-restore" && mv -f -- "$BACKUP_GIT/.config.cutover-restore" "$BACKUP_GIT/config" || exit 1
532
+ rm -f -- "$BACKUP_GIT/config.worktree" || exit 1
533
+ if [ -e "$BACKUP_GIT/commondir" ] || [ -L "$BACKUP_GIT/commondir" ]; then disable_path "$BACKUP_GIT/commondir" || exit 1; fi
534
+ if [ -L "$BACKUP_GIT/hooks" ] || { [ -e "$BACKUP_GIT/hooks" ] && [ ! -d "$BACKUP_GIT/hooks" ]; }; then disable_path "$BACKUP_GIT/hooks" || exit 1; fi
535
+ if [ "$hooks" = present ] || [ -d "$BACKUP_GIT/hooks" ]; then
536
+ mkdir -p -- "$BACKUP_GIT/hooks" || exit 1
537
+ for entry in "$BACKUP_GIT/hooks"/* "$BACKUP_GIT/hooks"/.[!.]* "$BACKUP_GIT/hooks"/..?*; do
538
+ [ -e "$entry" ] || [ -L "$entry" ] || continue
539
+ name=${entry##*/}
540
+ case "$name" in *.disabled-*) continue ;; esac
541
+ if [ -f "$tmp/hooks/$name" ] && [ ! -L "$tmp/hooks/$name" ] && [ -f "$entry" ] && [ ! -L "$entry" ] && cmp -s -- "$entry" "$tmp/hooks/$name"; then continue; fi
542
+ disable_path "$entry" || exit 1
543
+ done
544
+ for entry in "$tmp/hooks"/* "$tmp/hooks"/.[!.]* "$tmp/hooks"/..?*; do
545
+ [ -f "$entry" ] && [ ! -L "$entry" ] || continue
546
+ name=${entry##*/}
547
+ rm -f -- "$BACKUP_GIT/hooks/.$name.cutover-restore" && cp -p -- "$entry" "$BACKUP_GIT/hooks/.$name.cutover-restore" && mv -f -- "$BACKUP_GIT/hooks/.$name.cutover-restore" "$BACKUP_GIT/hooks/$name" || exit 1
548
+ done
549
+ fi
550
+ if [ -L "$BACKUP_GIT/info" ] || { [ -e "$BACKUP_GIT/info" ] && [ ! -d "$BACKUP_GIT/info" ]; }; then disable_path "$BACKUP_GIT/info" || exit 1; fi
551
+ if [ "$attributes" = present ]; then
552
+ mkdir -p -- "$BACKUP_GIT/info" || exit 1
553
+ rm -f -- "$BACKUP_GIT/info/.attributes.cutover-restore" && cp -p -- "$tmp/info/attributes" "$BACKUP_GIT/info/.attributes.cutover-restore" && mv -f -- "$BACKUP_GIT/info/.attributes.cutover-restore" "$BACKUP_GIT/info/attributes" || exit 1
554
+ elif [ -e "$BACKUP_GIT/info/attributes" ] || [ -L "$BACKUP_GIT/info/attributes" ]; then
555
+ rm -f -- "$BACKUP_GIT/info/attributes" || exit 1
556
+ fi
557
+ chown -R -h "$owner" "$BACKUP_GIT" && chmod "$mode" "$BACKUP_GIT"
558
+ ) || rc=1
559
+ rm -rf -- "$tmp"
560
+ [ "$rc" -eq 0 ] || return 1
561
+ GIT_READY=1
562
+ }
563
+
564
+ # A rollback step that runs git on repo.git: only once secure_backup_git has put it back.
565
+ git_step() {
566
+ if [ "$GIT_READY" -ne 1 ]; then say "skipped: repo.git's config and hooks were not put back, so no git runs on it"; return 1; fi
567
+ "$@"
568
+ }
569
+
570
+ restore_agent_sync() {
571
+ local value
572
+ value=$(file_value "$BEFORE" "agent.json sync") || { say "the before-state has no agent.json sync line"; return 1; }
573
+ [ "$(agent_sync_value)" = "$value" ] || edit_agent_sync restore "$value"
574
+ }
575
+
576
+ move_git_back() {
577
+ if [ -e "$BUNDLE/.git" ] && [ -e "$BACKUP_GIT" ]; then say "both $BUNDLE/.git and $BACKUP_GIT exist"; return 1; fi
578
+ if [ -e "$BUNDLE/.git" ]; then mv -- "$BUNDLE/.git" "$BACKUP_GIT"; fi
579
+ [ -d "$BACKUP_GIT" ]
580
+ }
581
+
582
+ # A git that a stop killed mid-write leaves index.lock behind, and every hourly backup would then fail unseen. It is removed
583
+ # only when no git runs on the host or in the container (M16).
584
+ clear_stale_index_lock() {
585
+ [ -e "$BACKUP_GIT/index.lock" ] || return 0
586
+ if pgrep -x git >/dev/null; then say "a git process is running on the host; $BACKUP_GIT/index.lock is left in place"; return 1; fi
587
+ if [ "$(container_running)" != false ]; then
588
+ local rc=0
589
+ docker exec "$CONTAINER" pgrep -x git >/dev/null 2>&1 || rc=$?
590
+ if [ "$rc" -ne 1 ]; then say "git may be running in $CONTAINER (pgrep exited $rc); $BACKUP_GIT/index.lock is left in place"; return 1; fi
591
+ fi
592
+ rm -f -- "$BACKUP_GIT/index.lock" && say "removed a stale $BACKUP_GIT/index.lock (no git was running)"
593
+ }
594
+
595
+ # Puts back the backup repository's config, branch, HEAD and exclude from the before-state.
596
+ restore_backup_repo() {
597
+ local branch key value encoded
598
+ [ -d "$BACKUP_GIT" ] || return 1
599
+ branch=$(before_value branch) || return 1
600
+ if [ "$branch" != "$BUNDLE_BRANCH" ] && backup_git rev-parse --verify -q "refs/heads/$BUNDLE_BRANCH" >/dev/null \
601
+ && [ "$(backup_git symbolic-ref --short HEAD)" = "$BUNDLE_BRANCH" ]; then
602
+ backup_git branch -M "$BUNDLE_BRANCH" "$branch" || return 1
603
+ fi
604
+ value=$(before_value origin.url) || return 1
605
+ restore_config backup_git remote.origin.url "$value" || return 1
606
+ value=$(before_value branch.remote) || return 1
607
+ restore_config backup_git "branch.$branch.remote" "$value" || return 1
608
+ value=$(before_value branch.merge) || return 1
609
+ restore_config backup_git "branch.$branch.merge" "$value" || return 1
610
+ for key in core.bare core.worktree core.sshCommand core.untrackedCache feature.manyFiles; do
611
+ value=$(before_value "$key") || return 1
612
+ restore_config backup_git "$key" "$value" || return 1
613
+ done
614
+ encoded=$(before_value info/exclude.base64) || return 1
615
+ if [ "$encoded" = "(absent)" ]; then
616
+ rm -f -- "$BACKUP_GIT/info/exclude"
617
+ else
618
+ mkdir -p -- "$BACKUP_GIT/info" || return 1
619
+ rm -f -- "$BACKUP_GIT/info/exclude.restore" || return 1
620
+ printf '%s' "$encoded" | base64 -d >"$BACKUP_GIT/info/exclude.restore" || return 1
621
+ mv -f -- "$BACKUP_GIT/info/exclude.restore" "$BACKUP_GIT/info/exclude"
622
+ fi
623
+ }
624
+
625
+ # A commit the apply made after step 4 that GitHub never got (step 9 stopped before its push) is dropped from the branch,
626
+ # so the hourly backup never pushes it. Only GitHub's own main decides: the branch moves back to the step-4 head only when
627
+ # GitHub's main is that head. A push can reach GitHub and still fail on the host, and the owner can push meanwhile, so
628
+ # local refs are never trusted for this. The work tree is not touched.
629
+ drop_unpushed_commit() {
630
+ local head current listed github
631
+ head=$(file_value "$APPLYING" head 2>/dev/null) || return 0
632
+ [ -d "$BACKUP_GIT" ] || return 1
633
+ current=$(backup_git rev-parse HEAD) || return 1
634
+ [ "$current" != "$head" ] || return 0
635
+ listed=$(GIT_SSH_COMMAND=$BACKUP_SSH backup_git ls-remote origin "refs/heads/$BUNDLE_BRANCH") || {
636
+ say "could not read GitHub's $BUNDLE_BRANCH (ls-remote failed), so the branch was left at $current; check GitHub, then run --rollback again"
637
+ return 1
638
+ }
639
+ github=${listed%%[[:space:]]*}
640
+ if [ "$github" = "$head" ]; then
641
+ backup_git reset -q --mixed "$head" || return 1
642
+ say "reset the branch to $head, dropping a commit GitHub never got"
643
+ else
644
+ say "kept the branch: GitHub already has ${github:-(no $BUNDLE_BRANCH)}"
645
+ fi
646
+ }
647
+
648
+ # The hourly backup's push hides its failures, so the rollback proves now that the next one can land.
649
+ check_backup_can_push() {
650
+ local out
651
+ [ -d "$BACKUP_GIT" ] || return 1
652
+ if out=$(GIT_SSH_COMMAND=$BACKUP_SSH backup_git push --dry-run origin "HEAD:$BUNDLE_BRANCH" 2>&1); then return 0; fi
653
+ if printf '%s' "$out" | grep -qiE 'rejected|non-fast-forward|fetch first'; then
654
+ say "the hourly backup can't push; GitHub has commits the host doesn't. Merge GitHub's $BUNDLE_BRANCH into repo.git's branch, then run --rollback again"
655
+ else
656
+ say "the hourly backup can't push: $(printf '%s' "$out" | tail -3 | tr '\n' ' ')"
657
+ fi
658
+ return 1
659
+ }
660
+
661
+ # repo.git's owner (recursively) and mode, as the before-state recorded them (M8).
662
+ restore_repo_owner() {
663
+ local owner mode
664
+ [ -d "$BACKUP_GIT" ] || return 1
665
+ owner=$(before_value "repo.git owner") || return 1
666
+ mode=$(before_value "repo.git mode") || return 1
667
+ chown -R -h "$owner" "$BACKUP_GIT" && chmod "$mode" "$BACKUP_GIT"
668
+ }
669
+
670
+ remove_installed_key() {
671
+ local state
672
+ state=$(before_value "sync key") || return 1
673
+ [ "$state" = present ] || rm -f -- "$SYNC_KEY" "$SYNC_DIR/.deploy_key.cutover"
674
+ }
675
+
676
+ restore_cron() {
677
+ if [ ! -e "$CRON_FILE" ] && [ -f "$CRON_DISABLED" ]; then mv -- "$CRON_DISABLED" "$CRON_FILE" || return 1; fi
678
+ [ -f "$CRON_FILE" ] || { say "$CRON_FILE is missing"; return 1; }
679
+ update_cron || return 1
680
+ grep -q 'backup\.sh' "$CRON_TABLE" || { say "$CRON_TABLE has no backup.sh entry after update_cron"; return 1; }
681
+ }
682
+
683
+ # Whether nothing of a cutover is on disk: no marker, no .git in the bundle, no apply in progress, no unfinished rollback
684
+ # and no disabled cron file.
685
+ nothing_to_roll_back() {
686
+ [ ! -e "$MARKER" ] && [ ! -e "$BUNDLE/.git" ] && [ ! -e "$APPLYING" ] && [ ! -e "$INCOMPLETE" ] && [ ! -e "$CRON_DISABLED" ]
687
+ }
688
+
689
+ # The before-state the apply recorded: in the in-progress file, the unfinished rollback's file, or the marker.
690
+ recorded_before() {
691
+ local file value
692
+ for file in "$APPLYING" "$INCOMPLETE" "$MARKER"; do
693
+ if [ -f "$file" ] && value=$(file_value "$file" before) && [ -f "$value" ]; then printf '%s' "$value"; return 0; fi
694
+ done
695
+ return 1
696
+ }
697
+
698
+ rollback() {
699
+ if nothing_to_roll_back; then say "nothing to roll back: no cutover marker and no .git in the bundle"; return 0; fi
700
+ # A running apply holds this lock and rolls itself back on failure; a second writer would race it.
701
+ exec 8>>"$CUTOVER_LOCK"
702
+ if ! flock -n 8; then
703
+ die "an apply is running (log: $(file_value "$APPLYING" log 2>/dev/null || printf 'unknown')); it rolls itself back on failure. Run --rollback after it ends"
704
+ fi
705
+ BEFORE=$(recorded_before) || die "no recorded before-state: none of $APPLYING, $INCOMPLETE or $MARKER names one"
706
+ exec 9>>"$LOCK_FILE"
707
+ flock -w 900 9 || die "the backup lock stayed busy for 15 minutes"
708
+ rollback_from 15
709
+ }
710
+
711
+ # ---------- apply ----------
712
+
713
+ STEP=0
714
+ APPLIED=0
715
+ IPTABLES_RULE=""
716
+ BEFORE=""
717
+ DETACHED_LOG=${OURO_CUTOVER_DETACHED:-}
718
+
719
+ # The one EXIT trap: an apply that did not finish rolls back from the step it reached; the iptables rule and the work
720
+ # directory never outlive the script; a detached run records its final status for the process following its log.
721
+ on_exit() {
722
+ local status=$?
723
+ trap '' INT TERM HUP PIPE
724
+ # A failure inside a redirected command (`f >/dev/null`) runs this trap under that redirection: write to the script's own output.
725
+ exec 1>&5 2>&6
726
+ if [ "$MODE" = apply ] && [ "$APPLIED" -ne 1 ] && [ "$STEP" -gt 0 ]; then
727
+ say "cutover failed at step $STEP (exit $status); rolling back"
728
+ rollback_from "$STEP" || true
729
+ status=1
730
+ fi
731
+ remove_drop_rule || status=1
732
+ [ -z "${WORK:-}" ] || rm -rf -- "$WORK"
733
+ if [ -n "$DETACHED_LOG" ]; then
734
+ printf '%s\n' "$status" >"$DETACHED_LOG.status.tmp" && mv -f -- "$DETACHED_LOG.status.tmp" "$DETACHED_LOG.status"
735
+ fi
736
+ exit "$status"
737
+ }
738
+
739
+ assert_owned_by_butler() {
740
+ local strays
741
+ strays=$(find "$BUNDLE/.git" ! -uid "$BUTLER_UID" -print | head -5)
742
+ [ -z "$strays" ] || die ".git still has paths not owned by $BUTLER_UID: $strays"
743
+ }
744
+
745
+ # The key's mode, owner and type, read without opening it.
746
+ assert_sync_key() {
747
+ "$HOST_NODE" -e '
748
+ const fs = require("node:fs");
749
+ const [dir, key, uid, gid, rootUid] = process.argv.slice(1);
750
+ const d = fs.lstatSync(dir), k = fs.lstatSync(key);
751
+ const fail = (m) => { process.stderr.write("cutover: " + m + "\n"); process.exit(1); };
752
+ if (!d.isDirectory() || (d.mode & 0o7777) !== 0o755 || d.uid !== Number(rootUid)) fail(dir + " must be a root-owned 0755 directory");
753
+ if (!k.isFile() || (k.mode & 0o7777) !== 0o400 || k.uid !== Number(uid) || k.gid !== Number(gid)) fail(key + " must be a regular file, mode 0400, owner " + uid + ":" + gid);
754
+ ' "$SYNC_DIR" "$SYNC_KEY" "$SYNC_UID" "$SYNC_GID" "${ROOT_OWNER%%:*}"
755
+ }
756
+
757
+ # Every psyche file is still tracked (M5).
758
+ assert_psyche_tracked() {
759
+ local name
760
+ for name in $PSYCHE_FILES; do
761
+ bundle_git ls-files --error-unmatch -- "psyche/$name" >/dev/null 2>&1 || die "psyche/$name would no longer be tracked; nothing was pushed"
762
+ done
763
+ }
764
+
765
+ # The effective audit of the started container, with sync on, so a missing /run/ouro-sync mount fails the apply (13b).
766
+ audit_started_container() {
767
+ local image=$1 reference
768
+ reference=$(docker inspect --format '{{.Config.Image}}' "$CONTAINER")
769
+ (umask 077; docker inspect "$CONTAINER" >"$WORK/container.json"; docker image inspect "$image" >"$WORK/image.json")
770
+ docker run --rm --pull=never --network=none \
771
+ --user 0:0 --read-only --cap-drop=ALL --security-opt=no-new-privileges \
772
+ --entrypoint /opt/ouro/deploy/unraid/audit-container-spec.sh \
773
+ --mount "type=bind,src=$WORK/container.json,dst=/audit/container.json,readonly" \
774
+ --mount "type=bind,src=$WORK/image.json,dst=/audit/image.json,readonly" \
775
+ "$image" --inspect /audit/container.json --image-inspect /audit/image.json --expected-image "$image" \
776
+ --mount-contract canonical-gateway --expected-image-reference "$reference" --expected-icon "$AUDIT_ICON" --sync-enabled true
777
+ }
778
+
779
+ apply() {
780
+ preflight
781
+ local image head remote_head untrack status dirty fetch
782
+ image=$(before_value image)
783
+ # The moved repository must fetch every branch into refs/remotes/origin/, so a push moves the tracking ref (I4).
784
+ fetch=$(before_value remote.origin.fetch)
785
+ [ "$fetch" = "$DEFAULT_FETCH" ] || die "repo.git's remote.origin.fetch is \"$fetch\", not \"$DEFAULT_FETCH\"; nothing was changed"
786
+ exec 8>>"$CUTOVER_LOCK"
787
+ flock -n 8 || die "another cutover run holds $CUTOVER_LOCK; nothing was changed"
788
+
789
+ take_git_snapshot
790
+ STEP=1; say "1. stopping $CONTAINER"
791
+ printf 'before: %s\nlog: %s\nstarted: %s\n%s\n' "$BEFORE" "${DETACHED_LOG:-(none)}" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$SNAPSHOT_LINES" >"$APPLYING.tmp"
792
+ mv -f -- "$APPLYING.tmp" "$APPLYING"
793
+ docker stop "$CONTAINER" >/dev/null
794
+
795
+ STEP=2; say "2. disabling the hourly backup cron"
796
+ mv -- "$CRON_FILE" "$CRON_DISABLED"
797
+ update_cron
798
+ if grep -q 'backup\.sh' "$CRON_TABLE"; then die "$CRON_TABLE still runs backup.sh"; fi
799
+
800
+ STEP=3; say "3. taking $LOCK_FILE"
801
+ exec 9>>"$LOCK_FILE"
802
+ flock -w 900 9 || die "the backup lock stayed busy for 15 minutes"
803
+
804
+ STEP=4; say "4. the last backup commit and push, as backup.sh makes them"
805
+ backup_git add -A
806
+ if ! backup_git diff --cached --quiet; then
807
+ backup_git -c "user.name=$BACKUP_COMMIT_NAME" -c "user.email=$BACKUP_COMMIT_EMAIL" commit -q -m "backup $(date -u +%Y-%m-%dT%H:%MZ)"
808
+ fi
809
+ GIT_SSH_COMMAND=$BACKUP_SSH backup_git push -q origin "HEAD:$BUNDLE_BRANCH"
810
+ printf 'head: %s\n' "$(backup_git rev-parse HEAD)" >>"$APPLYING"
811
+
812
+ STEP=5; say "5. moving the repository into the bundle"
813
+ mv -- "$BACKUP_GIT" "$BUNDLE/.git"
814
+ restore_config bundle_git core.bare "(unset)"
815
+ restore_config bundle_git core.worktree "(unset)"
816
+ restore_config bundle_git core.sshCommand "(unset)"
817
+
818
+ STEP=6; say "6. origin is $BUNDLE_REMOTE"
819
+ bundle_git remote set-url origin "$BUNDLE_REMOTE"
820
+
821
+ STEP=7; say "7. fetching origin $BUNDLE_BRANCH and tracking it"
822
+ GIT_SSH_COMMAND=$BACKUP_SSH bundle_git fetch -q origin "+refs/heads/$BUNDLE_BRANCH:refs/remotes/origin/$BUNDLE_BRANCH"
823
+ bundle_git branch -M "$BUNDLE_BRANCH"
824
+ bundle_git branch -q -u "origin/$BUNDLE_BRANCH"
825
+ head=$(bundle_git rev-parse HEAD)
826
+ remote_head=$(bundle_git rev-parse "refs/remotes/origin/$BUNDLE_BRANCH")
827
+ [ "$head" = "$remote_head" ] || die "HEAD $head is not origin/$BUNDLE_BRANCH $remote_head"
828
+
829
+ STEP=8; say "8. writing the release's exclude list"
830
+ mkdir -p -- "$BUNDLE/.git/info"
831
+ cp -- "$WORK/exclude" "$BUNDLE/.git/info/exclude.cutover"
832
+ mv -f -- "$BUNDLE/.git/info/exclude.cutover" "$BUNDLE/.git/info/exclude"
833
+
834
+ STEP=9; say "9. untracking excluded files"
835
+ untrack=$WORK/untrack
836
+ bundle_git ls-files -ci --exclude-standard -z >"$untrack"
837
+ if [ -s "$untrack" ]; then
838
+ bundle_git --literal-pathspecs rm --cached --quiet --pathspec-from-file="$untrack" --pathspec-file-nul
839
+ bundle_git -c "user.name=$AGENT_NAME" -c "user.email=$AGENT_NAME@ouro.local" commit -q -m "sync: stop tracking package-managed and excluded files"
840
+ assert_psyche_tracked
841
+ GIT_SSH_COMMAND=$BACKUP_SSH bundle_git push -q origin "HEAD:$BUNDLE_BRANCH"
842
+ fi
843
+ assert_psyche_tracked
844
+ bundle_git config core.untrackedCache true
845
+ bundle_git config feature.manyFiles true
846
+
847
+ STEP=10; say "10. checking nothing excluded is tracked"
848
+ [ -z "$(bundle_git ls-files -ci --exclude-standard)" ] || die "excluded files are still tracked"
849
+ status=$WORK/status
850
+ GIT_OPTIONAL_LOCKS=0 bundle_git status --porcelain --untracked-files=all >"$status"
851
+ dirty=$(status_problems "$image" "$status")
852
+ [ -z "$dirty" ] || die "git status shows package-managed or secret paths: $(printf '%s' "$dirty" | tr '\n' ' ')"
853
+
854
+ STEP=11; say "11. installing the sync key for ouro-sync"
855
+ [ ! -L "$SYNC_DIR" ] || die "$SYNC_DIR is a symlink"
856
+ mkdir -p -- "$SYNC_DIR"
857
+ chown -h "$ROOT_OWNER" "$SYNC_DIR"
858
+ chmod 0755 "$SYNC_DIR"
859
+ rm -f -- "$SYNC_DIR/.deploy_key.cutover"
860
+ # The copy is born 0600, so it is never readable to anyone else, even before the chmod (M6).
861
+ (umask 077; cp -- "$BACKUP_KEY" "$SYNC_DIR/.deploy_key.cutover")
862
+ chown "$SYNC_OWNER" "$SYNC_DIR/.deploy_key.cutover"
863
+ chmod 0400 "$SYNC_DIR/.deploy_key.cutover"
864
+ mv -f -- "$SYNC_DIR/.deploy_key.cutover" "$SYNC_KEY"
865
+ assert_sync_key
866
+
867
+ STEP=12; say "12. turning sync on in agent.json"
868
+ edit_agent_sync enable
869
+
870
+ STEP=13; say "13. handing .git to the Butler"
871
+ chown -R -h "$BUTLER_OWNER" "$BUNDLE/.git"
872
+ assert_owned_by_butler
873
+
874
+ STEP=14; say "14. writing $MARKER"
875
+ printf 'before: %s\n%s\ndone: %s\n' "$BEFORE" "$SNAPSHOT_LINES" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >"$MARKER"
876
+
877
+ STEP=15; say "15. starting $CONTAINER"
878
+ docker start "$CONTAINER" >/dev/null
879
+ say "13b. auditing the started container with sync on"
880
+ audit_started_container "$image" >/dev/null
881
+ exec_check -u 10001 "$CONTAINER" test -r "$CONTAINER_KEY"
882
+ case "$EXEC_STATUS" in
883
+ 0) die "the Butler's uid can read $CONTAINER_KEY" ;;
884
+ 1) ;;
885
+ *) die "test -r $CONTAINER_KEY as the Butler's uid exited $EXEC_STATUS, not 1" ;;
886
+ esac
887
+
888
+ rm -f -- "$APPLYING"
889
+ APPLIED=1
890
+ say "cutover done. Run --verify-live next."
891
+ }
892
+
893
+ # ---------- verify live ----------
894
+
895
+ # Removes the stall probe's rule and proves it is gone (`iptables -C` must then fail).
896
+ remove_drop_rule() {
897
+ [ -n "$IPTABLES_RULE" ] || return 0
898
+ local rule=$IPTABLES_RULE
899
+ IPTABLES_RULE=""
900
+ # shellcheck disable=SC2086
901
+ iptables -D $rule || say "verify: could not remove the iptables rule: iptables -D $rule" >&2
902
+ # shellcheck disable=SC2086
903
+ if iptables -C $rule 2>/dev/null; then
904
+ say "verify: THE IPTABLES RULE IS STILL IN PLACE; remove it by hand: iptables -D $rule" >&2
905
+ return 1
906
+ fi
907
+ }
908
+
909
+ butler_sync() { docker exec -u 10001 "$CONTAINER" timeout 60 node "$CLI_ENTRY" sync --agent "$AGENT_NAME"; }
910
+
911
+ verify_live() {
912
+ [ -e "$MARKER" ] || die "the cutover is not done ($MARKER is missing)"
913
+ local heads uid start elapsed status out leftover
914
+ trap 'trap "" INT TERM HUP; exit 130' INT TERM
915
+ trap 'trap "" INT TERM HUP; exit 129' HUP
916
+
917
+ say "the sync path reaches GitHub as ouro-sync"
918
+ heads=$(docker exec -u 10001 -e "GIT_SSH_COMMAND=sudo -n -u ouro-sync $SYNC_SSH_WRAPPER" -e GIT_SSH_VARIANT=ssh "$CONTAINER" \
919
+ git ls-remote "$BUNDLE_REMOTE" "refs/heads/$BUNDLE_BRANCH")
920
+ [ -n "$heads" ] || die "git ls-remote through $SYNC_SSH_WRAPPER returned no $BUNDLE_BRANCH"
921
+
922
+ say "the Butler's uid cannot read the key"
923
+ assert_key_unreadable
924
+
925
+ say "a stalled fetch ends on its own"
926
+ # The container shares the host's network, so the rule drops port 22 for the sync user's uid alone (M4).
927
+ exec_check "$CONTAINER" id -u "$SYNC_USER"
928
+ uid=$(tr -d '[:space:]' <"$WORK/exec.out")
929
+ [ "$EXEC_STATUS" -eq 0 ] && [ "$uid" = "$SYNC_CONTAINER_UID" ] || die "the image's $SYNC_USER uid is \"$uid\", not $SYNC_CONTAINER_UID"
930
+ IPTABLES_RULE="OUTPUT -m owner --uid-owner $uid -p tcp --dport 22 -m comment --comment $PROBE_COMMENT -j DROP"
931
+ # shellcheck disable=SC2086
932
+ iptables -I $IPTABLES_RULE
933
+ start=$(date +%s)
934
+ status=0
935
+ out=$(butler_sync 2>&1) || status=$?
936
+ elapsed=$(( $(date +%s) - start ))
937
+ remove_drop_rule || die "the stall probe's iptables rule could not be removed"
938
+ printf '%s\n' "$out"
939
+ [ "$status" -ne 0 ] || die "ouro sync succeeded while TCP 22 was dropped, so the rule did not stall it"
940
+ if printf '%s' "$out" | grep -qi 'paused'; then die "ouro sync was paused, so the stall proves nothing; remove state/sync-paused and run again"; fi
941
+ # The stall must end the way a stall ends: a timeout or a failed connect, not some other fast failure (M2).
942
+ printf '%s' "$out" | grep -Eqi 'timed out|connect to host' || die "the stalled ouro sync failed without a timeout or a connect failure (exit $status)"
943
+ [ "$elapsed" -le "$STALL_LIMIT_S" ] || die "the stalled ouro sync took ${elapsed}s; it must end within ${STALL_LIMIT_S}s"
944
+ exec_check "$CONTAINER" ps -u "$uid" -o pid=
945
+ leftover=$(tr -d '[:space:]' <"$WORK/exec.out")
946
+ case "$EXEC_STATUS" in
947
+ 0) die "ouro-sync processes outlived the stalled sync: $leftover" ;;
948
+ 1) [ -z "$leftover" ] || die "ouro-sync processes outlived the stalled sync: $leftover" ;;
949
+ *) die "ps -u $uid exited $EXEC_STATUS, not 0 or 1" ;;
950
+ esac
951
+ say "the stalled sync ended in ${elapsed}s with nothing left running"
952
+
953
+ say "ouro sync works again"
954
+ butler_sync
955
+ say "verify-live ok"
956
+ }
957
+
958
+ # ---------- detaching ----------
959
+
960
+ # Starts this script again for $1 in a new session with all output in a new log, then follows that log and exits with
961
+ # the detached run's status. Losing this process (Ctrl-C, a closed terminal, a dropped ssh session) never stops the run.
962
+ run_detached() {
963
+ local mode=$1 log
964
+ [ -d "$BACKUP" ] || die "$BACKUP is missing"
965
+ log=$BACKUP/cutover-$(date -u +%Y%m%dT%H%M%SZ)-$$.log
966
+ (set -C; umask 077; : >"$log") || die "could not create $log"
967
+ OURO_CUTOVER_DETACHED=$log setsid -f bash "$0" "$mode" </dev/null >>"$log" 2>&1 || die "could not start the detached run"
968
+ say "running detached; its log is $log"
969
+ follow_log "$log"
970
+ }
971
+
972
+ follow_log() {
973
+ local log=$1 printed=0 size pid="" waited=0
974
+ trap 'say ""; say "stopped following. The run continues detached; its log is $log"; exit 130' INT TERM HUP
975
+ while :; do
976
+ size=$(wc -c <"$log" | tr -d ' ')
977
+ if [ "$size" -gt "$printed" ]; then
978
+ tail -c +"$((printed + 1))" "$log" | head -c "$((size - printed))" || true
979
+ printed=$size
980
+ fi
981
+ if [ -f "$log.status" ]; then
982
+ size=$(wc -c <"$log" | tr -d ' ')
983
+ [ "$size" -le "$printed" ] || tail -c +"$((printed + 1))" "$log"
984
+ exit "$(cat "$log.status")"
985
+ fi
986
+ [ -n "$pid" ] || pid=$(cat "$log.pid" 2>/dev/null || true)
987
+ if [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null && [ ! -f "$log.status" ]; then
988
+ tail -c +"$((printed + 1))" "$log"
989
+ say "THE DETACHED RUN (pid $pid) ENDED WITHOUT A STATUS. Read $log; if the Butler is not back, run --rollback."
990
+ exit 1
991
+ fi
992
+ if [ -z "$pid" ]; then
993
+ waited=$((waited + 1))
994
+ [ "$waited" -lt 100 ] || { say "the detached run never started; see $log"; exit 1; }
995
+ fi
996
+ sleep 0.2
997
+ done
998
+ }
999
+
1000
+ # ---------- main ----------
1001
+
1002
+ MODE=dry-run
1003
+ MODE_ARG=${1:-}
1004
+ case "$MODE_ARG" in
1005
+ "" | --dry-run) MODE=dry-run ;;
1006
+ --apply) MODE=apply ;;
1007
+ --rollback) MODE=rollback ;;
1008
+ --verify-live) MODE=verify-live ;;
1009
+ *) die "usage: sanctuary-sync-cutover.sh [--dry-run|--apply|--rollback|--verify-live]" ;;
1010
+ esac
1011
+ [ "$#" -le 1 ] || die "usage: sanctuary-sync-cutover.sh [--dry-run|--apply|--rollback|--verify-live]"
1012
+ if [ -z "$TEST_ROOT" ] && [ "$(id -u)" -ne 0 ]; then die "run this as root on the host"; fi
1013
+
1014
+ if [ -z "$DETACHED_LOG" ] && { [ "$MODE" = apply ] || [ "$MODE" = rollback ]; }; then
1015
+ # The detached run starts this file again, so it must be one (not `bash -s` reading stdin).
1016
+ { [ -f "$0" ] && [ -r "$0" ]; } || die "run this script from a file, not stdin"
1017
+ if [ "$MODE" = rollback ] && nothing_to_roll_back; then say "nothing to roll back: no cutover marker and no .git in the bundle"; exit 0; fi
1018
+ run_detached "$MODE_ARG"
1019
+ fi
1020
+
1021
+ if [ -n "$DETACHED_LOG" ]; then
1022
+ # A detached run outlives its terminal: HUP is ignored, as under nohup. INT and TERM end an apply into its rollback.
1023
+ trap '' HUP
1024
+ printf '%s\n' "$$" >"$DETACHED_LOG.pid"
1025
+ fi
1026
+ exec 5>&1 6>&2
1027
+ WORK=$(mktemp -d "${TMPDIR:-/tmp}/sanctuary-sync-cutover.XXXXXX")
1028
+ trap on_exit EXIT
1029
+ trap 'trap "" INT TERM HUP; exit 130' INT TERM
1030
+
1031
+ case "$MODE" in
1032
+ dry-run) preflight ;;
1033
+ apply) apply ;;
1034
+ rollback)
1035
+ trap '' INT TERM HUP PIPE
1036
+ rollback
1037
+ ;;
1038
+ verify-live) verify_live ;;
1039
+ esac