@ouro.bot/cli 0.1.0-alpha.77 → 0.1.0-alpha.770
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.
- package/LICENSE +21 -0
- package/README.md +154 -23
- package/RepairGuide.ouro/agent.json +5 -0
- package/RepairGuide.ouro/psyche/IDENTITY.md +19 -0
- package/RepairGuide.ouro/psyche/SOUL.md +55 -0
- package/RepairGuide.ouro/skills/diagnose-broken-remote.md +63 -0
- package/RepairGuide.ouro/skills/diagnose-stacked-typed-issues.md +35 -0
- package/RepairGuide.ouro/skills/diagnose-sync-blocked.md +54 -0
- package/RepairGuide.ouro/skills/diagnose-vault-expired.md +60 -0
- package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/agent.json +4 -2
- package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/SOUL.md +2 -2
- package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-serpent.md +1 -1
- package/assets/bluebubbles-host +264 -0
- package/changelog.json +4930 -16
- package/deploy/unraid/Dockerfile +48 -0
- package/deploy/unraid/README.txt +2483 -0
- package/deploy/unraid/audit-container-spec.sh +4 -0
- package/deploy/unraid/container-runtime.json +1 -0
- package/deploy/unraid/migrate-sanctuary-bundle.mjs +21 -0
- package/deploy/unraid/ouro-events/bootstrap-spool.sh +92 -0
- package/deploy/unraid/ouro-events/emit-event.mjs +310 -0
- package/deploy/unraid/ouro-events/emit-usenet-event.sh +54 -0
- package/deploy/unraid/ouro-events/install-usenet-guard.sh +435 -0
- package/deploy/unraid/ouro-events/usenet-health.sh +176 -0
- package/deploy/unraid/sanctuary-acceptance-adapter.sh +37 -0
- package/deploy/unraid/sanctuary-acceptance-contract.json +201 -0
- package/deploy/unraid/sanctuary-acceptance-harness.sh +23 -0
- package/deploy/unraid/sanctuary-deployment-target.mjs +709 -0
- package/deploy/unraid/sanctuary-safe-rename.py +24 -0
- package/deploy/unraid/sanctuary-unit16-host-broker.mjs +1804 -0
- package/deploy/unraid/sanctuary-unit16-run.sh +544 -0
- package/deploy/unraid/sanctuary-unit18-target-audit.sh +27 -0
- package/deploy/unraid/sanctuary.ouro/agent.json +24 -0
- package/deploy/unraid/sanctuary.ouro/arc/README.md +3 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +5 -0
- package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +8 -0
- package/deploy/unraid/sanctuary.ouro/provider-readiness.json +11 -0
- package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +5 -0
- package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +5 -0
- package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +9 -0
- package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +9 -0
- package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +10 -0
- package/deploy/unraid/sanctuary.ouro/state/policy/steward.json +7 -0
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +23 -0
- package/deploy/unraid/sanctuary.xml +27 -0
- package/dist/a2a/card.js +57 -0
- package/dist/a2a/client.js +180 -0
- package/dist/a2a/config.js +50 -0
- package/dist/a2a/delegation-stores.js +56 -0
- package/dist/a2a/did-resolution.js +93 -0
- package/dist/a2a/identity.js +170 -0
- package/dist/a2a/inbound-share.js +107 -0
- package/dist/a2a/mission-result-wire.js +196 -0
- package/dist/a2a/onboarding.js +115 -0
- package/dist/a2a/pin-store.js +132 -0
- package/dist/a2a/seen-ledger.js +120 -0
- package/dist/a2a/server.js +604 -0
- package/dist/a2a/task-store.js +69 -0
- package/dist/a2a/transport.js +45 -0
- package/dist/a2a/types.js +3 -0
- package/dist/arc/attention-types.js +8 -0
- package/dist/arc/cares.js +360 -0
- package/dist/arc/episodes.js +118 -0
- package/dist/arc/evolution.js +490 -0
- package/dist/arc/flight-recorder.js +1034 -0
- package/dist/arc/intentions.js +134 -0
- package/dist/arc/json-store.js +117 -0
- package/dist/arc/obligations.js +386 -0
- package/dist/arc/packets.js +336 -0
- package/dist/arc/presence.js +185 -0
- package/dist/arc/task-lifecycle.js +57 -0
- package/dist/commerce/store.js +755 -0
- package/dist/commerce/types.js +3 -0
- package/dist/heart/active-work.js +820 -29
- package/dist/heart/agent-entry.js +190 -3
- package/dist/heart/approval-files.js +113 -0
- package/dist/heart/approval-store.js +950 -0
- package/dist/heart/attachments/image-normalize.js +194 -0
- package/dist/heart/attachments/materialize.js +97 -0
- package/dist/heart/attachments/originals.js +88 -0
- package/dist/heart/attachments/render.js +29 -0
- package/dist/heart/attachments/sources/bluebubbles.js +156 -0
- package/dist/heart/attachments/sources/cli-local-file.js +78 -0
- package/dist/heart/attachments/sources/index.js +18 -0
- package/dist/heart/attachments/sources/telegram.js +71 -0
- package/dist/heart/attachments/store.js +132 -0
- package/dist/heart/attachments/types.js +93 -0
- package/dist/heart/auth/auth-flow.js +495 -0
- package/dist/heart/autonomy-budget.js +478 -0
- package/dist/heart/awaiting/await-alert.js +148 -0
- package/dist/heart/awaiting/await-expiry.js +143 -0
- package/dist/heart/awaiting/await-loader.js +91 -0
- package/dist/heart/awaiting/await-parser.js +149 -0
- package/dist/heart/awaiting/await-runtime-state.js +100 -0
- package/dist/heart/awaiting/await-scheduler.js +382 -0
- package/dist/heart/background-operations.js +281 -0
- package/dist/heart/bridges/manager.js +137 -17
- package/dist/heart/bridges/store.js +14 -2
- package/dist/heart/bundle-state.js +168 -0
- package/dist/heart/commitments.js +64 -18
- package/dist/heart/config-registry.js +331 -0
- package/dist/heart/config.js +128 -129
- package/dist/heart/context-loss-gauntlet.js +383 -0
- package/dist/heart/context-loss-sentinel.js +1157 -0
- package/dist/heart/core.js +2014 -469
- package/dist/heart/cross-chat-delivery.js +9 -22
- package/dist/heart/daemon/agent-config-check.js +451 -0
- package/dist/heart/daemon/agent-discovery.js +185 -3
- package/dist/heart/daemon/agent-service.js +542 -0
- package/dist/heart/daemon/agentic-repair.js +547 -0
- package/dist/heart/daemon/await-private-wake.js +59 -0
- package/dist/heart/daemon/bluebubbles-cli-integration.js +71 -0
- package/dist/heart/daemon/bluebubbles-health-diagnostics.js +122 -0
- package/dist/heart/daemon/bluebubbles-host-protocol.js +433 -0
- package/dist/heart/daemon/bluebubbles-host.js +273 -0
- package/dist/heart/daemon/boot-sync-probe.js +197 -0
- package/dist/heart/daemon/cadence.js +180 -0
- package/dist/heart/daemon/cli-defaults.js +832 -0
- package/dist/heart/daemon/cli-desk.js +322 -0
- package/dist/heart/daemon/cli-exec.js +8692 -0
- package/dist/heart/daemon/cli-help.js +710 -0
- package/dist/heart/daemon/cli-parse.js +2714 -0
- package/dist/heart/daemon/cli-render-doctor.js +57 -0
- package/dist/heart/daemon/cli-render.js +777 -0
- package/dist/heart/daemon/cli-types.js +8 -0
- package/dist/heart/daemon/connect-bay.js +323 -0
- package/dist/heart/daemon/container-credential-bootstrap.js +273 -0
- package/dist/heart/daemon/container-healthcheck.js +96 -0
- package/dist/heart/daemon/container-runtime.js +97 -0
- package/dist/heart/daemon/container-spec-auditor-main.js +160 -0
- package/dist/heart/daemon/container-spec-auditor.js +176 -0
- package/dist/heart/daemon/daemon-bootstrap-startup.js +41 -0
- package/dist/heart/daemon/daemon-cli.js +29 -2181
- package/dist/heart/daemon/daemon-entry.js +944 -10
- package/dist/heart/daemon/daemon-health.js +186 -0
- package/dist/heart/daemon/daemon-rollup.js +57 -0
- package/dist/heart/daemon/daemon-runtime-sync.js +88 -13
- package/dist/heart/daemon/daemon-tombstone.js +236 -0
- package/dist/heart/daemon/daemon.js +1877 -88
- package/dist/heart/daemon/dns-workflow.js +394 -0
- package/dist/heart/daemon/doctor-types.js +8 -0
- package/dist/heart/daemon/doctor.js +1755 -0
- package/dist/heart/daemon/freshness.js +345 -0
- package/dist/heart/daemon/habit-private-wake.js +61 -0
- package/dist/heart/daemon/health-monitor.js +142 -11
- package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
- package/dist/heart/daemon/hooks/bundle-meta.js +135 -1
- package/dist/heart/daemon/http-health-probe.js +80 -0
- package/dist/heart/daemon/human-command-screens.js +234 -0
- package/dist/heart/daemon/human-readiness.js +114 -0
- package/dist/heart/daemon/inner-status.js +78 -0
- package/dist/heart/daemon/interactive-repair.js +394 -0
- package/dist/heart/daemon/launchd.js +37 -8
- package/dist/heart/daemon/log-tailer.js +79 -10
- package/dist/heart/daemon/logs-prune.js +132 -0
- package/dist/heart/daemon/mcp-canary.js +524 -0
- package/dist/heart/daemon/message-router.js +45 -5
- package/dist/heart/daemon/migrate-to-desk.js +848 -0
- package/dist/heart/daemon/os-cron-deps.js +135 -0
- package/dist/heart/daemon/os-cron.js +22 -14
- package/dist/heart/daemon/ouro-bot-entry.js +4 -2
- package/dist/heart/daemon/ouro-entry.js +3 -1
- package/dist/heart/daemon/plugin-cli.js +432 -0
- package/dist/heart/daemon/process-manager.js +624 -61
- package/dist/heart/daemon/provider-discovery.js +137 -0
- package/dist/heart/daemon/provider-ping-progress.js +83 -0
- package/dist/heart/daemon/prunable-bundle.js +144 -0
- package/dist/heart/daemon/pulse.js +511 -0
- package/dist/heart/daemon/readiness-repair.js +365 -0
- package/dist/heart/daemon/run-hooks.js +2 -0
- package/dist/heart/daemon/runtime-logging.js +47 -14
- package/dist/heart/daemon/runtime-metadata.js +2 -30
- package/dist/heart/daemon/runtime-mode.js +13 -2
- package/dist/heart/daemon/safe-mode.js +161 -0
- package/dist/heart/daemon/sanctuary-acceptance-adapter.js +2468 -0
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +1624 -0
- package/dist/heart/daemon/sanctuary-acceptance-marker.js +267 -0
- package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +976 -0
- package/dist/heart/daemon/sanctuary-bundle-migration.js +398 -0
- package/dist/heart/daemon/sanctuary-scheduler-liveness.js +227 -0
- package/dist/heart/daemon/sanctuary-scheduler-origin.js +132 -0
- package/dist/heart/daemon/sense-manager.js +689 -48
- package/dist/heart/daemon/session-id-resolver.js +131 -0
- package/dist/heart/daemon/skill-management-installer.js +1 -1
- package/dist/heart/daemon/socket-client.js +180 -12
- package/dist/heart/daemon/stale-bundle-prune.js +113 -0
- package/dist/heart/daemon/startup-tui.js +338 -0
- package/dist/heart/daemon/supercronic-supervisor.js +282 -0
- package/dist/heart/daemon/task-scheduler.js +117 -39
- package/dist/heart/daemon/terminal-ui.js +499 -0
- package/dist/heart/daemon/thoughts.js +138 -54
- package/dist/heart/daemon/up-progress.js +366 -0
- package/dist/heart/daemon/vault-items.js +56 -0
- package/dist/heart/delegation.js +1 -4
- package/dist/heart/external-events/router.js +1187 -0
- package/dist/heart/habits/habit-cancel.js +810 -0
- package/dist/heart/habits/habit-lifecycle.js +1327 -0
- package/dist/heart/habits/habit-migration.js +194 -0
- package/dist/heart/habits/habit-parser.js +278 -0
- package/dist/heart/habits/habit-runtime-state.js +93 -0
- package/dist/heart/habits/habit-scheduler.js +653 -0
- package/dist/heart/habits/habit-session-summary.js +318 -0
- package/dist/heart/habits/habit-session.js +636 -0
- package/dist/heart/{daemon → hatch}/hatch-flow.js +55 -62
- package/dist/heart/{daemon → hatch}/hatch-specialist.js +6 -8
- package/dist/heart/{daemon → hatch}/specialist-prompt.js +12 -9
- package/dist/heart/{daemon → hatch}/specialist-tools.js +53 -18
- package/dist/heart/identity.js +184 -59
- package/dist/heart/kept-notes.js +289 -0
- package/dist/heart/kicks.js +1 -1
- package/dist/heart/machine-identity.js +161 -0
- package/dist/heart/mail-import-discovery.js +353 -0
- package/dist/heart/mailbox/mailbox-http-hooks.js +98 -0
- package/dist/heart/mailbox/mailbox-http-response.js +7 -0
- package/dist/heart/mailbox/mailbox-http-routes.js +398 -0
- package/dist/heart/mailbox/mailbox-http-static.js +103 -0
- package/dist/heart/mailbox/mailbox-http-transport.js +116 -0
- package/dist/heart/mailbox/mailbox-http.js +99 -0
- package/dist/heart/mailbox/mailbox-read.js +39 -0
- package/dist/heart/mailbox/mailbox-types.js +27 -0
- package/dist/heart/mailbox/mailbox-view.js +197 -0
- package/dist/heart/mailbox/readers/agent-machine.js +428 -0
- package/dist/heart/mailbox/readers/continuity-readers.js +329 -0
- package/dist/heart/mailbox/readers/mail.js +375 -0
- package/dist/heart/mailbox/readers/runtime-readers.js +823 -0
- package/dist/heart/mailbox/readers/sessions.js +232 -0
- package/dist/heart/mailbox/readers/shared.js +111 -0
- package/dist/heart/mcp/mcp-server.js +696 -0
- package/dist/heart/migrate-config.js +100 -0
- package/dist/heart/model-capabilities.js +11 -0
- package/dist/heart/orientation-frame.js +345 -0
- package/dist/heart/platform.js +81 -0
- package/dist/heart/primitives.js +1 -1
- package/dist/heart/private-runtime/decision-renderer.js +158 -0
- package/dist/heart/private-runtime/index.js +18 -0
- package/dist/heart/private-runtime/ledger.js +221 -0
- package/dist/heart/private-runtime/policy.js +228 -0
- package/dist/heart/private-runtime/types.js +2 -0
- package/dist/heart/progress-story.js +1 -1
- package/dist/heart/provider-attempt.js +137 -0
- package/dist/heart/provider-binding-resolver.js +273 -0
- package/dist/heart/provider-credentials.js +449 -0
- package/dist/heart/provider-failover.js +311 -0
- package/dist/heart/provider-models.js +96 -0
- package/dist/heart/provider-ping.js +265 -0
- package/dist/heart/provider-readiness-cache.js +119 -0
- package/dist/heart/provider-visibility.js +188 -0
- package/dist/heart/providers/anthropic-token.js +131 -0
- package/dist/heart/providers/anthropic.js +173 -60
- package/dist/heart/providers/azure.js +23 -11
- package/dist/heart/providers/error-classification.js +127 -0
- package/dist/heart/providers/github-copilot.js +34 -38
- package/dist/heart/providers/minimax-vlm.js +189 -0
- package/dist/heart/providers/minimax.js +26 -8
- package/dist/heart/providers/openai-codex-token.js +349 -0
- package/dist/heart/providers/openai-codex.js +55 -40
- package/dist/heart/providers/openai-compatible.js +177 -0
- package/dist/heart/run-ledger.js +247 -0
- package/dist/heart/runtime-capability-check.js +170 -0
- package/dist/heart/runtime-credentials.js +607 -0
- package/dist/heart/runtime-cwd.js +87 -0
- package/dist/heart/sense-truth.js +22 -4
- package/dist/heart/session-activity.js +90 -24
- package/dist/heart/session-events.js +1318 -0
- package/dist/heart/session-playback-cli-main.js +5 -0
- package/dist/heart/session-playback-cli.js +36 -0
- package/dist/heart/session-playback.js +231 -0
- package/dist/heart/session-stats-cli-main.js +5 -0
- package/dist/heart/session-stats.js +182 -0
- package/dist/heart/session-transcript.js +133 -0
- package/dist/heart/start-of-turn-packet.js +372 -0
- package/dist/heart/steward-policy.js +302 -0
- package/dist/heart/streaming.js +604 -207
- package/dist/heart/structured-output.js +196 -0
- package/dist/heart/sync-classification.js +176 -0
- package/dist/heart/sync.js +449 -0
- package/dist/heart/target-resolution.js +11 -7
- package/dist/heart/tempo.js +93 -0
- package/dist/heart/temporal-view.js +41 -0
- package/dist/heart/timeouts.js +101 -0
- package/dist/heart/tool-activity-callbacks.js +59 -0
- package/dist/heart/tool-approval.js +342 -0
- package/dist/heart/tool-description.js +155 -0
- package/dist/heart/tool-friction.js +55 -0
- package/dist/heart/tool-loop.js +200 -0
- package/dist/heart/turn-context.js +481 -0
- package/dist/heart/{daemon → versioning}/ouro-bot-global-installer.js +6 -5
- package/dist/heart/{daemon → versioning}/ouro-bot-wrapper.js +1 -1
- package/dist/heart/versioning/ouro-path-installer.js +432 -0
- package/dist/heart/versioning/ouro-recovery-launcher.js +76 -0
- package/dist/heart/versioning/ouro-version-manager.js +409 -0
- package/dist/heart/{daemon → versioning}/staged-restart.js +40 -8
- package/dist/heart/{daemon → versioning}/update-checker.js +6 -1
- package/dist/heart/versioning/update-hooks.js +154 -0
- package/dist/heart/versioning/version-intent.js +114 -0
- package/dist/heart/work-card.js +380 -0
- package/dist/mailbox-ui/assets/index-Du_9G9WO.css +1 -0
- package/dist/mailbox-ui/assets/index-J8zDwHmE.js +1 -0
- package/dist/mailbox-ui/assets/vendor-CcN1XpQ9.js +61 -0
- package/dist/mailbox-ui/index.html +16 -0
- package/dist/mailroom/attention.js +167 -0
- package/dist/mailroom/autonomy.js +209 -0
- package/dist/mailroom/blob-store.js +889 -0
- package/dist/mailroom/body-cache.js +61 -0
- package/dist/mailroom/cache-sync-cli.js +58 -0
- package/dist/mailroom/core.js +803 -0
- package/dist/mailroom/entry.js +160 -0
- package/dist/mailroom/file-store.js +568 -0
- package/dist/mailroom/hosted-cache-sync.js +384 -0
- package/dist/mailroom/mbox-import.js +393 -0
- package/dist/mailroom/migration.js +164 -0
- package/dist/mailroom/outbound.js +380 -0
- package/dist/mailroom/policy.js +263 -0
- package/dist/mailroom/reader.js +292 -0
- package/dist/mailroom/search-cache.js +513 -0
- package/dist/mailroom/search-relevance.js +319 -0
- package/dist/mailroom/smtp-ingress.js +176 -0
- package/dist/mailroom/source-state.js +176 -0
- package/dist/mailroom/thread.js +109 -0
- package/dist/mailroom/travel-extract.js +89 -0
- package/dist/mind/bundle-manifest.js +24 -2
- package/dist/mind/context.js +278 -111
- package/dist/mind/desk-section.js +362 -0
- package/dist/mind/diary-integrity.js +60 -0
- package/dist/mind/{memory.js → diary.js} +68 -77
- package/dist/mind/embedding-provider.js +60 -0
- package/dist/mind/file-state.js +179 -0
- package/dist/mind/{associative-recall.js → note-search.js} +47 -58
- package/dist/mind/obligation-steering.js +227 -0
- package/dist/mind/pending.js +34 -8
- package/dist/mind/prompt-budget.js +479 -0
- package/dist/mind/prompt-refresh.js +3 -2
- package/dist/mind/prompt.js +1058 -187
- package/dist/mind/provenance-trust.js +26 -0
- package/dist/mind/record-paths.js +312 -0
- package/dist/mind/scrutiny.js +173 -0
- package/dist/mind/session-transaction.js +415 -0
- package/dist/nerves/cli-logging.js +7 -1
- package/dist/nerves/coverage/audit-rules.js +15 -6
- package/dist/nerves/coverage/audit.js +28 -2
- package/dist/nerves/coverage/cli.js +1 -1
- package/dist/nerves/coverage/contract.js +5 -5
- package/dist/nerves/coverage/file-completeness.js +139 -5
- package/dist/nerves/coverage/run-artifacts.js +55 -35
- package/dist/nerves/coverage/source-scanner.js +4 -4
- package/dist/nerves/event-buffer.js +111 -0
- package/dist/nerves/index.js +293 -9
- package/dist/nerves/observation.js +20 -0
- package/dist/nerves/redact.js +79 -0
- package/dist/nerves/review/cli-main.js +5 -0
- package/dist/nerves/review/cli.js +156 -0
- package/dist/nerves/review/core.js +159 -0
- package/dist/nerves/runtime.js +17 -1
- package/dist/repertoire/ado-client.js +15 -56
- package/dist/repertoire/ado-semantic.js +16 -10
- package/dist/repertoire/api-client.js +97 -0
- package/dist/repertoire/bitwarden-store.js +1045 -0
- package/dist/repertoire/bundle-templates.js +71 -0
- package/dist/repertoire/bw-installer.js +180 -0
- package/dist/repertoire/coding/codex-jsonl.js +64 -0
- package/dist/repertoire/coding/context-pack.js +331 -0
- package/dist/repertoire/coding/feedback.js +218 -30
- package/dist/repertoire/coding/index.js +15 -3
- package/dist/repertoire/coding/manager.js +166 -10
- package/dist/repertoire/coding/spawner.js +58 -11
- package/dist/repertoire/coding/tools.js +242 -14
- package/dist/repertoire/coding/workbench-client.js +272 -0
- package/dist/repertoire/coding/workbench-manager.js +346 -0
- package/dist/repertoire/commerce-errors.js +109 -0
- package/dist/repertoire/commerce-self-test.js +156 -0
- package/dist/repertoire/credential-access.js +178 -0
- package/dist/repertoire/desk/classifier.js +362 -0
- package/dist/repertoire/duffel-client.js +185 -0
- package/dist/repertoire/github-client.js +14 -55
- package/dist/repertoire/graph-client.js +11 -52
- package/dist/repertoire/guardrails.js +813 -18
- package/dist/repertoire/mcp-client.js +44 -3
- package/dist/repertoire/mcp-manager.js +259 -20
- package/dist/repertoire/mcp-tools.js +83 -0
- package/dist/repertoire/plugin-mcp.js +175 -0
- package/dist/repertoire/plugins.js +253 -0
- package/dist/repertoire/relationship-authorization.js +194 -0
- package/dist/repertoire/shell-sessions.js +133 -0
- package/dist/repertoire/skills.js +48 -4
- package/dist/repertoire/stripe-client.js +131 -0
- package/dist/repertoire/tool-arguments.js +143 -0
- package/dist/repertoire/tool-results.js +29 -0
- package/dist/repertoire/tools-a2a.js +699 -0
- package/dist/repertoire/tools-attachments.js +322 -0
- package/dist/repertoire/tools-awaiting.js +575 -0
- package/dist/repertoire/tools-base.js +82 -1139
- package/dist/repertoire/tools-bluebubbles.js +2 -0
- package/dist/repertoire/tools-bridge.js +144 -0
- package/dist/repertoire/tools-bundle.js +993 -0
- package/dist/repertoire/tools-commerce.js +253 -0
- package/dist/repertoire/tools-config.js +186 -0
- package/dist/repertoire/tools-continuity.js +521 -0
- package/dist/repertoire/tools-credential.js +383 -0
- package/dist/repertoire/tools-evolution.js +527 -0
- package/dist/repertoire/tools-files.js +344 -0
- package/dist/repertoire/tools-flight.js +290 -0
- package/dist/repertoire/tools-flow.js +119 -0
- package/dist/repertoire/tools-github.js +3 -8
- package/dist/repertoire/tools-habits.js +103 -0
- package/dist/repertoire/tools-mail.js +1828 -0
- package/dist/repertoire/tools-notes.js +485 -0
- package/dist/repertoire/tools-obligations.js +182 -0
- package/dist/repertoire/tools-orientation.js +31 -0
- package/dist/repertoire/tools-record.js +482 -0
- package/dist/repertoire/tools-rsvp.js +139 -0
- package/dist/repertoire/tools-runtime.js +150 -0
- package/dist/repertoire/tools-session.js +1096 -0
- package/dist/repertoire/tools-shell.js +133 -0
- package/dist/repertoire/tools-steward-policy.js +95 -0
- package/dist/repertoire/tools-stripe.js +224 -0
- package/dist/repertoire/tools-surface.js +355 -0
- package/dist/repertoire/tools-teams.js +12 -39
- package/dist/repertoire/tools-telegram-contacts.js +37 -0
- package/dist/repertoire/tools-travel.js +125 -0
- package/dist/repertoire/tools-trip.js +982 -0
- package/dist/repertoire/tools-unraid.js +459 -0
- package/dist/repertoire/tools-user-profile.js +146 -0
- package/dist/repertoire/tools-vault.js +40 -0
- package/dist/repertoire/tools-voice.js +145 -0
- package/dist/repertoire/tools.js +377 -80
- package/dist/repertoire/travel-api-client.js +360 -0
- package/dist/repertoire/unraid-client.js +155 -0
- package/dist/repertoire/unraid-restart.js +212 -0
- package/dist/repertoire/user-profile.js +131 -0
- package/dist/repertoire/vault-setup.js +246 -0
- package/dist/repertoire/vault-unlock.js +628 -0
- package/dist/rsvp/aisleplanner-client.js +354 -0
- package/dist/rsvp/cli.js +926 -0
- package/dist/rsvp/config.js +327 -0
- package/dist/rsvp/cutover.js +433 -0
- package/dist/rsvp/diagnostics.js +232 -0
- package/dist/rsvp/diff-renderer.js +176 -0
- package/dist/rsvp/habit-policy.js +110 -0
- package/dist/rsvp/habit-stage.js +123 -0
- package/dist/rsvp/incident-bundle.js +97 -0
- package/dist/rsvp/migration.js +204 -0
- package/dist/rsvp/native-habit-runner.js +630 -0
- package/dist/rsvp/outbound-state.js +628 -0
- package/dist/rsvp/query.js +118 -0
- package/dist/rsvp/replay.js +233 -0
- package/dist/rsvp/snapshot.js +232 -0
- package/dist/rsvp/spend-ledger.js +175 -0
- package/dist/scripts/claude-code-hook.js +41 -0
- package/dist/scripts/claude-code-stop-hook.js +47 -0
- package/dist/senses/a2a-entry.js +115 -0
- package/dist/senses/attention-queue.js +186 -0
- package/dist/senses/await-turn-message.js +58 -0
- package/dist/senses/bluebubbles/active-turns.js +216 -0
- package/dist/senses/bluebubbles/attachment-cache.js +53 -0
- package/dist/senses/bluebubbles/attachment-download.js +137 -0
- package/dist/senses/{bluebubbles-client.js → bluebubbles/client.js} +355 -40
- package/dist/senses/bluebubbles/context-packet.js +207 -0
- package/dist/senses/bluebubbles/context-smoke.js +144 -0
- package/dist/senses/bluebubbles/entry.js +90 -0
- package/dist/senses/{bluebubbles-inbound-log.js → bluebubbles/inbound-log.js} +21 -3
- package/dist/senses/bluebubbles/index.js +3839 -0
- package/dist/senses/bluebubbles/latest-turn.js +311 -0
- package/dist/senses/{bluebubbles-media.js → bluebubbles/media.js} +121 -71
- package/dist/senses/{bluebubbles-model.js → bluebubbles/model.js} +159 -28
- package/dist/senses/{bluebubbles-mutation-log.js → bluebubbles/mutation-log.js} +24 -13
- package/dist/senses/bluebubbles/outbound-state.js +308 -0
- package/dist/senses/bluebubbles/processed-log.js +133 -0
- package/dist/senses/bluebubbles/reaction-policy.js +47 -0
- package/dist/senses/bluebubbles/replay.js +284 -0
- package/dist/senses/{bluebubbles-runtime-state.js → bluebubbles/runtime-state.js} +30 -2
- package/dist/senses/bluebubbles/semantic-receipts.js +1986 -0
- package/dist/senses/{bluebubbles-session-cleanup.js → bluebubbles/session-cleanup.js} +1 -1
- package/dist/senses/bluebubbles/webhook-registration.js +228 -0
- package/dist/senses/bluebubbles-meta-guard.js +39 -0
- package/dist/senses/cli/bracketed-paste.js +82 -0
- package/dist/senses/cli/image-paste.js +287 -0
- package/dist/senses/cli/image-ref-navigation.js +75 -0
- package/dist/senses/cli/ink-app.js +156 -0
- package/dist/senses/cli/inline-diff.js +64 -0
- package/dist/senses/cli/input-keys.js +174 -0
- package/dist/senses/cli/kill-ring.js +86 -0
- package/dist/senses/cli/message-list.js +51 -0
- package/dist/senses/cli/ouro-tui.js +607 -0
- package/dist/senses/cli/spinner-imperative.js +135 -0
- package/dist/senses/cli/spinner.js +101 -0
- package/dist/senses/cli/status-line.js +60 -0
- package/dist/senses/cli/streaming-markdown.js +526 -0
- package/dist/senses/cli/tool-display.js +85 -0
- package/dist/senses/cli/tool-render.js +85 -0
- package/dist/senses/cli/tui-store.js +241 -0
- package/dist/senses/cli/virtual-list.js +35 -0
- package/dist/senses/cli-entry.js +60 -8
- package/dist/senses/cli.js +600 -317
- package/dist/senses/commands.js +66 -3
- package/dist/senses/context-packet-ledger.js +371 -0
- package/dist/senses/context-packets.js +154 -0
- package/dist/senses/habit-lifecycle-guard.js +24 -0
- package/dist/senses/habit-turn-message.js +160 -0
- package/dist/senses/ingress-evidence.js +2 -0
- package/dist/senses/mail-entry.js +66 -0
- package/dist/senses/mail.js +524 -0
- package/dist/senses/pipeline.js +1081 -204
- package/dist/senses/private-runtime-worker.js +780 -0
- package/dist/senses/private-runtime.js +1491 -0
- package/dist/senses/proactive-content-guard.js +51 -0
- package/dist/senses/sanctuary-download-credit-presentation.js +31 -0
- package/dist/senses/sanctuary-full-visibility-contract.js +302 -0
- package/dist/senses/sanctuary-grounding.js +118 -0
- package/dist/senses/sanctuary-health-acceptance-probe-entry.js +12 -0
- package/dist/senses/sanctuary-health-acceptance-probe.js +774 -0
- package/dist/senses/sanctuary-health-runner.js +101 -0
- package/dist/senses/sanctuary-health.js +408 -0
- package/dist/senses/sanctuary-interactive-control.js +306 -0
- package/dist/senses/sanctuary-media-optimization.js +436 -0
- package/dist/senses/sanctuary-runtime.js +287 -0
- package/dist/senses/sanctuary-sab.js +85 -0
- package/dist/senses/sanctuary-storage-optimization-contract.js +32 -0
- package/dist/senses/shared-turn.js +488 -0
- package/dist/senses/surface-tool.js +109 -0
- package/dist/senses/teams-entry.js +60 -8
- package/dist/senses/teams.js +515 -217
- package/dist/senses/telegram-admission.js +711 -0
- package/dist/senses/telegram-approval-runtime.js +566 -0
- package/dist/senses/telegram-attachments.js +182 -0
- package/dist/senses/telegram-audit-ledger.js +220 -0
- package/dist/senses/telegram-client.js +1676 -0
- package/dist/senses/telegram-effect-adapter.js +796 -0
- package/dist/senses/telegram-entry.js +83 -0
- package/dist/senses/telegram.js +2018 -0
- package/dist/senses/trust-gate.js +104 -9
- package/dist/senses/voice/audio-playback.js +237 -0
- package/dist/senses/voice/audio-routing.js +119 -0
- package/dist/senses/voice/elevenlabs.js +202 -0
- package/dist/senses/voice/floor-control.js +431 -0
- package/dist/senses/voice/floor-controller.js +115 -0
- package/dist/senses/voice/golden-path.js +116 -0
- package/dist/senses/voice/index.js +29 -0
- package/dist/senses/voice/meeting.js +113 -0
- package/dist/senses/voice/outbound.js +190 -0
- package/dist/senses/voice/phone.js +33 -0
- package/dist/senses/voice/playback.js +139 -0
- package/dist/senses/voice/realtime-eval.js +496 -0
- package/dist/senses/voice/realtime-trace.js +531 -0
- package/dist/senses/voice/transcript.js +70 -0
- package/dist/senses/voice/turn.js +191 -0
- package/dist/senses/voice/twilio-phone-runtime.js +807 -0
- package/dist/senses/voice/twilio-phone.js +5081 -0
- package/dist/senses/voice/types.js +2 -0
- package/dist/senses/voice/whisper.js +161 -0
- package/dist/senses/voice-entry.js +81 -0
- package/dist/senses/voice-realtime-eval-command.js +99 -0
- package/dist/senses/voice-realtime-eval-entry.js +21 -0
- package/dist/senses/voice-twilio-entry.js +87 -0
- package/dist/trips/core.js +138 -0
- package/dist/trips/store.js +265 -0
- package/dist/util/frontmatter.js +69 -0
- package/npm-shrinkwrap.json +8129 -0
- package/package.json +71 -12
- package/skills/agent-commerce.md +113 -0
- package/skills/browser-navigation.md +117 -0
- package/skills/commerce-setup-guide.md +116 -0
- package/skills/commerce-setup.md +84 -0
- package/skills/configure-dev-tools.md +99 -0
- package/skills/travel-planning.md +138 -0
- package/dist/heart/daemon/auth-flow.js +0 -430
- package/dist/heart/daemon/ouro-path-installer.js +0 -178
- package/dist/heart/daemon/update-hooks.js +0 -138
- package/dist/heart/obligations.js +0 -141
- package/dist/heart/safe-workspace.js +0 -228
- package/dist/heart/session-recall.js +0 -116
- package/dist/mind/friends/channel.js +0 -84
- package/dist/mind/friends/group-context.js +0 -144
- package/dist/mind/friends/resolver.js +0 -84
- package/dist/mind/friends/store-file.js +0 -190
- package/dist/mind/friends/store.js +0 -4
- package/dist/mind/friends/tokens.js +0 -26
- package/dist/mind/friends/trust-explanation.js +0 -74
- package/dist/mind/friends/types.js +0 -29
- package/dist/repertoire/tasks/board.js +0 -134
- package/dist/repertoire/tasks/index.js +0 -224
- package/dist/repertoire/tasks/lifecycle.js +0 -80
- package/dist/repertoire/tasks/middleware.js +0 -65
- package/dist/repertoire/tasks/parser.js +0 -173
- package/dist/repertoire/tasks/scanner.js +0 -132
- package/dist/repertoire/tasks/transitions.js +0 -144
- package/dist/senses/bluebubbles-entry.js +0 -13
- package/dist/senses/bluebubbles.js +0 -1180
- package/dist/senses/debug-activity.js +0 -148
- package/dist/senses/inner-dialog-worker.js +0 -90
- package/dist/senses/inner-dialog.js +0 -525
- package/subagents/README.md +0 -7
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/basilisk.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jafar.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jormungandr.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/kaa.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/medusa.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/nagini.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/ouroboros.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/python.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/quetzalcoatl.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/sir-hiss.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
- /package/dist/{repertoire/tasks/types.js → heart/attachments/sources/adapter.js} +0 -0
- /package/dist/heart/{daemon → hatch}/hatch-animation.js +0 -0
- /package/dist/heart/{daemon → hatch}/specialist-orchestrator.js +0 -0
- /package/dist/heart/{daemon → versioning}/ouro-uti.js +0 -0
- /package/dist/heart/{daemon → versioning}/wrapper-publish-guard.js +0 -0
|
@@ -0,0 +1,2483 @@
|
|
|
1
|
+
Mendelow Cloud Butler operator runbook
|
|
2
|
+
|
|
3
|
+
The production container is ouro-butler. It runs as UID/GID 10001, publishes no
|
|
4
|
+
ports, uses host networking only so its loopback-only Unraid GraphQL client can
|
|
5
|
+
reach 127.0.0.1, mounts the runtime and sanctuary.ouro bundle read-write plus
|
|
6
|
+
the privileged event spool and SAB configuration read-only, and uses restart
|
|
7
|
+
policy unless-stopped.
|
|
8
|
+
|
|
9
|
+
Start/stop:
|
|
10
|
+
docker start ouro-butler
|
|
11
|
+
docker stop ouro-butler
|
|
12
|
+
|
|
13
|
+
Status and health:
|
|
14
|
+
docker inspect --format '{{.State.Status}} {{.State.Health.Status}}' ouro-butler
|
|
15
|
+
docker logs --tail 200 ouro-butler
|
|
16
|
+
|
|
17
|
+
Effective-spec audit helper:
|
|
18
|
+
Run Update, Backup, and Restore command sequences from a root shell with `set -eu`.
|
|
19
|
+
Before any sequence, define this helper. It captures one actual container
|
|
20
|
+
inspect and its reviewed image inspect without printing either, invokes the
|
|
21
|
+
packaged auditor from the explicit validated runner image argument, and
|
|
22
|
+
removes its mode-0600 inputs on success or shell exit:
|
|
23
|
+
audit_effective() {
|
|
24
|
+
(
|
|
25
|
+
AUDIT_CONTAINER=$1
|
|
26
|
+
AUDIT_EXPECTED_IMAGE=$2
|
|
27
|
+
AUDIT_RUNNER_IMAGE_ID=$3
|
|
28
|
+
AUDIT_MOUNT_CONTRACT=${4-canonical}
|
|
29
|
+
validate_exact_image_id "$AUDIT_EXPECTED_IMAGE" || return $?
|
|
30
|
+
validate_exact_image_id "$AUDIT_RUNNER_IMAGE_ID" || return $?
|
|
31
|
+
test "$AUDIT_RUNNER_IMAGE_ID" != sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d || return $?
|
|
32
|
+
case "$AUDIT_MOUNT_CONTRACT" in
|
|
33
|
+
canonical) set -- ;;
|
|
34
|
+
legacy-alpha742) set -- --mount-contract legacy-alpha742 ;;
|
|
35
|
+
*) return 1 ;;
|
|
36
|
+
esac
|
|
37
|
+
INSPECT_DIR=$(mktemp -d /mnt/user/appdata/ouro-butler/staging/inspect.XXXXXX) || return $?
|
|
38
|
+
trap 'rm -f -- "$INSPECT_DIR/container.json" "$INSPECT_DIR/image.json"; rmdir -- "$INSPECT_DIR"' EXIT || return $?
|
|
39
|
+
chmod 0700 "$INSPECT_DIR" || return $?
|
|
40
|
+
umask 077 || return $?
|
|
41
|
+
docker inspect "$AUDIT_CONTAINER" >"$INSPECT_DIR/container.json" || return $?
|
|
42
|
+
docker image inspect "$AUDIT_EXPECTED_IMAGE" >"$INSPECT_DIR/image.json" || return $?
|
|
43
|
+
chmod 0600 "$INSPECT_DIR/container.json" "$INSPECT_DIR/image.json" || return $?
|
|
44
|
+
docker run --rm --pull=never --network=none \
|
|
45
|
+
--user 0:0 --read-only --cap-drop=ALL --security-opt=no-new-privileges \
|
|
46
|
+
--entrypoint /opt/ouro/deploy/unraid/audit-container-spec.sh \
|
|
47
|
+
--mount "type=bind,src=$INSPECT_DIR/container.json,dst=/audit/container.json,readonly" \
|
|
48
|
+
--mount "type=bind,src=$INSPECT_DIR/image.json,dst=/audit/image.json,readonly" \
|
|
49
|
+
"$AUDIT_RUNNER_IMAGE_ID" --inspect /audit/container.json --image-inspect /audit/image.json --expected-image "$AUDIT_EXPECTED_IMAGE" "$@" || return $?
|
|
50
|
+
rm -f -- "$INSPECT_DIR/container.json" "$INSPECT_DIR/image.json" || return $?
|
|
51
|
+
rmdir -- "$INSPECT_DIR" || return $?
|
|
52
|
+
trap - EXIT || return $?
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
Define these helpers in the same root shell. Each profile change calls Unraid's
|
|
56
|
+
own root-local Docker manager backend, after proving exact durable identities.
|
|
57
|
+
A missing Butler entry is a no-op when disabling; this avoids the backend's
|
|
58
|
+
false-to-index-zero behavior. The helper preserves every non-Butler row byte
|
|
59
|
+
for byte and restores the complete captured file atomically on any failure:
|
|
60
|
+
butler_autostart_counts() {
|
|
61
|
+
(
|
|
62
|
+
AUTOSTART_FILE=/var/lib/docker/unraid-autostart
|
|
63
|
+
test -f "$AUTOSTART_FILE" || return $?
|
|
64
|
+
awk '
|
|
65
|
+
$1 == "ouro-butler" { production++ }
|
|
66
|
+
$1 == "ouro-butler-staging" { staging++ }
|
|
67
|
+
$1 == "ouro-butler-rollback" { rollback++ }
|
|
68
|
+
$1 == "ouro-butler-legacy-evidence" { legacy++ }
|
|
69
|
+
END { printf "%d %d %d %d", production + 0, staging + 0, rollback + 0, legacy + 0 }
|
|
70
|
+
' "$AUTOSTART_FILE"
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
snapshot_nonbutler_autostart() {
|
|
74
|
+
awk '
|
|
75
|
+
$1 != "ouro-butler" && $1 != "ouro-butler-staging" && $1 != "ouro-butler-rollback" && $1 != "ouro-butler-legacy-evidence" { print }
|
|
76
|
+
' /var/lib/docker/unraid-autostart
|
|
77
|
+
}
|
|
78
|
+
run_unraid_autostart_backend() {
|
|
79
|
+
(
|
|
80
|
+
AUTOSTART_ACTION=$1
|
|
81
|
+
AUTOSTART_CONTAINER=$2
|
|
82
|
+
AUTOSTART_VALUE=$3
|
|
83
|
+
case "$AUTOSTART_ACTION:$AUTOSTART_VALUE" in wait:0|autostart:true|autostart:false) ;; *) return 1 ;; esac
|
|
84
|
+
case "$AUTOSTART_CONTAINER" in
|
|
85
|
+
ouro-butler|ouro-butler-staging|ouro-butler-rollback|ouro-butler-legacy-evidence) ;;
|
|
86
|
+
*) return 1 ;;
|
|
87
|
+
esac
|
|
88
|
+
timeout -s KILL 20 /usr/bin/php -r '
|
|
89
|
+
[$action, $container, $value] = array_slice($argv, 1);
|
|
90
|
+
$allowed = ["ouro-butler", "ouro-butler-staging", "ouro-butler-rollback", "ouro-butler-legacy-evidence"];
|
|
91
|
+
if (!in_array($container, $allowed, true)) exit(2);
|
|
92
|
+
if ($action === "wait" && $value === "0") {
|
|
93
|
+
$_POST = ["action" => "wait", "container" => $container, "wait" => "0"];
|
|
94
|
+
} elseif ($action === "autostart" && ($value === "true" || $value === "false")) {
|
|
95
|
+
$_POST = ["action" => "autostart", "container" => $container, "auto" => $value, "wait" => "0"];
|
|
96
|
+
} else exit(2);
|
|
97
|
+
$_SERVER["DOCUMENT_ROOT"] = "/usr/local/emhttp";
|
|
98
|
+
$_SERVER["REQUEST_URI"] = "docker";
|
|
99
|
+
require "/usr/local/emhttp/plugins/dynamix.docker.manager/include/UpdateConfig.php";
|
|
100
|
+
' "$AUTOSTART_ACTION" "$AUTOSTART_CONTAINER" "$AUTOSTART_VALUE"
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
mutate_butler_autostart() {
|
|
104
|
+
(
|
|
105
|
+
AUTOSTART_CONTAINER=$1
|
|
106
|
+
AUTOSTART_ENABLED=$2
|
|
107
|
+
case "$AUTOSTART_CONTAINER" in
|
|
108
|
+
ouro-butler|ouro-butler-staging|ouro-butler-rollback|ouro-butler-legacy-evidence) ;;
|
|
109
|
+
*) return 1 ;;
|
|
110
|
+
esac
|
|
111
|
+
case "$AUTOSTART_ENABLED" in true|false) ;; *) return 1 ;; esac
|
|
112
|
+
AUTOSTART_COUNT=$(awk -v name="$AUTOSTART_CONTAINER" '$1 == name { count++ } END { print count + 0 }' /var/lib/docker/unraid-autostart) || return $?
|
|
113
|
+
case "$AUTOSTART_COUNT" in 0|1) ;; *) return 1 ;; esac
|
|
114
|
+
if test "$AUTOSTART_ENABLED" = false && test "$AUTOSTART_COUNT" -eq 0; then
|
|
115
|
+
return 0
|
|
116
|
+
fi
|
|
117
|
+
if test "$AUTOSTART_COUNT" -eq 1; then
|
|
118
|
+
run_unraid_autostart_backend wait "$AUTOSTART_CONTAINER" 0 || return $?
|
|
119
|
+
fi
|
|
120
|
+
if test "$AUTOSTART_ENABLED" = false; then
|
|
121
|
+
run_unraid_autostart_backend autostart "$AUTOSTART_CONTAINER" false || return $?
|
|
122
|
+
AUTOSTART_EXPECTED_COUNT=0
|
|
123
|
+
else
|
|
124
|
+
if test "$AUTOSTART_COUNT" -eq 0; then
|
|
125
|
+
run_unraid_autostart_backend autostart "$AUTOSTART_CONTAINER" true || return $?
|
|
126
|
+
fi
|
|
127
|
+
AUTOSTART_EXPECTED_COUNT=1
|
|
128
|
+
fi
|
|
129
|
+
AUTOSTART_COUNT=$(awk -v name="$AUTOSTART_CONTAINER" '$1 == name { count++ } END { print count + 0 }' /var/lib/docker/unraid-autostart) || return $?
|
|
130
|
+
test "$AUTOSTART_COUNT" -eq "$AUTOSTART_EXPECTED_COUNT" || return $?
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
set_butler_autostart() {
|
|
134
|
+
(
|
|
135
|
+
AUTOSTART_PROFILE=$1
|
|
136
|
+
case "$AUTOSTART_PROFILE" in disabled|production) ;; *) return 1 ;; esac
|
|
137
|
+
test "$(id -u)" -eq 0 || return $?
|
|
138
|
+
AUTOSTART_FILE=/var/lib/docker/unraid-autostart
|
|
139
|
+
AUTOSTART_INCLUDE=/usr/local/emhttp/plugins/dynamix.docker.manager/include/UpdateConfig.php
|
|
140
|
+
test -f "$AUTOSTART_FILE" && test ! -L "$AUTOSTART_FILE" || return 1
|
|
141
|
+
test "$(stat -c '%u:%g:%a' "$AUTOSTART_FILE")" = 0:0:644 || return $?
|
|
142
|
+
test -f "$AUTOSTART_INCLUDE" && test ! -L "$AUTOSTART_INCLUDE" || return 1
|
|
143
|
+
test "$(stat -c '%u:%g:%a' "$AUTOSTART_INCLUDE")" = 0:0:644 || return $?
|
|
144
|
+
AUTOSTART_COUNTS=$(butler_autostart_counts) || return $?
|
|
145
|
+
set -- $AUTOSTART_COUNTS
|
|
146
|
+
test "$#" -eq 4 || return $?
|
|
147
|
+
for AUTOSTART_COUNT in "$@"; do case "$AUTOSTART_COUNT" in 0|1) ;; *) return 1 ;; esac; done
|
|
148
|
+
if test "$AUTOSTART_PROFILE" = production; then
|
|
149
|
+
docker container inspect ouro-butler >/dev/null 2>&1 || return $?
|
|
150
|
+
fi
|
|
151
|
+
AUTOSTART_BACKUP=$(mktemp /var/lib/docker/.unraid-autostart.ouro.XXXXXX) || return $?
|
|
152
|
+
AUTOSTART_NONBUTLER_BEFORE=$(mktemp /run/unraid-autostart.nonbutler-before.XXXXXX) || { rm -f -- "$AUTOSTART_BACKUP"; return 1; }
|
|
153
|
+
AUTOSTART_NONBUTLER_AFTER=$(mktemp /run/unraid-autostart.nonbutler-after.XXXXXX) || { rm -f -- "$AUTOSTART_BACKUP" "$AUTOSTART_NONBUTLER_BEFORE"; return 1; }
|
|
154
|
+
AUTOSTART_COMMITTED=no
|
|
155
|
+
AUTOSTART_BACKUP_READY=no
|
|
156
|
+
AUTOSTART_RESTORE_TMP=
|
|
157
|
+
autostart_cleanup() {
|
|
158
|
+
AUTOSTART_STATUS=$?
|
|
159
|
+
trap - EXIT HUP INT TERM
|
|
160
|
+
if test "$AUTOSTART_COMMITTED" = yes; then
|
|
161
|
+
rm -f -- "$AUTOSTART_BACKUP" || AUTOSTART_STATUS=1
|
|
162
|
+
elif test "$AUTOSTART_BACKUP_READY" = yes; then
|
|
163
|
+
AUTOSTART_RESTORE_TMP=$(mktemp /var/lib/docker/.unraid-autostart.restore.XXXXXX) || AUTOSTART_RESTORE_TMP=
|
|
164
|
+
if test -n "$AUTOSTART_RESTORE_TMP" \
|
|
165
|
+
&& cp -p -- "$AUTOSTART_BACKUP" "$AUTOSTART_RESTORE_TMP" \
|
|
166
|
+
&& sync -f "$AUTOSTART_RESTORE_TMP" \
|
|
167
|
+
&& mv -f -- "$AUTOSTART_RESTORE_TMP" "$AUTOSTART_FILE" \
|
|
168
|
+
&& sync -f "$AUTOSTART_FILE" \
|
|
169
|
+
&& sync -f /var/lib/docker; then
|
|
170
|
+
rm -f -- "$AUTOSTART_BACKUP" || AUTOSTART_STATUS=1
|
|
171
|
+
else
|
|
172
|
+
test -z "$AUTOSTART_RESTORE_TMP" || rm -f -- "$AUTOSTART_RESTORE_TMP"
|
|
173
|
+
printf 'CRITICAL: Butler autostart rollback failed; recovery copy preserved at %s\n' "$AUTOSTART_BACKUP" >&2
|
|
174
|
+
AUTOSTART_STATUS=1
|
|
175
|
+
fi
|
|
176
|
+
else
|
|
177
|
+
rm -f -- "$AUTOSTART_BACKUP" || AUTOSTART_STATUS=1
|
|
178
|
+
fi
|
|
179
|
+
rm -f -- "$AUTOSTART_NONBUTLER_BEFORE" "$AUTOSTART_NONBUTLER_AFTER" || AUTOSTART_STATUS=1
|
|
180
|
+
exit "$AUTOSTART_STATUS"
|
|
181
|
+
}
|
|
182
|
+
trap autostart_cleanup EXIT
|
|
183
|
+
trap 'exit 129' HUP
|
|
184
|
+
trap 'exit 130' INT
|
|
185
|
+
trap 'exit 143' TERM
|
|
186
|
+
cp -p -- "$AUTOSTART_FILE" "$AUTOSTART_BACKUP" || return $?
|
|
187
|
+
cmp -s -- "$AUTOSTART_FILE" "$AUTOSTART_BACKUP" || return 1
|
|
188
|
+
sync -f "$AUTOSTART_BACKUP" || return $?
|
|
189
|
+
sync -f /var/lib/docker || return $?
|
|
190
|
+
AUTOSTART_BACKUP_READY=yes
|
|
191
|
+
snapshot_nonbutler_autostart >"$AUTOSTART_NONBUTLER_BEFORE" || return $?
|
|
192
|
+
mutate_butler_autostart ouro-butler-staging false || return $?
|
|
193
|
+
mutate_butler_autostart ouro-butler-rollback false || return $?
|
|
194
|
+
mutate_butler_autostart ouro-butler-legacy-evidence false || return $?
|
|
195
|
+
if test "$AUTOSTART_PROFILE" = production; then
|
|
196
|
+
mutate_butler_autostart ouro-butler true || return $?
|
|
197
|
+
AUTOSTART_EXPECTED_COUNTS="1 0 0 0"
|
|
198
|
+
else
|
|
199
|
+
mutate_butler_autostart ouro-butler false || return $?
|
|
200
|
+
AUTOSTART_EXPECTED_COUNTS="0 0 0 0"
|
|
201
|
+
fi
|
|
202
|
+
verify_butler_autostart "$AUTOSTART_EXPECTED_COUNTS" || return $?
|
|
203
|
+
snapshot_nonbutler_autostart >"$AUTOSTART_NONBUTLER_AFTER" || return $?
|
|
204
|
+
cmp -s -- "$AUTOSTART_NONBUTLER_BEFORE" "$AUTOSTART_NONBUTLER_AFTER" || return 1
|
|
205
|
+
sync -f "$AUTOSTART_FILE" || return $?
|
|
206
|
+
sync -f /var/lib/docker || return $?
|
|
207
|
+
AUTOSTART_COMMITTED=yes
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
verify_butler_autostart() {
|
|
211
|
+
(
|
|
212
|
+
EXPECTED_AUTOSTART_COUNTS=$1
|
|
213
|
+
AUTOSTART_COUNTS=$(butler_autostart_counts) || return $?
|
|
214
|
+
test "$AUTOSTART_COUNTS" = "$EXPECTED_AUTOSTART_COUNTS" || return $?
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
disable_butler_autostart() {
|
|
218
|
+
set_butler_autostart disabled || return $?
|
|
219
|
+
verify_butler_autostart "0 0 0 0" || return $?
|
|
220
|
+
}
|
|
221
|
+
enable_butler_autostart() {
|
|
222
|
+
set_butler_autostart production || return $?
|
|
223
|
+
verify_butler_autostart "1 0 0 0" || return $?
|
|
224
|
+
}
|
|
225
|
+
This bounded wait accepts only Docker's healthy state. The image healthcheck
|
|
226
|
+
verifies fresh daemon state plus exactly one managed Telegram process and one
|
|
227
|
+
managed scheduler process. It fails immediately on an exited, dead, or
|
|
228
|
+
unhealthy container and times out after four minutes:
|
|
229
|
+
wait_butler_ready() {
|
|
230
|
+
WAIT_CONTAINER=$1
|
|
231
|
+
WAIT_NOW=$(date +%s) || return $?
|
|
232
|
+
WAIT_DEADLINE=$(( WAIT_NOW + 240 ))
|
|
233
|
+
while :; do
|
|
234
|
+
WAIT_NOW=$(date +%s) || return $?
|
|
235
|
+
test "$WAIT_NOW" -lt "$WAIT_DEADLINE" || break
|
|
236
|
+
WAIT_STATE=$(docker inspect --format '{{.State.Status}} {{.State.Health.Status}}' "$WAIT_CONTAINER") || return $?
|
|
237
|
+
if test "$WAIT_STATE" = "running healthy"; then
|
|
238
|
+
return 0
|
|
239
|
+
fi
|
|
240
|
+
case "$WAIT_STATE" in
|
|
241
|
+
exited\ *|dead\ *|*\ unhealthy) return 1 ;;
|
|
242
|
+
esac
|
|
243
|
+
sleep 5 || return $?
|
|
244
|
+
done
|
|
245
|
+
return 1
|
|
246
|
+
}
|
|
247
|
+
Define these fail-closed image and topology helpers in the same shell. They use
|
|
248
|
+
exact Docker image IDs and container identity, image provenance, and writable
|
|
249
|
+
root overlap; no mutation is performed:
|
|
250
|
+
validate_exact_image_id() {
|
|
251
|
+
VALIDATE_IMAGE_ID=$1
|
|
252
|
+
case "$VALIDATE_IMAGE_ID" in
|
|
253
|
+
sha256:*) VALIDATE_IMAGE_HEX=${VALIDATE_IMAGE_ID#sha256:} ;;
|
|
254
|
+
*) return 1 ;;
|
|
255
|
+
esac
|
|
256
|
+
test "${#VALIDATE_IMAGE_HEX}" -eq 64 || return $?
|
|
257
|
+
case "$VALIDATE_IMAGE_HEX" in
|
|
258
|
+
*[!0-9a-f]*) return 1 ;;
|
|
259
|
+
esac
|
|
260
|
+
docker image inspect "$VALIDATE_IMAGE_ID" >/dev/null || return $?
|
|
261
|
+
}
|
|
262
|
+
assert_only_running_butler() {
|
|
263
|
+
(
|
|
264
|
+
EXPECTED_RUNNING_BUTLER=$1
|
|
265
|
+
case "$EXPECTED_RUNNING_BUTLER" in
|
|
266
|
+
-) EXPECTED_RUNNING_BUTLER_COUNT=0 ;;
|
|
267
|
+
ouro-butler|ouro-butler-staging) EXPECTED_RUNNING_BUTLER_COUNT=1 ;;
|
|
268
|
+
*) return 1 ;;
|
|
269
|
+
esac
|
|
270
|
+
RUNNING_CONTAINER_IDS=$(docker container ls -q) || return $?
|
|
271
|
+
MATCHING_RUNNING_BUTLERS=0
|
|
272
|
+
for RUNNING_CONTAINER_ID in $RUNNING_CONTAINER_IDS; do
|
|
273
|
+
RUNNING_CONTAINER_NAME=$(docker container inspect --format '{{.Name}}' "$RUNNING_CONTAINER_ID") || return $?
|
|
274
|
+
RUNNING_CONTAINER_NAME=${RUNNING_CONTAINER_NAME#/}
|
|
275
|
+
if test "$RUNNING_CONTAINER_NAME" = "$EXPECTED_RUNNING_BUTLER"; then
|
|
276
|
+
MATCHING_RUNNING_BUTLERS=$(( MATCHING_RUNNING_BUTLERS + 1 ))
|
|
277
|
+
continue
|
|
278
|
+
fi
|
|
279
|
+
RUNNING_CONTAINER_IMAGE_ID=$(docker container inspect --format '{{.Image}}' "$RUNNING_CONTAINER_ID") || return $?
|
|
280
|
+
RUNNING_CONTAINER_IMAGE_REF=$(docker container inspect --format '{{.Config.Image}}' "$RUNNING_CONTAINER_ID") || return $?
|
|
281
|
+
RUNNING_CONTAINER_IMAGE_SOURCE=$(docker image inspect --format '{{with .Config.Labels}}{{index . "org.opencontainers.image.source"}}{{end}}' "$RUNNING_CONTAINER_IMAGE_ID") || return $?
|
|
282
|
+
test "$RUNNING_CONTAINER_IMAGE_SOURCE" != https://github.com/ourostack/ouroboros || return 1
|
|
283
|
+
test "$RUNNING_CONTAINER_IMAGE_ID" != sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d || return 1
|
|
284
|
+
case "$RUNNING_CONTAINER_IMAGE_REF" in
|
|
285
|
+
ouro-butler|ouro-butler:*|ouro-butler@sha256:*|ouroboros-butler|ouroboros-butler:*|ouroboros-butler@sha256:*|ghcr.io/ourostack/ouroboros-butler|ghcr.io/ourostack/ouroboros-butler:*|ghcr.io/ourostack/ouroboros-butler@sha256:*) return 1 ;;
|
|
286
|
+
esac
|
|
287
|
+
RUNNING_WRITABLE_MOUNT_SOURCES=$(docker container inspect --format '{{range .Mounts}}{{if .RW}}{{println .Source}}{{end}}{{end}}' "$RUNNING_CONTAINER_ID") || return $?
|
|
288
|
+
if printf '%s\n' "$RUNNING_WRITABLE_MOUNT_SOURCES" | awk '
|
|
289
|
+
function overlaps(path, root) {
|
|
290
|
+
return length(path) > 0 && (path == root || index(path, root "/") == 1 || index(root, path "/") == 1)
|
|
291
|
+
}
|
|
292
|
+
overlaps($0, "/mnt/user/appdata/ouro-butler/runtime") || overlaps($0, "/mnt/user/appdata/ouro-butler/runtime/.ouro-cli") || overlaps($0, "/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro") || overlaps($0, "/mnt/user/appdata/ouro-butler/AgentBundles") { found = 1 }
|
|
293
|
+
END { exit found ? 0 : 1 }
|
|
294
|
+
'; then
|
|
295
|
+
return 1
|
|
296
|
+
fi
|
|
297
|
+
done
|
|
298
|
+
test "$MATCHING_RUNNING_BUTLERS" -eq "$EXPECTED_RUNNING_BUTLER_COUNT" || return $?
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
assert_update_topology() {
|
|
302
|
+
EXPECTED_ROLLBACK_IMAGE_ID=$1
|
|
303
|
+
UPDATE_CONTAINER_NAMES=$(docker container ls -a --format '{{.Names}}') || return $?
|
|
304
|
+
UPDATE_NAME_COUNTS=$(printf '%s\n' "$UPDATE_CONTAINER_NAMES" | awk '
|
|
305
|
+
$0 == "ouro-butler" { production++ }
|
|
306
|
+
$0 == "ouro-butler-staging" { staging++ }
|
|
307
|
+
$0 == "ouro-butler-rollback" { rollback++ }
|
|
308
|
+
$0 == "ouro-butler-legacy-evidence" { legacy++ }
|
|
309
|
+
END {
|
|
310
|
+
printf "%d %d %d %d", production + 0, staging + 0, rollback + 0, legacy + 0
|
|
311
|
+
}
|
|
312
|
+
') || return $?
|
|
313
|
+
case "$UPDATE_NAME_COUNTS" in
|
|
314
|
+
"1 0 0 0"|"1 0 1 0"|"1 0 0 1"|"1 0 1 1") ;;
|
|
315
|
+
*) return 1 ;;
|
|
316
|
+
esac
|
|
317
|
+
UPDATE_PRODUCTION_RUNNING=$(docker inspect --format '{{.State.Running}}' ouro-butler) || return $?
|
|
318
|
+
test "$UPDATE_PRODUCTION_RUNNING" = true || return $?
|
|
319
|
+
UPDATE_PRODUCTION_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler) || return $?
|
|
320
|
+
test "$UPDATE_PRODUCTION_IMAGE_ID" = "$EXPECTED_ROLLBACK_IMAGE_ID" || return $?
|
|
321
|
+
case "$UPDATE_NAME_COUNTS" in
|
|
322
|
+
"1 0 1 0"|"1 0 1 1")
|
|
323
|
+
UPDATE_ROLLBACK_RUNNING=$(docker inspect --format '{{.State.Running}}' ouro-butler-rollback) || return $?
|
|
324
|
+
test "$UPDATE_ROLLBACK_RUNNING" = false || return $?
|
|
325
|
+
UPDATE_ROLLBACK_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-rollback) || return $?
|
|
326
|
+
test "$UPDATE_ROLLBACK_IMAGE_ID" = "$EXPECTED_ROLLBACK_IMAGE_ID" || return $?
|
|
327
|
+
;;
|
|
328
|
+
esac
|
|
329
|
+
case "$UPDATE_NAME_COUNTS" in
|
|
330
|
+
"1 0 0 1"|"1 0 1 1")
|
|
331
|
+
UPDATE_LEGACY_RUNNING=$(docker inspect --format '{{.State.Running}}' ouro-butler-legacy-evidence) || return $?
|
|
332
|
+
test "$UPDATE_LEGACY_RUNNING" = false || return $?
|
|
333
|
+
UPDATE_LEGACY_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-legacy-evidence) || return $?
|
|
334
|
+
validate_exact_image_id "$UPDATE_LEGACY_IMAGE_ID" || return $?
|
|
335
|
+
;;
|
|
336
|
+
esac
|
|
337
|
+
assert_only_running_butler ouro-butler || return $?
|
|
338
|
+
}
|
|
339
|
+
assert_legacy_alpha742_source() {
|
|
340
|
+
EXPECTED_SOURCE_IMAGE_ID=$1
|
|
341
|
+
AUDIT_RUNNER_IMAGE_ID=$2
|
|
342
|
+
test "$EXPECTED_SOURCE_IMAGE_ID" = sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d || return $?
|
|
343
|
+
audit_effective ouro-butler "$EXPECTED_SOURCE_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" legacy-alpha742
|
|
344
|
+
}
|
|
345
|
+
assert_update_source() {
|
|
346
|
+
EXPECTED_SOURCE_IMAGE_ID=$1
|
|
347
|
+
AUDIT_RUNNER_IMAGE_ID=$2
|
|
348
|
+
if test "$EXPECTED_SOURCE_IMAGE_ID" = sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d; then
|
|
349
|
+
assert_legacy_alpha742_source "$EXPECTED_SOURCE_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID"
|
|
350
|
+
else
|
|
351
|
+
audit_effective ouro-butler "$EXPECTED_SOURCE_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID"
|
|
352
|
+
fi
|
|
353
|
+
}
|
|
354
|
+
validate_sanctuary_roots() {
|
|
355
|
+
(
|
|
356
|
+
VALIDATE_RUNTIME_ROOT=$1
|
|
357
|
+
VALIDATE_AGENT_ROOT=$2
|
|
358
|
+
test -d "$VALIDATE_RUNTIME_ROOT" || return $?
|
|
359
|
+
test -d "$VALIDATE_AGENT_ROOT" || return $?
|
|
360
|
+
VALIDATE_BAD_SHAPE=$(find "$VALIDATE_RUNTIME_ROOT" "$VALIDATE_AGENT_ROOT" -xdev \( -type l -o \( ! -type d -a ! -type f \) \) -print -quit) || return $?
|
|
361
|
+
test -z "$VALIDATE_BAD_SHAPE" || return $?
|
|
362
|
+
VALIDATE_UNEXPECTED_SOCKET=$(find "$VALIDATE_RUNTIME_ROOT" "$VALIDATE_AGENT_ROOT" -xdev -type s -print -quit) || return $?
|
|
363
|
+
test -z "$VALIDATE_UNEXPECTED_SOCKET" || return $?
|
|
364
|
+
for VALIDATE_REQUIRED_FILE in \
|
|
365
|
+
agent.json bundle-meta.json provider-readiness.json tool-profiles.json \
|
|
366
|
+
psyche/SOUL.md habits/sanctuary-health.md; do
|
|
367
|
+
test -s "$VALIDATE_AGENT_ROOT/$VALIDATE_REQUIRED_FILE" || return $?
|
|
368
|
+
done
|
|
369
|
+
test -d "$VALIDATE_RUNTIME_ROOT/vault-unlock" || return $?
|
|
370
|
+
VALIDATE_UNLOCK_FILES=$(find "$VALIDATE_RUNTIME_ROOT/vault-unlock" -xdev -mindepth 1 -maxdepth 1 -type f -name '*.secret' -size +0c -print) || return $?
|
|
371
|
+
test "$(printf '%s\n' "$VALIDATE_UNLOCK_FILES" | awk 'NF { count++ } END { print count + 0 }')" = 1 || return $?
|
|
372
|
+
test ! -e "$VALIDATE_RUNTIME_ROOT/container-credentials.json" || return $?
|
|
373
|
+
test ! -e "$VALIDATE_RUNTIME_ROOT/container-credentials.json.consuming" || return $?
|
|
374
|
+
test ! -S "$VALIDATE_AGENT_ROOT/state/acceptance/telegram-control.sock" || return $?
|
|
375
|
+
VALIDATE_WRONG_OWNER=$(find "$VALIDATE_RUNTIME_ROOT" "$VALIDATE_AGENT_ROOT" -xdev \( ! -user 10001 -o ! -group 10001 \) -print -quit) || return $?
|
|
376
|
+
test -z "$VALIDATE_WRONG_OWNER" || return $?
|
|
377
|
+
VALIDATE_WRONG_RUNTIME_DIR_MODE=$(find "$VALIDATE_RUNTIME_ROOT" -xdev -type d ! -perm 0700 \
|
|
378
|
+
! \( -perm 0755 \( \
|
|
379
|
+
-path "$VALIDATE_RUNTIME_ROOT/scheduler" -o -path "$VALIDATE_RUNTIME_ROOT/scheduler/*" -o \
|
|
380
|
+
-path "$VALIDATE_RUNTIME_ROOT/daemon/logs" -o -path "$VALIDATE_RUNTIME_ROOT/daemon/logs/*" -o \
|
|
381
|
+
-path "$VALIDATE_RUNTIME_ROOT/daemon/external-events" -o -path "$VALIDATE_RUNTIME_ROOT/daemon/external-events/*" \
|
|
382
|
+
\) \) -print -quit) || return $?
|
|
383
|
+
test -z "$VALIDATE_WRONG_RUNTIME_DIR_MODE" || return $?
|
|
384
|
+
VALIDATE_WRONG_AGENT_DIR_MODE=$(find "$VALIDATE_AGENT_ROOT" -xdev -type d ! -perm 0700 \
|
|
385
|
+
! \( -perm 0755 \( \
|
|
386
|
+
-path "$VALIDATE_AGENT_ROOT/arc/flight-recorder" -o -path "$VALIDATE_AGENT_ROOT/arc/flight-recorder/*" -o \
|
|
387
|
+
-path "$VALIDATE_AGENT_ROOT/state/health" -o -path "$VALIDATE_AGENT_ROOT/state/health/*" -o \
|
|
388
|
+
-path "$VALIDATE_AGENT_ROOT/state/logs" -o -path "$VALIDATE_AGENT_ROOT/state/logs/*" -o \
|
|
389
|
+
-path "$VALIDATE_AGENT_ROOT/state/habits" -o -path "$VALIDATE_AGENT_ROOT/state/habits/*" \
|
|
390
|
+
\) \) -print -quit) || return $?
|
|
391
|
+
test -z "$VALIDATE_WRONG_AGENT_DIR_MODE" || return $?
|
|
392
|
+
VALIDATE_WRONG_RUNTIME_FILE_MODE=$(find "$VALIDATE_RUNTIME_ROOT" -xdev -type f ! -perm 0600 \
|
|
393
|
+
! \( -perm 0644 \( \
|
|
394
|
+
-path "$VALIDATE_RUNTIME_ROOT/daemon/logs/*" -o \
|
|
395
|
+
-path "$VALIDATE_RUNTIME_ROOT/scheduler/*" -o \
|
|
396
|
+
-path "$VALIDATE_RUNTIME_ROOT/pulse.json" -o \
|
|
397
|
+
-path "$VALIDATE_RUNTIME_ROOT/pulse-delivered.json" -o \
|
|
398
|
+
-path "$VALIDATE_RUNTIME_ROOT/daemon.pids" -o \
|
|
399
|
+
-path "$VALIDATE_RUNTIME_ROOT/daemon-health.json" \
|
|
400
|
+
\) \) -print -quit) || return $?
|
|
401
|
+
test -z "$VALIDATE_WRONG_RUNTIME_FILE_MODE" || return $?
|
|
402
|
+
# Alpha.753 and earlier wrote content-free autonomy receipts as 0644 below a 0700 tree.
|
|
403
|
+
# Preserve those audit records; current writers create and normalize this exact format to 0600.
|
|
404
|
+
VALIDATE_WRONG_AGENT_FILE_MODE=$(find "$VALIDATE_AGENT_ROOT" -xdev -type f ! -perm 0600 \
|
|
405
|
+
! \( -perm 0644 \( \
|
|
406
|
+
-path "$VALIDATE_AGENT_ROOT/arc/flight-recorder/*" -o \
|
|
407
|
+
-path "$VALIDATE_AGENT_ROOT/state/health/*" -o \
|
|
408
|
+
-path "$VALIDATE_AGENT_ROOT/state/logs/*" -o \
|
|
409
|
+
-path "$VALIDATE_AGENT_ROOT/state/habits/*" -o \
|
|
410
|
+
-path "$VALIDATE_AGENT_ROOT/state/arc/context-loss-sentinel-watermark.json" -o \
|
|
411
|
+
-exec /bin/sh -c '
|
|
412
|
+
VALIDATE_AUTONOMY_RECEIPT=$1
|
|
413
|
+
VALIDATE_AUTONOMY_RECEIPTS_ROOT=$2
|
|
414
|
+
test "${VALIDATE_AUTONOMY_RECEIPT%/*}" = "$VALIDATE_AUTONOMY_RECEIPTS_ROOT" || exit 1
|
|
415
|
+
VALIDATE_AUTONOMY_RECEIPT_NAME=${VALIDATE_AUTONOMY_RECEIPT##*/}
|
|
416
|
+
case "$VALIDATE_AUTONOMY_RECEIPT_NAME" in
|
|
417
|
+
autr_????????????????????????????????.json) ;;
|
|
418
|
+
*) exit 1 ;;
|
|
419
|
+
esac
|
|
420
|
+
VALIDATE_AUTONOMY_RECEIPT_ID=${VALIDATE_AUTONOMY_RECEIPT_NAME#autr_}
|
|
421
|
+
VALIDATE_AUTONOMY_RECEIPT_ID=${VALIDATE_AUTONOMY_RECEIPT_ID%.json}
|
|
422
|
+
case "$VALIDATE_AUTONOMY_RECEIPT_ID" in *[!0-9a-f]*) exit 1 ;; esac
|
|
423
|
+
' sanctuary-autonomy-receipt-validator {} "$VALIDATE_AGENT_ROOT/state/autonomy/receipts" \; \
|
|
424
|
+
\) \) -print -quit) || return $?
|
|
425
|
+
test -z "$VALIDATE_WRONG_AGENT_FILE_MODE" || return $?
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
verify_sanctuary_snapshot_provenance() {
|
|
429
|
+
(
|
|
430
|
+
SNAPSHOT_ROOT=$1
|
|
431
|
+
EXPECTED_SNAPSHOT_IMAGE_ID=$2
|
|
432
|
+
SNAPSHOT_PROVENANCE_ROOT=$SNAPSHOT_ROOT/provenance
|
|
433
|
+
for SNAPSHOT_TOP_LEVEL_DIRECTORY in runtime agent host provenance; do
|
|
434
|
+
test -d "$SNAPSHOT_ROOT/$SNAPSHOT_TOP_LEVEL_DIRECTORY" || return $?
|
|
435
|
+
test ! -L "$SNAPSHOT_ROOT/$SNAPSHOT_TOP_LEVEL_DIRECTORY" || return 1
|
|
436
|
+
done
|
|
437
|
+
test "$(find "$SNAPSHOT_ROOT" -xdev -mindepth 1 -maxdepth 1 -print | wc -l)" -eq 4 || return $?
|
|
438
|
+
test -d "$SNAPSHOT_PROVENANCE_ROOT" || return $?
|
|
439
|
+
test ! -L "$SNAPSHOT_PROVENANCE_ROOT" || return 1
|
|
440
|
+
test "$(stat -c '%u:%g:%a' "$SNAPSHOT_PROVENANCE_ROOT")" = 0:0:700 || return $?
|
|
441
|
+
SNAPSHOT_BAD_PROVENANCE_ENTRY=$(find "$SNAPSHOT_PROVENANCE_ROOT" -xdev -mindepth 1 ! -type f -print -quit) || return $?
|
|
442
|
+
test -z "$SNAPSHOT_BAD_PROVENANCE_ENTRY" || return $?
|
|
443
|
+
for SNAPSHOT_PROVENANCE_FILE in image-id container-inspect.json image-inspect.json package-version manifest.sha256; do
|
|
444
|
+
test -f "$SNAPSHOT_PROVENANCE_ROOT/$SNAPSHOT_PROVENANCE_FILE" || return $?
|
|
445
|
+
test ! -L "$SNAPSHOT_PROVENANCE_ROOT/$SNAPSHOT_PROVENANCE_FILE" || return 1
|
|
446
|
+
test "$(stat -c '%u:%g:%a' "$SNAPSHOT_PROVENANCE_ROOT/$SNAPSHOT_PROVENANCE_FILE")" = 0:0:600 || return $?
|
|
447
|
+
done
|
|
448
|
+
test "$(find "$SNAPSHOT_PROVENANCE_ROOT" -xdev -mindepth 1 -maxdepth 1 -type f | wc -l)" -eq 5 || return $?
|
|
449
|
+
test "$(wc -l <"$SNAPSHOT_PROVENANCE_ROOT/image-id")" -eq 1 || return $?
|
|
450
|
+
IFS= read -r SNAPSHOT_IMAGE_ID <"$SNAPSHOT_PROVENANCE_ROOT/image-id" || return $?
|
|
451
|
+
test "$SNAPSHOT_IMAGE_ID" = "$EXPECTED_SNAPSHOT_IMAGE_ID" || return $?
|
|
452
|
+
case "$SNAPSHOT_IMAGE_ID" in sha256:????????????????????????????????????????????????????????????????) ;; *) return 1 ;; esac
|
|
453
|
+
case "${SNAPSHOT_IMAGE_ID#sha256:}" in *[!0-9a-f]*) return 1 ;; esac
|
|
454
|
+
/usr/local/bin/node -e '
|
|
455
|
+
const fs = require("node:fs");
|
|
456
|
+
const [containerPath, imagePath, imageId, hostPath] = process.argv.slice(1);
|
|
457
|
+
const container = JSON.parse(fs.readFileSync(containerPath, "utf8"));
|
|
458
|
+
const image = JSON.parse(fs.readFileSync(imagePath, "utf8"));
|
|
459
|
+
if (!Array.isArray(container) || container.length !== 1 || !container[0] || container[0].Image !== imageId) process.exit(1);
|
|
460
|
+
if (!Array.isArray(image) || image.length !== 1 || !image[0] || image[0].Id !== imageId) process.exit(1);
|
|
461
|
+
const source = image[0].Config?.Labels?.["org.opencontainers.image.source"];
|
|
462
|
+
const legacyAlpha742 = "sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d";
|
|
463
|
+
if (source !== "https://github.com/ourostack/ouroboros" && !(source === undefined && imageId === legacyAlpha742)) process.exit(1);
|
|
464
|
+
const expected = ["custom/usenet_health.sh", "custom/ouro-events/bootstrap-spool.sh", "custom/ouro-events/emit-event.mjs", "custom/ouro-events/emit-usenet-event.sh", "custom/ouro-events/install-usenet-guard.sh", "go.butler-lines", "crontab.butler-lines", "global-state"];
|
|
465
|
+
const expectedUid = 0;
|
|
466
|
+
const expectedGid = 0;
|
|
467
|
+
const hostRoot = fs.lstatSync(hostPath);
|
|
468
|
+
if (!hostRoot.isDirectory() || hostRoot.isSymbolicLink() || hostRoot.uid !== expectedUid || hostRoot.gid !== expectedGid || (hostRoot.mode & 0o777) !== 0o700) process.exit(1);
|
|
469
|
+
const rows = fs.readFileSync(`${hostPath}/inventory`, "utf8").trim().split("\n").map(line => line.split("\t"));
|
|
470
|
+
if (rows.length !== expected.length || rows.some((row, index) => row.length !== 2 || row[1] !== expected[index] || !["present", "absent"].includes(row[0]))) process.exit(1);
|
|
471
|
+
const present = rows.filter(row => row[0] === "present").map(row => row[1]);
|
|
472
|
+
if (!["go.butler-lines", "crontab.butler-lines", "global-state"].every(relative => present.includes(relative))) process.exit(1);
|
|
473
|
+
const globalRows = fs.readFileSync(`${hostPath}/global-state`, "utf8").trim().split("\n").map(line => line.split("\t"));
|
|
474
|
+
if (globalRows.length !== 2 || globalRows[0][0] !== "go" || globalRows[1][0] !== "crontab") process.exit(1);
|
|
475
|
+
for (const [name, state, digest, count] of globalRows) {
|
|
476
|
+
if (!["go", "crontab"].includes(name) || !["present", "absent"].includes(state) || !/^(?:[0-9a-f]{64}|-)$/.test(digest) || !/^(?:0|[1-9][0-9]*)$/.test(count)) process.exit(1);
|
|
477
|
+
if ((state === "absent") !== (digest === "-")) process.exit(1);
|
|
478
|
+
const fragmentLines = fs.readFileSync(`${hostPath}/${name}.butler-lines`, "utf8").split("\n").filter(Boolean);
|
|
479
|
+
if (fragmentLines.length !== Number(count)) process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
for (const [state, relative] of rows) {
|
|
482
|
+
if (fs.existsSync(`${hostPath}/${relative}`) !== (state === "present")) process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
const files = [];
|
|
485
|
+
const directories = [];
|
|
486
|
+
const walk = (directory, prefix = "") => {
|
|
487
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
488
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
489
|
+
const metadata = fs.lstatSync(`${directory}/${entry.name}`);
|
|
490
|
+
if (metadata.uid !== expectedUid || metadata.gid !== expectedGid) process.exit(1);
|
|
491
|
+
if (entry.isDirectory() && (metadata.mode & 0o777) === 0o700) { directories.push(relative); walk(`${directory}/${entry.name}`, relative); }
|
|
492
|
+
else if (entry.isFile() && (metadata.mode & 0o777) === 0o600) files.push(relative);
|
|
493
|
+
else process.exit(1);
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
walk(hostPath);
|
|
497
|
+
if (JSON.stringify(directories.sort()) !== JSON.stringify(["custom", "custom/ouro-events"])) process.exit(1);
|
|
498
|
+
if (JSON.stringify(files.sort()) !== JSON.stringify(["inventory", ...present].sort())) process.exit(1);
|
|
499
|
+
' "$SNAPSHOT_PROVENANCE_ROOT/container-inspect.json" "$SNAPSHOT_PROVENANCE_ROOT/image-inspect.json" "$SNAPSHOT_IMAGE_ID" "$SNAPSHOT_ROOT/host" || return $?
|
|
500
|
+
test "$(wc -l <"$SNAPSHOT_PROVENANCE_ROOT/package-version")" -eq 1 || return $?
|
|
501
|
+
grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' "$SNAPSHOT_PROVENANCE_ROOT/package-version" || return $?
|
|
502
|
+
test "$(stat -c '%u:%g:%a' "$SNAPSHOT_ROOT/host")" = 0:0:700 || return $?
|
|
503
|
+
SNAPSHOT_BAD_HOST_ENTRY=$(find "$SNAPSHOT_ROOT/host" -xdev \( \( -type d ! -perm 0700 \) -o \( -type f ! -perm 0600 \) -o \( ! -type d -a ! -type f \) \) -print -quit) || return $?
|
|
504
|
+
test -z "$SNAPSHOT_BAD_HOST_ENTRY" || return $?
|
|
505
|
+
test -f "$SNAPSHOT_ROOT/host/inventory" || return $?
|
|
506
|
+
test ! -e "$SNAPSHOT_ROOT/host/notify.conf" || return 1
|
|
507
|
+
test ! -e "$SNAPSHOT_ROOT/host/sabnzbd.ini" || return 1
|
|
508
|
+
SNAPSHOT_VERIFY_ROOT=$(mktemp -d /tmp/ouro-snapshot-verify.XXXXXX) || return $?
|
|
509
|
+
trap 'rm -f -- "$SNAPSHOT_VERIFY_ROOT/current-files" "$SNAPSHOT_VERIFY_ROOT/manifest-files"; rmdir -- "$SNAPSHOT_VERIFY_ROOT"' EXIT || return $?
|
|
510
|
+
(
|
|
511
|
+
cd "$SNAPSHOT_ROOT" || return $?
|
|
512
|
+
find runtime agent host provenance -xdev -type f ! -path provenance/manifest.sha256 -print | LC_ALL=C sort >"$SNAPSHOT_VERIFY_ROOT/current-files" || return $?
|
|
513
|
+
grep -E '^[0-9a-f]{64} [^/].+$' provenance/manifest.sha256 >/dev/null || return $?
|
|
514
|
+
! grep -Ev '^[0-9a-f]{64} [^/].+$' provenance/manifest.sha256 >/dev/null || return 1
|
|
515
|
+
sed 's/^[0-9a-f]\{64\} //' provenance/manifest.sha256 | LC_ALL=C sort >"$SNAPSHOT_VERIFY_ROOT/manifest-files" || return $?
|
|
516
|
+
cmp -s "$SNAPSHOT_VERIFY_ROOT/current-files" "$SNAPSHOT_VERIFY_ROOT/manifest-files" || return $?
|
|
517
|
+
sha256sum -c -- provenance/manifest.sha256 >/dev/null || return $?
|
|
518
|
+
) || return $?
|
|
519
|
+
rm -f -- "$SNAPSHOT_VERIFY_ROOT/current-files" "$SNAPSHOT_VERIFY_ROOT/manifest-files" || return $?
|
|
520
|
+
rmdir -- "$SNAPSHOT_VERIFY_ROOT" || return $?
|
|
521
|
+
trap - EXIT || return $?
|
|
522
|
+
)
|
|
523
|
+
}
|
|
524
|
+
assert_restore_preflight() {
|
|
525
|
+
(
|
|
526
|
+
test -n "${BACKUP_ROOT-}" || return 1
|
|
527
|
+
test -n "${IMAGE_ID-}" || return 1
|
|
528
|
+
test -n "${AUDIT_RUNNER_IMAGE_ID-}" || return 1
|
|
529
|
+
case "$BACKUP_ROOT" in
|
|
530
|
+
/*) ;;
|
|
531
|
+
*) return 1 ;;
|
|
532
|
+
esac
|
|
533
|
+
test "$BACKUP_ROOT" != / || return $?
|
|
534
|
+
RESTORE_BACKUP_ROOT=$(cd -- "$BACKUP_ROOT" && pwd -P) || return $?
|
|
535
|
+
test "$RESTORE_BACKUP_ROOT" = "$BACKUP_ROOT" || return $?
|
|
536
|
+
test -d "$BACKUP_ROOT/runtime/.ouro-cli" || return $?
|
|
537
|
+
test -d "$BACKUP_ROOT/agent/sanctuary.ouro" || return $?
|
|
538
|
+
verify_sanctuary_snapshot_provenance "$BACKUP_ROOT" "$IMAGE_ID" || return $?
|
|
539
|
+
validate_sanctuary_roots "$BACKUP_ROOT/runtime/.ouro-cli" "$BACKUP_ROOT/agent/sanctuary.ouro" || return $?
|
|
540
|
+
validate_exact_image_id "$IMAGE_ID" || return $?
|
|
541
|
+
validate_exact_image_id "$AUDIT_RUNNER_IMAGE_ID" || return $?
|
|
542
|
+
test "$AUDIT_RUNNER_IMAGE_ID" != sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d || return $?
|
|
543
|
+
RESTORE_CONTAINER_NAMES=$(docker container ls -a --format '{{.Names}}') || return $?
|
|
544
|
+
RESTORE_NAME_COUNTS=$(printf '%s\n' "$RESTORE_CONTAINER_NAMES" | awk '
|
|
545
|
+
$0 == "ouro-butler" { production++ }
|
|
546
|
+
$0 == "ouro-butler-staging" { staging++ }
|
|
547
|
+
$0 == "ouro-butler-rollback" { rollback++ }
|
|
548
|
+
$0 == "ouro-butler-legacy-evidence" { legacy++ }
|
|
549
|
+
END {
|
|
550
|
+
printf "%d %d %d %d", production + 0, staging + 0, rollback + 0, legacy + 0
|
|
551
|
+
}
|
|
552
|
+
') || return $?
|
|
553
|
+
case "$RESTORE_NAME_COUNTS" in
|
|
554
|
+
"1 0 0 0"|"1 0 0 1") ;;
|
|
555
|
+
*) return 1 ;;
|
|
556
|
+
esac
|
|
557
|
+
RESTORE_PRODUCTION_RUNNING=$(docker inspect --format '{{.State.Running}}' ouro-butler) || return $?
|
|
558
|
+
test "$RESTORE_PRODUCTION_RUNNING" = true || return $?
|
|
559
|
+
RESTORE_PRODUCTION_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler) || return $?
|
|
560
|
+
validate_exact_image_id "$RESTORE_PRODUCTION_IMAGE_ID" || return $?
|
|
561
|
+
if test "$RESTORE_NAME_COUNTS" = "1 0 0 1"; then
|
|
562
|
+
RESTORE_LEGACY_RUNNING=$(docker inspect --format '{{.State.Running}}' ouro-butler-legacy-evidence) || return $?
|
|
563
|
+
test "$RESTORE_LEGACY_RUNNING" = false || return $?
|
|
564
|
+
RESTORE_LEGACY_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-legacy-evidence) || return $?
|
|
565
|
+
validate_exact_image_id "$RESTORE_LEGACY_IMAGE_ID" || return $?
|
|
566
|
+
fi
|
|
567
|
+
assert_only_running_butler ouro-butler || return $?
|
|
568
|
+
assert_update_source "$RESTORE_PRODUCTION_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" || return $?
|
|
569
|
+
)
|
|
570
|
+
}
|
|
571
|
+
ensure_sanctuary_machine_identity() {
|
|
572
|
+
MACHINE_PATH=$1
|
|
573
|
+
test ! -L "$MACHINE_PATH" || return 1
|
|
574
|
+
/usr/local/bin/node -e '
|
|
575
|
+
const fs = require("node:fs");
|
|
576
|
+
const path = process.argv[1];
|
|
577
|
+
const valid = value => value && value.schemaVersion === 1 && value.machineId === "sanctuary"
|
|
578
|
+
&& typeof value.createdAt === "string" && typeof value.updatedAt === "string"
|
|
579
|
+
&& Array.isArray(value.hostnameAliases);
|
|
580
|
+
if (fs.existsSync(path)) {
|
|
581
|
+
if (!valid(JSON.parse(fs.readFileSync(path, "utf8")))) process.exit(1);
|
|
582
|
+
process.exit(0);
|
|
583
|
+
}
|
|
584
|
+
const now = new Date().toISOString();
|
|
585
|
+
const value = { schemaVersion: 1, machineId: "sanctuary", createdAt: now, updatedAt: now, hostnameAliases: [] };
|
|
586
|
+
const temporary = `${path}.tmp.${process.pid}`;
|
|
587
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
588
|
+
fs.renameSync(temporary, path);
|
|
589
|
+
' "$MACHINE_PATH" || return $?
|
|
590
|
+
chown 10001:10001 "$MACHINE_PATH" || return $?
|
|
591
|
+
chmod 0600 "$MACHINE_PATH" || return $?
|
|
592
|
+
sync -f "$MACHINE_PATH" || return $?
|
|
593
|
+
}
|
|
594
|
+
migrate_sanctuary_package_managed_bundle() {
|
|
595
|
+
MIGRATE_IMAGE_ID=$1
|
|
596
|
+
MIGRATE_OPERATION=$2
|
|
597
|
+
MIGRATE_ROLLBACK_IMAGE_ID=${3-}
|
|
598
|
+
validate_exact_image_id "$MIGRATE_IMAGE_ID" || return $?
|
|
599
|
+
case "$MIGRATE_OPERATION" in migrate|rollback|commit|status) ;; *) return 1 ;; esac
|
|
600
|
+
if test "$MIGRATE_OPERATION" = migrate; then
|
|
601
|
+
validate_exact_image_id "$MIGRATE_ROLLBACK_IMAGE_ID" || return $?
|
|
602
|
+
test "$MIGRATE_IMAGE_ID" != "$MIGRATE_ROLLBACK_IMAGE_ID" || return 1
|
|
603
|
+
set -- --rollback-image-id "$MIGRATE_ROLLBACK_IMAGE_ID" --target-image-id "$MIGRATE_IMAGE_ID"
|
|
604
|
+
else
|
|
605
|
+
set --
|
|
606
|
+
fi
|
|
607
|
+
docker run --rm --pull=never --network=none --read-only --user 10001:10001 \
|
|
608
|
+
--entrypoint /usr/local/bin/node \
|
|
609
|
+
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
610
|
+
"$MIGRATE_IMAGE_ID" /opt/ouro/deploy/unraid/migrate-sanctuary-bundle.mjs \
|
|
611
|
+
--package-root /opt/ouro/deploy/unraid/sanctuary.ouro \
|
|
612
|
+
--agent-root /home/ouro/AgentBundles/sanctuary.ouro \
|
|
613
|
+
--operation "$MIGRATE_OPERATION" "$@" || return $?
|
|
614
|
+
}
|
|
615
|
+
recover_pending_sanctuary_bundle_migration() {
|
|
616
|
+
RECOVERY_IMAGE_ID=$1
|
|
617
|
+
RECOVERY_AGENT_ROOT=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro
|
|
618
|
+
RECOVERY_RECORD=$RECOVERY_AGENT_ROOT/.sanctuary-package-managed-rollback.json
|
|
619
|
+
RECOVERY_COMMITTING_RECORD=$RECOVERY_RECORD.committing
|
|
620
|
+
RECOVERY_RECORD_COUNT=0
|
|
621
|
+
for RECOVERY_CANDIDATE in "$RECOVERY_RECORD" "$RECOVERY_COMMITTING_RECORD"; do
|
|
622
|
+
test ! -L "$RECOVERY_CANDIDATE" || return 1
|
|
623
|
+
if test -e "$RECOVERY_CANDIDATE"; then
|
|
624
|
+
test -f "$RECOVERY_CANDIDATE" || return 1
|
|
625
|
+
RECOVERY_RECORD_COUNT=$((RECOVERY_RECORD_COUNT + 1))
|
|
626
|
+
fi
|
|
627
|
+
done
|
|
628
|
+
test "$RECOVERY_RECORD_COUNT" -le 1 || return 1
|
|
629
|
+
test "$RECOVERY_RECORD_COUNT" -eq 1 || return 0
|
|
630
|
+
RECOVERY_STATUS=$(migrate_sanctuary_package_managed_bundle "$RECOVERY_IMAGE_ID" status) || return $?
|
|
631
|
+
RECOVERY_IDENTITIES=$(printf '%s' "$RECOVERY_STATUS" | /usr/local/bin/node -e '
|
|
632
|
+
let input = "";
|
|
633
|
+
process.stdin.setEncoding("utf8");
|
|
634
|
+
process.stdin.on("data", chunk => { input += chunk; });
|
|
635
|
+
process.stdin.on("end", () => {
|
|
636
|
+
const value = JSON.parse(input);
|
|
637
|
+
if (!value || !["rollback", "committing"].includes(value.state)
|
|
638
|
+
|| typeof value.rollbackImageId !== "string" || typeof value.targetImageId !== "string") process.exit(1);
|
|
639
|
+
process.stdout.write(`${value.state} ${value.rollbackImageId} ${value.targetImageId}`);
|
|
640
|
+
});
|
|
641
|
+
') || return $?
|
|
642
|
+
set -- $RECOVERY_IDENTITIES
|
|
643
|
+
test "$#" -eq 3 || return 1
|
|
644
|
+
RECOVERY_STATE=$1
|
|
645
|
+
RECOVERY_ROLLBACK_IMAGE_ID=$2
|
|
646
|
+
RECOVERY_TARGET_IMAGE_ID=$3
|
|
647
|
+
validate_exact_image_id "$RECOVERY_ROLLBACK_IMAGE_ID" || return $?
|
|
648
|
+
validate_exact_image_id "$RECOVERY_TARGET_IMAGE_ID" || return $?
|
|
649
|
+
test "$RECOVERY_TARGET_IMAGE_ID" != "$RECOVERY_ROLLBACK_IMAGE_ID" || return 1
|
|
650
|
+
disable_butler_autostart || return $?
|
|
651
|
+
if test "$RECOVERY_STATE" = committing; then
|
|
652
|
+
! docker container inspect ouro-butler-staging >/dev/null 2>&1 || return 1
|
|
653
|
+
RECOVERY_PRODUCTION_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler) || return $?
|
|
654
|
+
test "$(docker inspect --format '{{.State.Running}}' ouro-butler)" = true || return 1
|
|
655
|
+
if test "$RECOVERY_PRODUCTION_IMAGE_ID" = "$RECOVERY_TARGET_IMAGE_ID"; then
|
|
656
|
+
RECOVERY_CURRENT_ROLLBACK_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-rollback) || return $?
|
|
657
|
+
test "$RECOVERY_CURRENT_ROLLBACK_IMAGE_ID" = "$RECOVERY_ROLLBACK_IMAGE_ID" || return 1
|
|
658
|
+
test "$(docker inspect --format '{{.State.Running}}' ouro-butler-rollback)" = false || return 1
|
|
659
|
+
elif test "$RECOVERY_PRODUCTION_IMAGE_ID" = "$RECOVERY_ROLLBACK_IMAGE_ID"; then
|
|
660
|
+
! docker container inspect ouro-butler-rollback >/dev/null 2>&1 || return 1
|
|
661
|
+
else
|
|
662
|
+
return 1
|
|
663
|
+
fi
|
|
664
|
+
assert_only_running_butler ouro-butler || return $?
|
|
665
|
+
assert_update_source "$RECOVERY_PRODUCTION_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" || return $?
|
|
666
|
+
wait_butler_ready ouro-butler || return $?
|
|
667
|
+
enable_butler_autostart || return $?
|
|
668
|
+
migrate_sanctuary_package_managed_bundle "$RECOVERY_IMAGE_ID" commit || return $?
|
|
669
|
+
return 0
|
|
670
|
+
fi
|
|
671
|
+
if docker container inspect ouro-butler-rollback >/dev/null 2>&1; then
|
|
672
|
+
RECOVERY_CURRENT_ROLLBACK_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-rollback) || return $?
|
|
673
|
+
test "$RECOVERY_CURRENT_ROLLBACK_IMAGE_ID" = "$RECOVERY_ROLLBACK_IMAGE_ID" || return 1
|
|
674
|
+
test "$(docker inspect --format '{{.State.Running}}' ouro-butler-rollback)" = false || return 1
|
|
675
|
+
if docker container inspect ouro-butler-staging >/dev/null 2>&1; then
|
|
676
|
+
RECOVERY_STAGING_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-staging) || return $?
|
|
677
|
+
test "$RECOVERY_STAGING_IMAGE_ID" = "$RECOVERY_TARGET_IMAGE_ID" || return 1
|
|
678
|
+
docker stop ouro-butler-staging >/dev/null 2>&1 || true
|
|
679
|
+
docker rm --force ouro-butler-staging || return $?
|
|
680
|
+
fi
|
|
681
|
+
if docker container inspect ouro-butler >/dev/null 2>&1; then
|
|
682
|
+
RECOVERY_PRODUCTION_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler) || return $?
|
|
683
|
+
test "$RECOVERY_PRODUCTION_IMAGE_ID" = "$RECOVERY_TARGET_IMAGE_ID" || return 1
|
|
684
|
+
docker stop ouro-butler >/dev/null 2>&1 || true
|
|
685
|
+
docker rm --force ouro-butler || return $?
|
|
686
|
+
fi
|
|
687
|
+
migrate_sanctuary_package_managed_bundle "$RECOVERY_IMAGE_ID" rollback || return $?
|
|
688
|
+
docker rename ouro-butler-rollback ouro-butler || return $?
|
|
689
|
+
elif docker container inspect ouro-butler >/dev/null 2>&1; then
|
|
690
|
+
! docker container inspect ouro-butler-staging >/dev/null 2>&1 || return 1
|
|
691
|
+
RECOVERY_PRODUCTION_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler) || return $?
|
|
692
|
+
test "$RECOVERY_PRODUCTION_IMAGE_ID" = "$RECOVERY_ROLLBACK_IMAGE_ID" || return 1
|
|
693
|
+
! docker container inspect ouro-butler-rollback >/dev/null 2>&1 || return 1
|
|
694
|
+
else
|
|
695
|
+
return 1
|
|
696
|
+
fi
|
|
697
|
+
RECOVERY_PRODUCTION_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler) || return $?
|
|
698
|
+
test "$RECOVERY_PRODUCTION_IMAGE_ID" = "$RECOVERY_ROLLBACK_IMAGE_ID" || return 1
|
|
699
|
+
assert_update_source "$RECOVERY_ROLLBACK_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" || return $?
|
|
700
|
+
docker start ouro-butler || return $?
|
|
701
|
+
assert_only_running_butler ouro-butler || return $?
|
|
702
|
+
wait_butler_ready ouro-butler || return $?
|
|
703
|
+
enable_butler_autostart || return $?
|
|
704
|
+
migrate_sanctuary_package_managed_bundle "$RECOVERY_IMAGE_ID" commit || return $?
|
|
705
|
+
}
|
|
706
|
+
rollback_sanctuary_bundle_if_pending() {
|
|
707
|
+
OPTIONAL_ROLLBACK_IMAGE_ID=$1
|
|
708
|
+
OPTIONAL_ROLLBACK_RECORD=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro/.sanctuary-package-managed-rollback.json
|
|
709
|
+
OPTIONAL_COMMITTING_RECORD=$OPTIONAL_ROLLBACK_RECORD.committing
|
|
710
|
+
test ! -L "$OPTIONAL_ROLLBACK_RECORD" || return 1
|
|
711
|
+
test ! -L "$OPTIONAL_COMMITTING_RECORD" || return 1
|
|
712
|
+
test ! -e "$OPTIONAL_COMMITTING_RECORD" || return 1
|
|
713
|
+
if test -e "$OPTIONAL_ROLLBACK_RECORD"; then
|
|
714
|
+
test -f "$OPTIONAL_ROLLBACK_RECORD" || return 1
|
|
715
|
+
migrate_sanctuary_package_managed_bundle "$OPTIONAL_ROLLBACK_IMAGE_ID" rollback || return $?
|
|
716
|
+
fi
|
|
717
|
+
}
|
|
718
|
+
# Package installation and migration never grant restart authority. The
|
|
719
|
+
# migration is the pre-activation assertion: it fails before live mutation
|
|
720
|
+
# if packaged steward.json carries any routine grant. It preserves an
|
|
721
|
+
# existing live steward policy and its audit receipts byte for byte. On a
|
|
722
|
+
# new installation, start the healthy private runtime, then use
|
|
723
|
+
# an authenticated owner Telegram turn to grant each exact restart target
|
|
724
|
+
# through steward_policy_manage. That existing tool binds the grant to the
|
|
725
|
+
# verified owner relationship and durable session event. Do not seed grants
|
|
726
|
+
# in the packaged steward.json, copy an old session-event ID, or edit the
|
|
727
|
+
# live policy/audit files directly. A missing grant remains fail-closed until
|
|
728
|
+
# that explicit owner ceremony succeeds.
|
|
729
|
+
prepare_canonical_sanctuary_roots() {
|
|
730
|
+
PREPARE_IMAGE_ID=$1
|
|
731
|
+
validate_exact_image_id "$PREPARE_IMAGE_ID" || return $?
|
|
732
|
+
PREPARE_RUNTIME_ROOT=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli
|
|
733
|
+
PREPARE_AGENT_ROOT=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro
|
|
734
|
+
install -d -m 0700 -o 10001 -g 10001 "$PREPARE_RUNTIME_ROOT" || return $?
|
|
735
|
+
install -d -m 0700 -o 10001 -g 10001 "$PREPARE_AGENT_ROOT" || return $?
|
|
736
|
+
ensure_sanctuary_machine_identity "$PREPARE_RUNTIME_ROOT/machine.json" || return $?
|
|
737
|
+
if test ! -f "$PREPARE_AGENT_ROOT/agent.json"; then
|
|
738
|
+
PREPARE_EXISTING_ENTRY=$(find "$PREPARE_AGENT_ROOT" -mindepth 1 -print -quit) || return $?
|
|
739
|
+
test -z "$PREPARE_EXISTING_ENTRY" || return $?
|
|
740
|
+
! docker container inspect ouro-butler-bundle-bootstrap >/dev/null 2>&1 || return 1
|
|
741
|
+
docker run --rm --pull=never --network=none --name ouro-butler-bundle-bootstrap --user 10001:10001 \
|
|
742
|
+
--mount "type=bind,src=$PREPARE_RUNTIME_ROOT,dst=/home/ouro/.ouro-cli" \
|
|
743
|
+
--mount "type=bind,src=$PREPARE_AGENT_ROOT,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
744
|
+
--entrypoint /bin/sh "$PREPARE_IMAGE_ID" -ceu '
|
|
745
|
+
cp -R /opt/ouro/deploy/unraid/sanctuary.ouro/. /home/ouro/AgentBundles/sanctuary.ouro/
|
|
746
|
+
find /home/ouro/AgentBundles/sanctuary.ouro -type d -exec chmod 0700 {} +
|
|
747
|
+
find /home/ouro/AgentBundles/sanctuary.ouro -type f -exec chmod 0600 {} +
|
|
748
|
+
' || return $?
|
|
749
|
+
! docker container inspect ouro-butler-bundle-bootstrap >/dev/null 2>&1 || return 1
|
|
750
|
+
fi
|
|
751
|
+
for PREPARE_REQUIRED_FILE in \
|
|
752
|
+
agent.json bundle-meta.json provider-readiness.json tool-profiles.json \
|
|
753
|
+
psyche/SOUL.md habits/sanctuary-health.md; do
|
|
754
|
+
test -f "$PREPARE_AGENT_ROOT/$PREPARE_REQUIRED_FILE" || return $?
|
|
755
|
+
done
|
|
756
|
+
PREPARE_WRONG_OWNER=$(find "$PREPARE_RUNTIME_ROOT" "$PREPARE_AGENT_ROOT" \( ! -user 10001 -o ! -group 10001 \) -print -quit) || return $?
|
|
757
|
+
test -z "$PREPARE_WRONG_OWNER" || return $?
|
|
758
|
+
PREPARE_WRONG_DIR_MODE=$(find "$PREPARE_RUNTIME_ROOT" "$PREPARE_AGENT_ROOT" -type d ! -perm 0700 -print -quit) || return $?
|
|
759
|
+
test -z "$PREPARE_WRONG_DIR_MODE" || return $?
|
|
760
|
+
PREPARE_WRONG_FILE_MODE=$(find "$PREPARE_RUNTIME_ROOT" "$PREPARE_AGENT_ROOT" -type f ! -perm 0600 -print -quit) || return $?
|
|
761
|
+
test -z "$PREPARE_WRONG_FILE_MODE" || return $?
|
|
762
|
+
}
|
|
763
|
+
validate_sanctuary_legacy_staging() {
|
|
764
|
+
EXPECTED_LEGACY_CONTAINER_ID=${1-}
|
|
765
|
+
EXPECTED_LEGACY_IMAGE_ID=${2-}
|
|
766
|
+
LEGACY_CONTAINER_NAMES=$(docker container ls -a --format '{{.Names}}') || return $?
|
|
767
|
+
LEGACY_NAME_COUNTS=$(printf '%s\n' "$LEGACY_CONTAINER_NAMES" | awk '
|
|
768
|
+
/butler/ { butlers++ }
|
|
769
|
+
$0 == "ouro-butler-staging" { staging++ }
|
|
770
|
+
END { printf "%d %d", butlers + 0, staging + 0 }
|
|
771
|
+
') || return $?
|
|
772
|
+
test "$LEGACY_NAME_COUNTS" = "1 1" || return $?
|
|
773
|
+
assert_only_running_butler ouro-butler-staging || return $?
|
|
774
|
+
LEGACY_STAGING_RUNNING=$(docker inspect --format '{{.State.Running}}' ouro-butler-staging) || return $?
|
|
775
|
+
test "$LEGACY_STAGING_RUNNING" = true || return $?
|
|
776
|
+
LEGACY_STAGING_CONTAINER_ID=$(docker inspect --format '{{.Id}}' ouro-butler-staging) || return $?
|
|
777
|
+
case "$LEGACY_STAGING_CONTAINER_ID" in *[!0-9a-f]*|'') return 1 ;; esac
|
|
778
|
+
test "${#LEGACY_STAGING_CONTAINER_ID}" -eq 64 || return 1
|
|
779
|
+
LEGACY_STAGING_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-staging) || return $?
|
|
780
|
+
validate_exact_image_id "$LEGACY_STAGING_IMAGE_ID" || return $?
|
|
781
|
+
if test -n "$EXPECTED_LEGACY_CONTAINER_ID"; then
|
|
782
|
+
test "$LEGACY_STAGING_CONTAINER_ID" = "$EXPECTED_LEGACY_CONTAINER_ID" || return 1
|
|
783
|
+
test "$LEGACY_STAGING_IMAGE_ID" = "$EXPECTED_LEGACY_IMAGE_ID" || return 1
|
|
784
|
+
fi
|
|
785
|
+
}
|
|
786
|
+
prepare_sanctuary_legacy_adoption() {
|
|
787
|
+
IMAGE_ID=$1
|
|
788
|
+
validate_exact_image_id "$IMAGE_ID" || return $?
|
|
789
|
+
validate_sanctuary_legacy_staging || return $?
|
|
790
|
+
PREPARED_LEGACY_CONTAINER_ID=$LEGACY_STAGING_CONTAINER_ID
|
|
791
|
+
PREPARED_LEGACY_IMAGE_ID=$LEGACY_STAGING_IMAGE_ID
|
|
792
|
+
prepare_canonical_sanctuary_roots "$IMAGE_ID" || return $?
|
|
793
|
+
bootstrap_sanctuary_vault "$IMAGE_ID" \
|
|
794
|
+
/mnt/user/appdata/ouro-butler/runtime/container-credentials.json \
|
|
795
|
+
sanctuary-unraid sanctuary || return $?
|
|
796
|
+
provision_sanctuary_sab_credential "$IMAGE_ID" || return $?
|
|
797
|
+
verify_sanctuary_sab_readiness "$IMAGE_ID" || return $?
|
|
798
|
+
validate_sanctuary_legacy_staging "$PREPARED_LEGACY_CONTAINER_ID" "$PREPARED_LEGACY_IMAGE_ID" || return $?
|
|
799
|
+
LEGACY_STAGING_CONTAINER_ID=$PREPARED_LEGACY_CONTAINER_ID
|
|
800
|
+
LEGACY_STAGING_IMAGE_ID=$PREPARED_LEGACY_IMAGE_ID
|
|
801
|
+
}
|
|
802
|
+
capture_sanctuary_legacy_evidence() {
|
|
803
|
+
CAPTURED_LEGACY_CONTAINER_ID=$1
|
|
804
|
+
CAPTURED_LEGACY_IMAGE_ID=$2
|
|
805
|
+
LEGACY_EVIDENCE_ROOT=$3
|
|
806
|
+
case "$CAPTURED_LEGACY_CONTAINER_ID" in *[!0-9a-f]*|'') return 1 ;; esac
|
|
807
|
+
test "${#CAPTURED_LEGACY_CONTAINER_ID}" -eq 64 || return 1
|
|
808
|
+
validate_exact_image_id "$CAPTURED_LEGACY_IMAGE_ID" || return $?
|
|
809
|
+
case "$LEGACY_EVIDENCE_ROOT" in /*) ;; *) return 1 ;; esac
|
|
810
|
+
if test -e "$LEGACY_EVIDENCE_ROOT" || test -L "$LEGACY_EVIDENCE_ROOT"; then
|
|
811
|
+
/usr/local/bin/node -e '
|
|
812
|
+
const fs = require("node:fs");
|
|
813
|
+
const root = fs.lstatSync(process.argv[1]);
|
|
814
|
+
if (!root.isDirectory() || root.isSymbolicLink() || (root.mode & 0o777) !== 0o700) process.exit(1);
|
|
815
|
+
if (root.uid !== process.geteuid() || root.gid !== process.getegid()) process.exit(1);
|
|
816
|
+
' "$LEGACY_EVIDENCE_ROOT" || return $?
|
|
817
|
+
else
|
|
818
|
+
install -d -m 0700 -o 0 -g 0 "$LEGACY_EVIDENCE_ROOT" || return $?
|
|
819
|
+
fi
|
|
820
|
+
LEGACY_EVIDENCE_DIR="$LEGACY_EVIDENCE_ROOT/${CAPTURED_LEGACY_IMAGE_ID#sha256:}"
|
|
821
|
+
if test -e "$LEGACY_EVIDENCE_DIR" || test -L "$LEGACY_EVIDENCE_DIR"; then
|
|
822
|
+
test -d "$LEGACY_EVIDENCE_DIR" && test ! -L "$LEGACY_EVIDENCE_DIR" || return 1
|
|
823
|
+
else
|
|
824
|
+
mkdir -m 0700 "$LEGACY_EVIDENCE_DIR" || return $?
|
|
825
|
+
fi
|
|
826
|
+
/usr/local/bin/node -e '
|
|
827
|
+
const fs = require("node:fs");
|
|
828
|
+
const [rootPath, directoryPath] = process.argv.slice(1);
|
|
829
|
+
const root = fs.lstatSync(rootPath);
|
|
830
|
+
const directory = fs.lstatSync(directoryPath);
|
|
831
|
+
const privateDirectory = value => value.isDirectory() && !value.isSymbolicLink() && (value.mode & 0o777) === 0o700;
|
|
832
|
+
if (!privateDirectory(root) || !privateDirectory(directory)) process.exit(1);
|
|
833
|
+
if (root.uid !== process.geteuid() || root.gid !== process.getegid()) process.exit(1);
|
|
834
|
+
if (root.uid !== directory.uid || root.gid !== directory.gid) process.exit(1);
|
|
835
|
+
const allowed = new Set(["container.json", "image.json"]);
|
|
836
|
+
if (fs.readdirSync(directoryPath).some(name => !allowed.has(name))) process.exit(1);
|
|
837
|
+
' "$LEGACY_EVIDENCE_ROOT" "$LEGACY_EVIDENCE_DIR" || return $?
|
|
838
|
+
validate_legacy_evidence_entry() {
|
|
839
|
+
VALIDATED_EVIDENCE_KIND=$1
|
|
840
|
+
VALIDATED_EVIDENCE_PATH=$2
|
|
841
|
+
/usr/local/bin/node -e '
|
|
842
|
+
const fs = require("node:fs");
|
|
843
|
+
const [kind, filePath, rootPath, containerId, imageId] = process.argv.slice(1);
|
|
844
|
+
const root = fs.lstatSync(rootPath);
|
|
845
|
+
const entry = fs.lstatSync(filePath);
|
|
846
|
+
if (!entry.isFile() || entry.isSymbolicLink() || entry.uid !== root.uid || entry.gid !== root.gid) process.exit(1);
|
|
847
|
+
const mode = entry.mode & 0o777;
|
|
848
|
+
if (mode !== 0o600 && mode !== 0o644) process.exit(1);
|
|
849
|
+
const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
850
|
+
if (!Array.isArray(value) || value.length !== 1 || !value[0] || typeof value[0] !== "object") process.exit(1);
|
|
851
|
+
if (kind === "container" && (value[0].Id !== containerId || value[0].Image !== imageId)) process.exit(1);
|
|
852
|
+
if (kind === "image" && value[0].Id !== imageId) process.exit(1);
|
|
853
|
+
' "$VALIDATED_EVIDENCE_KIND" "$VALIDATED_EVIDENCE_PATH" "$LEGACY_EVIDENCE_ROOT" "$CAPTURED_LEGACY_CONTAINER_ID" "$CAPTURED_LEGACY_IMAGE_ID"
|
|
854
|
+
}
|
|
855
|
+
capture_missing_legacy_evidence_entry() {
|
|
856
|
+
MISSING_EVIDENCE_KIND=$1
|
|
857
|
+
MISSING_EVIDENCE_PATH=$2
|
|
858
|
+
LEGACY_EVIDENCE_TMP=$(mktemp "$LEGACY_EVIDENCE_ROOT/.capture.XXXXXXXX") || return $?
|
|
859
|
+
if test "$MISSING_EVIDENCE_KIND" = container; then
|
|
860
|
+
docker container inspect "$CAPTURED_LEGACY_CONTAINER_ID" >"$LEGACY_EVIDENCE_TMP" || {
|
|
861
|
+
CAPTURE_STATUS=$?
|
|
862
|
+
rm -f "$LEGACY_EVIDENCE_TMP"
|
|
863
|
+
return "$CAPTURE_STATUS"
|
|
864
|
+
}
|
|
865
|
+
else
|
|
866
|
+
docker image inspect "$CAPTURED_LEGACY_IMAGE_ID" >"$LEGACY_EVIDENCE_TMP" || {
|
|
867
|
+
CAPTURE_STATUS=$?
|
|
868
|
+
rm -f "$LEGACY_EVIDENCE_TMP"
|
|
869
|
+
return "$CAPTURE_STATUS"
|
|
870
|
+
}
|
|
871
|
+
fi
|
|
872
|
+
chmod 0600 "$LEGACY_EVIDENCE_TMP" || {
|
|
873
|
+
CAPTURE_STATUS=$?
|
|
874
|
+
rm -f "$LEGACY_EVIDENCE_TMP"
|
|
875
|
+
return "$CAPTURE_STATUS"
|
|
876
|
+
}
|
|
877
|
+
validate_legacy_evidence_entry "$MISSING_EVIDENCE_KIND" "$LEGACY_EVIDENCE_TMP" || {
|
|
878
|
+
CAPTURE_STATUS=$?
|
|
879
|
+
rm -f "$LEGACY_EVIDENCE_TMP"
|
|
880
|
+
return "$CAPTURE_STATUS"
|
|
881
|
+
}
|
|
882
|
+
sync -f "$LEGACY_EVIDENCE_TMP" || {
|
|
883
|
+
CAPTURE_STATUS=$?
|
|
884
|
+
rm -f "$LEGACY_EVIDENCE_TMP"
|
|
885
|
+
return "$CAPTURE_STATUS"
|
|
886
|
+
}
|
|
887
|
+
if ln "$LEGACY_EVIDENCE_TMP" "$MISSING_EVIDENCE_PATH"; then
|
|
888
|
+
rm -f "$LEGACY_EVIDENCE_TMP" || return $?
|
|
889
|
+
else
|
|
890
|
+
rm -f "$LEGACY_EVIDENCE_TMP" || return $?
|
|
891
|
+
validate_legacy_evidence_entry "$MISSING_EVIDENCE_KIND" "$MISSING_EVIDENCE_PATH" || return $?
|
|
892
|
+
fi
|
|
893
|
+
}
|
|
894
|
+
for LEGACY_EVIDENCE_KIND in container image; do
|
|
895
|
+
LEGACY_EVIDENCE_PATH="$LEGACY_EVIDENCE_DIR/$LEGACY_EVIDENCE_KIND.json"
|
|
896
|
+
if test -e "$LEGACY_EVIDENCE_PATH" || test -L "$LEGACY_EVIDENCE_PATH"; then
|
|
897
|
+
validate_legacy_evidence_entry "$LEGACY_EVIDENCE_KIND" "$LEGACY_EVIDENCE_PATH" || return $?
|
|
898
|
+
else
|
|
899
|
+
capture_missing_legacy_evidence_entry "$LEGACY_EVIDENCE_KIND" "$LEGACY_EVIDENCE_PATH" || return $?
|
|
900
|
+
fi
|
|
901
|
+
chmod 0600 "$LEGACY_EVIDENCE_PATH" || return $?
|
|
902
|
+
validate_legacy_evidence_entry "$LEGACY_EVIDENCE_KIND" "$LEGACY_EVIDENCE_PATH" || return $?
|
|
903
|
+
done
|
|
904
|
+
sync -f "$LEGACY_EVIDENCE_DIR" || return $?
|
|
905
|
+
}
|
|
906
|
+
install_from_legacy_staging() {
|
|
907
|
+
prepare_sanctuary_legacy_adoption "$IMAGE_ID" || return $?
|
|
908
|
+
ADOPTION_PREPARED_CONTAINER_ID=$LEGACY_STAGING_CONTAINER_ID
|
|
909
|
+
ADOPTION_PREPARED_IMAGE_ID=$LEGACY_STAGING_IMAGE_ID
|
|
910
|
+
verify_sanctuary_provider_readiness "$IMAGE_ID" || return $?
|
|
911
|
+
ADOPTION_CONTAINER_NAMES=$(docker container ls -a --format '{{.Names}}') || return $?
|
|
912
|
+
ADOPTION_NAME_COUNTS=$(printf '%s\n' "$ADOPTION_CONTAINER_NAMES" | awk '
|
|
913
|
+
/butler/ { butlers++ }
|
|
914
|
+
$0 == "ouro-butler-staging" { staging++ }
|
|
915
|
+
END { printf "%d %d", butlers + 0, staging + 0 }
|
|
916
|
+
') || return $?
|
|
917
|
+
test "$ADOPTION_NAME_COUNTS" = "1 1" || return $?
|
|
918
|
+
assert_only_running_butler ouro-butler-staging || return $?
|
|
919
|
+
test "$(docker inspect --format '{{.State.Running}}' ouro-butler-staging)" = true || return $?
|
|
920
|
+
test "$(docker inspect --format '{{.Id}}' ouro-butler-staging)" = "$ADOPTION_PREPARED_CONTAINER_ID" || return $?
|
|
921
|
+
test "$(docker inspect --format '{{.Image}}' ouro-butler-staging)" = "$ADOPTION_PREPARED_IMAGE_ID" || return $?
|
|
922
|
+
LEGACY_STAGING_CONTAINER_ID=$ADOPTION_PREPARED_CONTAINER_ID
|
|
923
|
+
LEGACY_STAGING_IMAGE_ID=$ADOPTION_PREPARED_IMAGE_ID
|
|
924
|
+
LEGACY_EVIDENCE_ROOT=/mnt/user/appdata/ouro-butler/legacy-evidence
|
|
925
|
+
capture_sanctuary_legacy_evidence "$LEGACY_STAGING_CONTAINER_ID" "$LEGACY_STAGING_IMAGE_ID" "$LEGACY_EVIDENCE_ROOT" || return $?
|
|
926
|
+
validate_sanctuary_legacy_staging "$ADOPTION_PREPARED_CONTAINER_ID" "$ADOPTION_PREPARED_IMAGE_ID" || return $?
|
|
927
|
+
LEGACY_STAGING_CONTAINER_ID=$ADOPTION_PREPARED_CONTAINER_ID
|
|
928
|
+
LEGACY_STAGING_IMAGE_ID=$ADOPTION_PREPARED_IMAGE_ID
|
|
929
|
+
disable_butler_autostart || return $?
|
|
930
|
+
if docker stop "$LEGACY_STAGING_CONTAINER_ID" \
|
|
931
|
+
&& CURRENT_LEGACY_STAGING_IMAGE_ID=$(docker inspect --format '{{.Image}}' "$LEGACY_STAGING_CONTAINER_ID") \
|
|
932
|
+
&& test "$CURRENT_LEGACY_STAGING_IMAGE_ID" = "$LEGACY_STAGING_IMAGE_ID" \
|
|
933
|
+
&& test "$(docker inspect --format '{{.State.Running}}' "$LEGACY_STAGING_CONTAINER_ID")" = false \
|
|
934
|
+
&& docker rename "$LEGACY_STAGING_CONTAINER_ID" ouro-butler-legacy-evidence \
|
|
935
|
+
&& CURRENT_LEGACY_EVIDENCE_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-legacy-evidence) \
|
|
936
|
+
&& test "$CURRENT_LEGACY_EVIDENCE_IMAGE_ID" = "$LEGACY_STAGING_IMAGE_ID" \
|
|
937
|
+
&& test "$(docker inspect --format '{{.State.Running}}' ouro-butler-legacy-evidence)" = false \
|
|
938
|
+
&& assert_only_running_butler - \
|
|
939
|
+
&& docker create --name ouro-butler-staging --network host --restart unless-stopped --user 10001:10001 \
|
|
940
|
+
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli,dst=/home/ouro/.ouro-cli" \
|
|
941
|
+
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
942
|
+
--mount "type=bind,src=/boot/config/custom/ouro-events/spool,dst=/run/ouro-events,readonly" \
|
|
943
|
+
"$IMAGE_ID" \
|
|
944
|
+
&& audit_effective ouro-butler-staging "$IMAGE_ID" "$IMAGE_ID" \
|
|
945
|
+
&& docker start ouro-butler-staging \
|
|
946
|
+
&& assert_only_running_butler ouro-butler-staging \
|
|
947
|
+
&& wait_butler_ready ouro-butler-staging \
|
|
948
|
+
&& docker stop ouro-butler-staging \
|
|
949
|
+
&& test "$(docker inspect --format '{{.Image}}' ouro-butler-staging)" = "$IMAGE_ID" \
|
|
950
|
+
&& docker rm ouro-butler-staging \
|
|
951
|
+
&& assert_only_running_butler - \
|
|
952
|
+
&& docker create --name ouro-butler --network host --restart unless-stopped --user 10001:10001 \
|
|
953
|
+
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli,dst=/home/ouro/.ouro-cli" \
|
|
954
|
+
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
955
|
+
--mount "type=bind,src=/boot/config/custom/ouro-events/spool,dst=/run/ouro-events,readonly" \
|
|
956
|
+
"$IMAGE_ID" \
|
|
957
|
+
&& audit_effective ouro-butler "$IMAGE_ID" "$IMAGE_ID" \
|
|
958
|
+
&& docker start ouro-butler \
|
|
959
|
+
&& assert_only_running_butler ouro-butler \
|
|
960
|
+
&& wait_butler_ready ouro-butler \
|
|
961
|
+
&& enable_butler_autostart; then
|
|
962
|
+
return 0
|
|
963
|
+
else
|
|
964
|
+
ADOPTION_STATUS=$?
|
|
965
|
+
fi
|
|
966
|
+
ADOPTION_RECOVERY_NAMES=$(docker container ls -a --format '{{.Names}}') || return $?
|
|
967
|
+
case "
|
|
968
|
+
$ADOPTION_RECOVERY_NAMES
|
|
969
|
+
" in
|
|
970
|
+
*"
|
|
971
|
+
ouro-butler
|
|
972
|
+
"*)
|
|
973
|
+
ADOPTION_PARTIAL_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler) || return $?
|
|
974
|
+
test "$ADOPTION_PARTIAL_IMAGE_ID" = "$IMAGE_ID" || return $?
|
|
975
|
+
docker stop ouro-butler >/dev/null 2>&1 || true
|
|
976
|
+
docker rm --force ouro-butler || return $?
|
|
977
|
+
;;
|
|
978
|
+
esac
|
|
979
|
+
ADOPTION_RECOVERY_NAMES=$(docker container ls -a --format '{{.Names}}') || return $?
|
|
980
|
+
case "
|
|
981
|
+
$ADOPTION_RECOVERY_NAMES
|
|
982
|
+
" in
|
|
983
|
+
*"
|
|
984
|
+
ouro-butler-staging
|
|
985
|
+
"*)
|
|
986
|
+
CURRENT_LEGACY_STAGING_CONTAINER_ID=$(docker inspect --format '{{.Id}}' ouro-butler-staging) || return $?
|
|
987
|
+
case "$CURRENT_LEGACY_STAGING_CONTAINER_ID" in *[!0-9a-f]*|'') return 1 ;; esac
|
|
988
|
+
test "${#CURRENT_LEGACY_STAGING_CONTAINER_ID}" -eq 64 || return 1
|
|
989
|
+
CURRENT_LEGACY_STAGING_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-staging) || return $?
|
|
990
|
+
case "$CURRENT_LEGACY_STAGING_IMAGE_ID" in
|
|
991
|
+
"$IMAGE_ID")
|
|
992
|
+
docker stop ouro-butler-staging >/dev/null 2>&1 || true
|
|
993
|
+
docker rm --force ouro-butler-staging || return $?
|
|
994
|
+
;;
|
|
995
|
+
"$LEGACY_STAGING_IMAGE_ID")
|
|
996
|
+
test "$CURRENT_LEGACY_STAGING_CONTAINER_ID" = "$LEGACY_STAGING_CONTAINER_ID" || return 1
|
|
997
|
+
docker stop "$LEGACY_STAGING_CONTAINER_ID" >/dev/null 2>&1 || return $?
|
|
998
|
+
test "$(docker inspect --format '{{.State.Running}}' "$LEGACY_STAGING_CONTAINER_ID")" = false || return $?
|
|
999
|
+
docker rename "$LEGACY_STAGING_CONTAINER_ID" ouro-butler-legacy-evidence || return $?
|
|
1000
|
+
;;
|
|
1001
|
+
*) return 1 ;;
|
|
1002
|
+
esac
|
|
1003
|
+
;;
|
|
1004
|
+
esac
|
|
1005
|
+
CURRENT_LEGACY_EVIDENCE_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-legacy-evidence) || return $?
|
|
1006
|
+
test "$CURRENT_LEGACY_EVIDENCE_IMAGE_ID" = "$LEGACY_STAGING_IMAGE_ID" || return $?
|
|
1007
|
+
test "$(docker inspect --format '{{.State.Running}}' ouro-butler-legacy-evidence)" = false || return $?
|
|
1008
|
+
return "$ADOPTION_STATUS"
|
|
1009
|
+
}
|
|
1010
|
+
validate_sanctuary_legacy_import_marker() {
|
|
1011
|
+
IMPORT_MARKER=$1
|
|
1012
|
+
IMPORT_SOURCE=$2
|
|
1013
|
+
test -f "$IMPORT_MARKER" && test ! -L "$IMPORT_MARKER" || return 1
|
|
1014
|
+
test "$(stat -c '%u:%g %a' "$IMPORT_MARKER")" = "10001:10001 600" || return 1
|
|
1015
|
+
/usr/local/bin/node -e '
|
|
1016
|
+
const crypto = require("node:crypto");
|
|
1017
|
+
const fs = require("node:fs");
|
|
1018
|
+
const marker = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
|
1019
|
+
const sourceDigest = `sha256:${crypto.createHash("sha256").update(fs.readFileSync(process.argv[2])).digest("hex")}`;
|
|
1020
|
+
if (JSON.stringify(Object.keys(marker).sort()) !== JSON.stringify(["importedAt", "machineId", "schemaVersion", "sourceDigest"])) process.exit(1);
|
|
1021
|
+
if (marker.schemaVersion !== 1 || marker.machineId !== "sanctuary" || marker.sourceDigest !== sourceDigest) process.exit(1);
|
|
1022
|
+
if (!Number.isFinite(Date.parse(marker.importedAt))) process.exit(1);
|
|
1023
|
+
' "$IMPORT_MARKER" "$IMPORT_SOURCE"
|
|
1024
|
+
}
|
|
1025
|
+
record_sanctuary_legacy_import_marker() {
|
|
1026
|
+
IMPORT_MARKER=$1
|
|
1027
|
+
IMPORT_SOURCE=$2
|
|
1028
|
+
test ! -e "$IMPORT_MARKER" || return 1
|
|
1029
|
+
IMPORT_MARKER_TMP=$(mktemp "${IMPORT_MARKER}.tmp.XXXXXX") || return $?
|
|
1030
|
+
trap 'rm -f -- "$IMPORT_MARKER_TMP"' EXIT || return $?
|
|
1031
|
+
/usr/local/bin/node -e '
|
|
1032
|
+
const crypto = require("node:crypto");
|
|
1033
|
+
const fs = require("node:fs");
|
|
1034
|
+
const sourceDigest = `sha256:${crypto.createHash("sha256").update(fs.readFileSync(process.argv[2])).digest("hex")}`;
|
|
1035
|
+
const marker = { schemaVersion: 1, machineId: "sanctuary", sourceDigest, importedAt: new Date().toISOString() };
|
|
1036
|
+
fs.writeFileSync(process.argv[1], `${JSON.stringify(marker)}\n`, { mode: 0o600, flag: "w" });
|
|
1037
|
+
' "$IMPORT_MARKER_TMP" "$IMPORT_SOURCE" || return $?
|
|
1038
|
+
chown 10001:10001 "$IMPORT_MARKER_TMP" || return $?
|
|
1039
|
+
chmod 0600 "$IMPORT_MARKER_TMP" || return $?
|
|
1040
|
+
mv -f -- "$IMPORT_MARKER_TMP" "$IMPORT_MARKER" || return $?
|
|
1041
|
+
sync -f "$IMPORT_MARKER" || return $?
|
|
1042
|
+
sync -f "$(dirname "$IMPORT_MARKER")" || return $?
|
|
1043
|
+
trap - EXIT || return $?
|
|
1044
|
+
}
|
|
1045
|
+
bootstrap_sanctuary_vault() {
|
|
1046
|
+
BOOTSTRAP_IMAGE_ID=$1
|
|
1047
|
+
BOOTSTRAP_LEGACY_CREDENTIAL_SOURCE=${2-}
|
|
1048
|
+
BOOTSTRAP_SOURCE_MACHINE_ID=${3-}
|
|
1049
|
+
BOOTSTRAP_TARGET_MACHINE_ID=${4-}
|
|
1050
|
+
validate_exact_image_id "$BOOTSTRAP_IMAGE_ID" || return $?
|
|
1051
|
+
case "$BOOTSTRAP_LEGACY_CREDENTIAL_SOURCE" in
|
|
1052
|
+
"")
|
|
1053
|
+
test -z "$BOOTSTRAP_SOURCE_MACHINE_ID" || return 1
|
|
1054
|
+
test -z "$BOOTSTRAP_TARGET_MACHINE_ID" || return 1
|
|
1055
|
+
;;
|
|
1056
|
+
/mnt/user/appdata/ouro-butler/runtime/container-credentials.json)
|
|
1057
|
+
test "$BOOTSTRAP_SOURCE_MACHINE_ID" = sanctuary-unraid || return 1
|
|
1058
|
+
test "$BOOTSTRAP_TARGET_MACHINE_ID" = sanctuary || return 1
|
|
1059
|
+
;;
|
|
1060
|
+
*) return 1 ;;
|
|
1061
|
+
esac
|
|
1062
|
+
BOOTSTRAP_RUNTIME_ROOT=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli
|
|
1063
|
+
BOOTSTRAP_AGENT_ROOT=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro
|
|
1064
|
+
for BOOTSTRAP_CONTAINER in ouro-butler-vault-status ouro-butler-vault-bootstrap ouro-butler-credential-bootstrap ouro-butler-provider-readiness; do
|
|
1065
|
+
! docker container inspect "$BOOTSTRAP_CONTAINER" >/dev/null 2>&1 || return 1
|
|
1066
|
+
done
|
|
1067
|
+
VAULT_STATUS=$(docker run --rm --pull=never --network host --name ouro-butler-vault-status --user 10001:10001 \
|
|
1068
|
+
--mount "type=bind,src=$BOOTSTRAP_RUNTIME_ROOT,dst=/home/ouro/.ouro-cli" \
|
|
1069
|
+
--mount "type=bind,src=$BOOTSTRAP_AGENT_ROOT,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
1070
|
+
--entrypoint node "$BOOTSTRAP_IMAGE_ID" /opt/ouro/dist/heart/daemon/ouro-entry.js \
|
|
1071
|
+
vault status --agent sanctuary --store plaintext-file) || return $?
|
|
1072
|
+
! docker container inspect ouro-butler-vault-status >/dev/null 2>&1 || return 1
|
|
1073
|
+
case "
|
|
1074
|
+
$VAULT_STATUS
|
|
1075
|
+
" in
|
|
1076
|
+
*"
|
|
1077
|
+
vault locator: not configured in agent.json
|
|
1078
|
+
"*) VAULT_ACTION=create ;;
|
|
1079
|
+
*"
|
|
1080
|
+
vault locator: agent.json
|
|
1081
|
+
"*)
|
|
1082
|
+
case "
|
|
1083
|
+
$VAULT_STATUS
|
|
1084
|
+
" in
|
|
1085
|
+
*"
|
|
1086
|
+
local unlock: available
|
|
1087
|
+
"*) VAULT_ACTION=none ;;
|
|
1088
|
+
*"
|
|
1089
|
+
local unlock: missing
|
|
1090
|
+
"*) VAULT_ACTION=unlock ;;
|
|
1091
|
+
*) return 1 ;;
|
|
1092
|
+
esac
|
|
1093
|
+
;;
|
|
1094
|
+
*) return 1 ;;
|
|
1095
|
+
esac
|
|
1096
|
+
if test "$VAULT_ACTION" != none; then
|
|
1097
|
+
docker run --rm -it --pull=never --network host --name ouro-butler-vault-bootstrap --user 10001:10001 \
|
|
1098
|
+
--mount "type=bind,src=$BOOTSTRAP_RUNTIME_ROOT,dst=/home/ouro/.ouro-cli" \
|
|
1099
|
+
--mount "type=bind,src=$BOOTSTRAP_AGENT_ROOT,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
1100
|
+
--entrypoint node "$BOOTSTRAP_IMAGE_ID" /opt/ouro/dist/heart/daemon/ouro-entry.js \
|
|
1101
|
+
vault "$VAULT_ACTION" --agent sanctuary --store plaintext-file || return $?
|
|
1102
|
+
! docker container inspect ouro-butler-vault-bootstrap >/dev/null 2>&1 || return 1
|
|
1103
|
+
fi
|
|
1104
|
+
VERIFIED_VAULT_STATUS=$(docker run --rm --pull=never --network host --name ouro-butler-vault-status --user 10001:10001 \
|
|
1105
|
+
--mount "type=bind,src=$BOOTSTRAP_RUNTIME_ROOT,dst=/home/ouro/.ouro-cli" \
|
|
1106
|
+
--mount "type=bind,src=$BOOTSTRAP_AGENT_ROOT,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
1107
|
+
--entrypoint node "$BOOTSTRAP_IMAGE_ID" /opt/ouro/dist/heart/daemon/ouro-entry.js \
|
|
1108
|
+
vault status --agent sanctuary --store plaintext-file) || return $?
|
|
1109
|
+
! docker container inspect ouro-butler-vault-status >/dev/null 2>&1 || return 1
|
|
1110
|
+
case "
|
|
1111
|
+
$VERIFIED_VAULT_STATUS
|
|
1112
|
+
" in
|
|
1113
|
+
*"
|
|
1114
|
+
vault locator: agent.json
|
|
1115
|
+
"*) ;;
|
|
1116
|
+
*) return 1 ;;
|
|
1117
|
+
esac
|
|
1118
|
+
case "
|
|
1119
|
+
$VERIFIED_VAULT_STATUS
|
|
1120
|
+
" in
|
|
1121
|
+
*"
|
|
1122
|
+
local unlock: available
|
|
1123
|
+
"*) ;;
|
|
1124
|
+
*) return 1 ;;
|
|
1125
|
+
esac
|
|
1126
|
+
if test -n "$BOOTSTRAP_LEGACY_CREDENTIAL_SOURCE"; then
|
|
1127
|
+
BOOTSTRAP_CREDENTIAL_SOURCE="$BOOTSTRAP_RUNTIME_ROOT/container-credentials.json"
|
|
1128
|
+
BOOTSTRAP_CREDENTIAL_CLAIM="$BOOTSTRAP_CREDENTIAL_SOURCE.consuming"
|
|
1129
|
+
test -f "$BOOTSTRAP_LEGACY_CREDENTIAL_SOURCE" || return $?
|
|
1130
|
+
test ! -L "$BOOTSTRAP_LEGACY_CREDENTIAL_SOURCE" || return $?
|
|
1131
|
+
BOOTSTRAP_LEGACY_METADATA=$(stat -c '%u:%g %a' "$BOOTSTRAP_LEGACY_CREDENTIAL_SOURCE") || return $?
|
|
1132
|
+
test "$BOOTSTRAP_LEGACY_METADATA" = "10001:10001 600" || return $?
|
|
1133
|
+
BOOTSTRAP_IMPORT_MARKER="$BOOTSTRAP_RUNTIME_ROOT/legacy-credentials-imported.json"
|
|
1134
|
+
if test -e "$BOOTSTRAP_IMPORT_MARKER"; then
|
|
1135
|
+
validate_sanctuary_legacy_import_marker "$BOOTSTRAP_IMPORT_MARKER" "$BOOTSTRAP_LEGACY_CREDENTIAL_SOURCE" || return $?
|
|
1136
|
+
test ! -e "$BOOTSTRAP_CREDENTIAL_SOURCE" || return 1
|
|
1137
|
+
test ! -e "$BOOTSTRAP_CREDENTIAL_CLAIM" || return 1
|
|
1138
|
+
else
|
|
1139
|
+
if test -e "$BOOTSTRAP_CREDENTIAL_SOURCE"; then
|
|
1140
|
+
test -f "$BOOTSTRAP_CREDENTIAL_SOURCE" || return $?
|
|
1141
|
+
test ! -L "$BOOTSTRAP_CREDENTIAL_SOURCE" || return $?
|
|
1142
|
+
test "$(stat -c '%u:%g %a' "$BOOTSTRAP_CREDENTIAL_SOURCE")" = "10001:10001 600" || return $?
|
|
1143
|
+
cmp -s "$BOOTSTRAP_LEGACY_CREDENTIAL_SOURCE" "$BOOTSTRAP_CREDENTIAL_SOURCE" || return $?
|
|
1144
|
+
elif test -e "$BOOTSTRAP_CREDENTIAL_CLAIM"; then
|
|
1145
|
+
test -f "$BOOTSTRAP_CREDENTIAL_CLAIM" || return $?
|
|
1146
|
+
test ! -L "$BOOTSTRAP_CREDENTIAL_CLAIM" || return $?
|
|
1147
|
+
test "$(stat -c '%u:%g %a' "$BOOTSTRAP_CREDENTIAL_CLAIM")" = "10001:10001 600" || return $?
|
|
1148
|
+
cmp -s "$BOOTSTRAP_LEGACY_CREDENTIAL_SOURCE" "$BOOTSTRAP_CREDENTIAL_CLAIM" || return $?
|
|
1149
|
+
else
|
|
1150
|
+
BOOTSTRAP_CREDENTIAL_TMP=$(mktemp "$BOOTSTRAP_RUNTIME_ROOT/container-credentials.json.tmp.XXXXXX") || return $?
|
|
1151
|
+
trap 'rm -f -- "$BOOTSTRAP_CREDENTIAL_TMP"' EXIT || return $?
|
|
1152
|
+
install -m 0600 -o 10001 -g 10001 "$BOOTSTRAP_LEGACY_CREDENTIAL_SOURCE" "$BOOTSTRAP_CREDENTIAL_TMP" || return $?
|
|
1153
|
+
cmp -s "$BOOTSTRAP_LEGACY_CREDENTIAL_SOURCE" "$BOOTSTRAP_CREDENTIAL_TMP" || return $?
|
|
1154
|
+
sync -f "$BOOTSTRAP_CREDENTIAL_TMP" || return $?
|
|
1155
|
+
mv -f -- "$BOOTSTRAP_CREDENTIAL_TMP" "$BOOTSTRAP_CREDENTIAL_SOURCE" || return $?
|
|
1156
|
+
sync -f "$BOOTSTRAP_RUNTIME_ROOT" || return $?
|
|
1157
|
+
trap - EXIT || return $?
|
|
1158
|
+
fi
|
|
1159
|
+
docker run --rm --pull=never --network host --name ouro-butler-credential-bootstrap --user 10001:10001 \
|
|
1160
|
+
--mount "type=bind,src=$BOOTSTRAP_RUNTIME_ROOT,dst=/home/ouro/.ouro-cli" \
|
|
1161
|
+
--mount "type=bind,src=$BOOTSTRAP_AGENT_ROOT,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
1162
|
+
--entrypoint node "$BOOTSTRAP_IMAGE_ID" -e '
|
|
1163
|
+
const { loadContainerCredentialBootstrap } = require("/opt/ouro/dist/heart/daemon/container-credential-bootstrap.js")
|
|
1164
|
+
loadContainerCredentialBootstrap(["sanctuary"], {
|
|
1165
|
+
machineIdMigration: {
|
|
1166
|
+
sourceMachineId: process.argv[1],
|
|
1167
|
+
targetMachineId: process.argv[2],
|
|
1168
|
+
},
|
|
1169
|
+
}).then(
|
|
1170
|
+
() => process.exit(0),
|
|
1171
|
+
() => { process.stderr.write("credential bootstrap failed\n"); process.exit(1) },
|
|
1172
|
+
)
|
|
1173
|
+
' "$BOOTSTRAP_SOURCE_MACHINE_ID" "$BOOTSTRAP_TARGET_MACHINE_ID" || return $?
|
|
1174
|
+
# The path-locked Sanctuary migration imports the legacy MiniMax provider
|
|
1175
|
+
# record into the agent vault before strict validation and persistence.
|
|
1176
|
+
# The original legacy envelope remains byte-for-byte unchanged and is the
|
|
1177
|
+
# sole digest authority.
|
|
1178
|
+
! docker container inspect ouro-butler-credential-bootstrap >/dev/null 2>&1 || return 1
|
|
1179
|
+
test ! -e "$BOOTSTRAP_CREDENTIAL_SOURCE" || return $?
|
|
1180
|
+
test ! -e "$BOOTSTRAP_CREDENTIAL_CLAIM" || return $?
|
|
1181
|
+
record_sanctuary_legacy_import_marker "$BOOTSTRAP_IMPORT_MARKER" "$BOOTSTRAP_LEGACY_CREDENTIAL_SOURCE" || return $?
|
|
1182
|
+
validate_sanctuary_legacy_import_marker "$BOOTSTRAP_IMPORT_MARKER" "$BOOTSTRAP_LEGACY_CREDENTIAL_SOURCE" || return $?
|
|
1183
|
+
fi
|
|
1184
|
+
test -f "$BOOTSTRAP_LEGACY_CREDENTIAL_SOURCE" || return $?
|
|
1185
|
+
test ! -L "$BOOTSTRAP_LEGACY_CREDENTIAL_SOURCE" || return $?
|
|
1186
|
+
test "$(stat -c '%u:%g %a' "$BOOTSTRAP_LEGACY_CREDENTIAL_SOURCE")" = "$BOOTSTRAP_LEGACY_METADATA" || return $?
|
|
1187
|
+
fi
|
|
1188
|
+
validate_sanctuary_roots "$BOOTSTRAP_RUNTIME_ROOT" "$BOOTSTRAP_AGENT_ROOT" || return $?
|
|
1189
|
+
}
|
|
1190
|
+
provision_sanctuary_sab_credential() {
|
|
1191
|
+
SAB_BOOTSTRAP_IMAGE_ID=$1
|
|
1192
|
+
validate_exact_image_id "$SAB_BOOTSTRAP_IMAGE_ID" || return $?
|
|
1193
|
+
SAB_BOOTSTRAP_SOURCE=/mnt/user/appdata/sabnzbd/sabnzbd.ini
|
|
1194
|
+
SAB_BOOTSTRAP_RUNTIME_ROOT=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli
|
|
1195
|
+
SAB_BOOTSTRAP_AGENT_ROOT=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro
|
|
1196
|
+
SAB_BOOTSTRAP_ENVELOPE="$SAB_BOOTSTRAP_RUNTIME_ROOT/container-credentials.json"
|
|
1197
|
+
SAB_BOOTSTRAP_CLAIM="$SAB_BOOTSTRAP_ENVELOPE.consuming"
|
|
1198
|
+
test -f "$SAB_BOOTSTRAP_SOURCE" || return $?
|
|
1199
|
+
test ! -L "$SAB_BOOTSTRAP_SOURCE" || return 1
|
|
1200
|
+
test ! -e "$SAB_BOOTSTRAP_ENVELOPE" || return 1
|
|
1201
|
+
test ! -e "$SAB_BOOTSTRAP_CLAIM" || return 1
|
|
1202
|
+
! docker container inspect ouro-butler-sab-credential-bootstrap >/dev/null 2>&1 || return 1
|
|
1203
|
+
SAB_BOOTSTRAP_TMP=$(mktemp "$SAB_BOOTSTRAP_RUNTIME_ROOT/container-credentials.json.tmp.XXXXXX") || return $?
|
|
1204
|
+
trap 'rm -f -- "$SAB_BOOTSTRAP_TMP"' EXIT || return $?
|
|
1205
|
+
/usr/local/bin/node -e '
|
|
1206
|
+
const fs = require("node:fs");
|
|
1207
|
+
const [source, destination] = process.argv.slice(1);
|
|
1208
|
+
const match = fs.readFileSync(source, "utf8").match(/^\s*api_key\s*=\s*(\S+)\s*$/mu);
|
|
1209
|
+
if (!match || !match[1]) process.exit(1);
|
|
1210
|
+
const credential = { type: "ouro.runtimeCredentialBootstrap", agentName: "sanctuary", machineId: "sanctuary", machineRuntimeConfig: { sabnzbdApiKey: match[1] } };
|
|
1211
|
+
const envelope = { schemaVersion: 1, credentials: [credential] };
|
|
1212
|
+
fs.writeFileSync(destination, `${JSON.stringify(envelope)}\n`, { mode: 0o600, flag: "w" });
|
|
1213
|
+
' "$SAB_BOOTSTRAP_SOURCE" "$SAB_BOOTSTRAP_TMP" || return $?
|
|
1214
|
+
chown 10001:10001 "$SAB_BOOTSTRAP_TMP" || return $?
|
|
1215
|
+
chmod 0600 "$SAB_BOOTSTRAP_TMP" || return $?
|
|
1216
|
+
sync -f "$SAB_BOOTSTRAP_TMP" || return $?
|
|
1217
|
+
mv -f -- "$SAB_BOOTSTRAP_TMP" "$SAB_BOOTSTRAP_ENVELOPE" || return $?
|
|
1218
|
+
sync -f "$SAB_BOOTSTRAP_RUNTIME_ROOT" || return $?
|
|
1219
|
+
trap - EXIT || return $?
|
|
1220
|
+
docker run --rm --pull=never --network host --name ouro-butler-sab-credential-bootstrap --user 10001:10001 \
|
|
1221
|
+
--mount "type=bind,src=$SAB_BOOTSTRAP_RUNTIME_ROOT,dst=/home/ouro/.ouro-cli" \
|
|
1222
|
+
--mount "type=bind,src=$SAB_BOOTSTRAP_AGENT_ROOT,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
1223
|
+
--entrypoint node "$SAB_BOOTSTRAP_IMAGE_ID" -e '
|
|
1224
|
+
const { loadContainerCredentialBootstrap } = require("/opt/ouro/dist/heart/daemon/container-credential-bootstrap.js");
|
|
1225
|
+
loadContainerCredentialBootstrap(["sanctuary"]).then(
|
|
1226
|
+
() => process.exit(0),
|
|
1227
|
+
() => { process.stderr.write("SAB credential bootstrap failed\n"); process.exit(1); },
|
|
1228
|
+
);
|
|
1229
|
+
' || return $?
|
|
1230
|
+
! docker container inspect ouro-butler-sab-credential-bootstrap >/dev/null 2>&1 || return 1
|
|
1231
|
+
test ! -e "$SAB_BOOTSTRAP_ENVELOPE" || return 1
|
|
1232
|
+
test ! -e "$SAB_BOOTSTRAP_CLAIM" || return 1
|
|
1233
|
+
}
|
|
1234
|
+
verify_sanctuary_sab_readiness() {
|
|
1235
|
+
SAB_READINESS_IMAGE_ID=$1
|
|
1236
|
+
validate_exact_image_id "$SAB_READINESS_IMAGE_ID" || return $?
|
|
1237
|
+
! docker container inspect ouro-butler-sab-readiness >/dev/null 2>&1 || return 1
|
|
1238
|
+
docker run --rm --pull=never --network host --name ouro-butler-sab-readiness --user 10001:10001 \
|
|
1239
|
+
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli,dst=/home/ouro/.ouro-cli" \
|
|
1240
|
+
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
1241
|
+
--entrypoint node "$SAB_READINESS_IMAGE_ID" -e '
|
|
1242
|
+
(async () => {
|
|
1243
|
+
try {
|
|
1244
|
+
const { refreshMachineRuntimeCredentialConfig } = require("/opt/ouro/dist/heart/runtime-credentials.js");
|
|
1245
|
+
const { createSanctuarySabClient } = require("/opt/ouro/dist/senses/sanctuary-sab.js");
|
|
1246
|
+
const current = await refreshMachineRuntimeCredentialConfig("sanctuary", "sanctuary");
|
|
1247
|
+
if (!current.ok || typeof current.config.sabnzbdApiKey !== "string" || !current.config.sabnzbdApiKey.trim()) throw new Error();
|
|
1248
|
+
const snapshot = await createSanctuarySabClient({ loadApiKey: async () => current.config.sabnzbdApiKey }).readQueue();
|
|
1249
|
+
if (typeof snapshot.paused !== "boolean" || !Number.isSafeInteger(snapshot.queuedJobs) || !snapshot.observedAt) throw new Error();
|
|
1250
|
+
process.stdout.write("Sanctuary SAB queue readiness verified.\n");
|
|
1251
|
+
} catch {
|
|
1252
|
+
process.stderr.write("Sanctuary SAB queue readiness verification failed.\n");
|
|
1253
|
+
process.exitCode = 1;
|
|
1254
|
+
}
|
|
1255
|
+
})();
|
|
1256
|
+
' || return $?
|
|
1257
|
+
! docker container inspect ouro-butler-sab-readiness >/dev/null 2>&1 || return 1
|
|
1258
|
+
}
|
|
1259
|
+
authenticate_sanctuary_provider() {
|
|
1260
|
+
AUTH_IMAGE_ID=$1
|
|
1261
|
+
AUTH_PROVIDER=${2-}
|
|
1262
|
+
validate_exact_image_id "$AUTH_IMAGE_ID" || return $?
|
|
1263
|
+
case "$AUTH_PROVIDER" in
|
|
1264
|
+
openai-compatible) AUTH_CONTAINER=ouro-butler-provider-auth-glm ;;
|
|
1265
|
+
openai-compatible-gemini) AUTH_CONTAINER=ouro-butler-provider-auth-gemini ;;
|
|
1266
|
+
*) return 1 ;;
|
|
1267
|
+
esac
|
|
1268
|
+
AUTH_RUNTIME_ROOT=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli
|
|
1269
|
+
AUTH_AGENT_ROOT=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro
|
|
1270
|
+
! docker container inspect "$AUTH_CONTAINER" >/dev/null 2>&1 || return 1
|
|
1271
|
+
docker run --rm -it --pull=never --network host --name "$AUTH_CONTAINER" --user 10001:10001 \
|
|
1272
|
+
--mount "type=bind,src=$AUTH_RUNTIME_ROOT,dst=/home/ouro/.ouro-cli" \
|
|
1273
|
+
--mount "type=bind,src=$AUTH_AGENT_ROOT,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
1274
|
+
--entrypoint node "$AUTH_IMAGE_ID" /opt/ouro/dist/heart/daemon/ouro-entry.js \
|
|
1275
|
+
auth --agent sanctuary --provider "$AUTH_PROVIDER" || return $?
|
|
1276
|
+
! docker container inspect "$AUTH_CONTAINER" >/dev/null 2>&1 || return 1
|
|
1277
|
+
}
|
|
1278
|
+
verify_sanctuary_provider_readiness() {
|
|
1279
|
+
READINESS_IMAGE_ID=$1
|
|
1280
|
+
validate_exact_image_id "$READINESS_IMAGE_ID" || return $?
|
|
1281
|
+
READINESS_RUNTIME_ROOT=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli
|
|
1282
|
+
READINESS_AGENT_ROOT=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro
|
|
1283
|
+
! docker container inspect ouro-butler-provider-readiness >/dev/null 2>&1 || return 1
|
|
1284
|
+
docker run --rm --pull=never --network host --name ouro-butler-provider-readiness --user 10001:10001 \
|
|
1285
|
+
--mount "type=bind,src=$READINESS_RUNTIME_ROOT,dst=/home/ouro/.ouro-cli" \
|
|
1286
|
+
--mount "type=bind,src=$READINESS_AGENT_ROOT,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
1287
|
+
--entrypoint /bin/sh "$READINESS_IMAGE_ID" -ceu '
|
|
1288
|
+
umask 077
|
|
1289
|
+
node /opt/ouro/dist/heart/daemon/ouro-entry.js check --agent sanctuary --lane outward
|
|
1290
|
+
node /opt/ouro/dist/heart/daemon/ouro-entry.js check --agent sanctuary --lane inner
|
|
1291
|
+
node - <<'"'"'NODE'"'"'
|
|
1292
|
+
(async () => {
|
|
1293
|
+
try {
|
|
1294
|
+
const fs = require("node:fs");
|
|
1295
|
+
const root = "/home/ouro/AgentBundles/sanctuary.ouro";
|
|
1296
|
+
const agent = JSON.parse(fs.readFileSync(`${root}/agent.json`, "utf8"));
|
|
1297
|
+
const policy = JSON.parse(fs.readFileSync(`${root}/provider-readiness.json`, "utf8"));
|
|
1298
|
+
const expectedPolicy = { version: 1, selectionPolicy: "explicit-same-lane-only" };
|
|
1299
|
+
const expected = new Map([
|
|
1300
|
+
["minimax", { provider: "minimax", model: "MiniMax-M3", vaultItem: "providers/minimax" }],
|
|
1301
|
+
]);
|
|
1302
|
+
const exactLane = value => value && value.provider === "minimax" && value.model === "MiniMax-M3";
|
|
1303
|
+
if (!exactLane(agent.humanFacing) || !exactLane(agent.agentFacing)
|
|
1304
|
+
|| policy.version !== expectedPolicy.version || policy.selectionPolicy !== expectedPolicy.selectionPolicy
|
|
1305
|
+
|| !Array.isArray(policy.providers) || policy.providers.length !== expected.size) throw new Error();
|
|
1306
|
+
for (const entry of policy.providers) {
|
|
1307
|
+
const wanted = expected.get(entry.provider);
|
|
1308
|
+
if (!wanted || entry.model !== wanted.model || entry.vaultItem !== wanted.vaultItem) throw new Error();
|
|
1309
|
+
expected.delete(entry.provider);
|
|
1310
|
+
}
|
|
1311
|
+
if (expected.size !== 0) throw new Error();
|
|
1312
|
+
const { refreshProviderCredentialPool } = require("/opt/ouro/dist/heart/provider-credentials.js");
|
|
1313
|
+
const refreshed = await refreshProviderCredentialPool("sanctuary", {
|
|
1314
|
+
providers: ["minimax"],
|
|
1315
|
+
skipCache: true,
|
|
1316
|
+
});
|
|
1317
|
+
if (!refreshed.ok) throw new Error();
|
|
1318
|
+
const minimax = refreshed.pool.providers["minimax"];
|
|
1319
|
+
const exactRecord = (record, provider) => record && record.provider === provider
|
|
1320
|
+
&& typeof record.revision === "string" && record.revision.length > 0
|
|
1321
|
+
&& typeof record.credentials?.apiKey === "string" && record.credentials.apiKey.trim().length > 0;
|
|
1322
|
+
if (!exactRecord(minimax, "minimax")) throw new Error();
|
|
1323
|
+
process.stdout.write("Sanctuary provider readiness verified.\n");
|
|
1324
|
+
} catch {
|
|
1325
|
+
process.stderr.write("Sanctuary provider readiness verification failed.\n");
|
|
1326
|
+
process.exitCode = 1;
|
|
1327
|
+
}
|
|
1328
|
+
})();
|
|
1329
|
+
NODE
|
|
1330
|
+
' || return $?
|
|
1331
|
+
! docker container inspect ouro-butler-provider-readiness >/dev/null 2>&1 || return 1
|
|
1332
|
+
validate_sanctuary_roots "$READINESS_RUNTIME_ROOT" "$READINESS_AGENT_ROOT" || return $?
|
|
1333
|
+
}
|
|
1334
|
+
run_sanctuary_docker() {
|
|
1335
|
+
/usr/bin/timeout -s KILL 20 /usr/bin/docker "$@"
|
|
1336
|
+
}
|
|
1337
|
+
run_sanctuary_unraid_api() {
|
|
1338
|
+
/usr/bin/timeout -s KILL 20 /usr/local/sbin/unraid-api "$@"
|
|
1339
|
+
}
|
|
1340
|
+
run_sanctuary_node() {
|
|
1341
|
+
/usr/local/bin/node "$@"
|
|
1342
|
+
}
|
|
1343
|
+
resolve_sanctuary_unraid_key() {
|
|
1344
|
+
run_sanctuary_node -e '
|
|
1345
|
+
const fs = require("node:fs");
|
|
1346
|
+
const root = "/boot/config/plugins/dynamix.my.servers/keys";
|
|
1347
|
+
const expected = process.argv[1];
|
|
1348
|
+
const found = [];
|
|
1349
|
+
for (const name of fs.readdirSync(root)) {
|
|
1350
|
+
if (!/^[A-Za-z0-9._:-]+\.json$/.test(name)) process.exit(1);
|
|
1351
|
+
const path = `${root}/${name}`;
|
|
1352
|
+
const stat = fs.lstatSync(path);
|
|
1353
|
+
if (!stat.isFile() || stat.isSymbolicLink()) process.exit(1);
|
|
1354
|
+
const value = JSON.parse(fs.readFileSync(path, "utf8"));
|
|
1355
|
+
if (value.id === expected) found.push({ path, name: value.name });
|
|
1356
|
+
}
|
|
1357
|
+
if (found.length !== 1 || typeof found[0].name !== "string" || !/^[A-Za-z0-9 ._:-]+$/.test(found[0].name)) process.exit(1);
|
|
1358
|
+
process.stdout.write(`${found[0].path}\t${found[0].name}`);
|
|
1359
|
+
' "$1"
|
|
1360
|
+
}
|
|
1361
|
+
sanctuary_revoked_key_recovery_path() {
|
|
1362
|
+
case "$1" in ''|*[!A-Za-z0-9._:-]*) return 1 ;; esac
|
|
1363
|
+
printf '%s/%s.json' /mnt/user/appdata/ouro-butler/acceptance/revoked-key-proof "$1"
|
|
1364
|
+
}
|
|
1365
|
+
validate_sanctuary_revoked_key_recovery() {
|
|
1366
|
+
test -f "$1" && test ! -L "$1" || return 1
|
|
1367
|
+
test "$(stat -c '%u:%g %a' "$1")" = "0:0 600" || return 1
|
|
1368
|
+
}
|
|
1369
|
+
preserve_sanctuary_revoked_key() {
|
|
1370
|
+
KEY_ID=$1
|
|
1371
|
+
KEY_SOURCE=$2
|
|
1372
|
+
RECOVERY_ROOT=/mnt/user/appdata/ouro-butler/acceptance/revoked-key-proof
|
|
1373
|
+
test -f "$KEY_SOURCE" && test ! -L "$KEY_SOURCE" || return 1
|
|
1374
|
+
if test ! -e "$RECOVERY_ROOT"; then
|
|
1375
|
+
install -d -m 0700 -o root -g root "$RECOVERY_ROOT" || return $?
|
|
1376
|
+
fi
|
|
1377
|
+
test -d "$RECOVERY_ROOT" && test ! -L "$RECOVERY_ROOT" || return 1
|
|
1378
|
+
test "$(stat -c '%u:%g %a' "$RECOVERY_ROOT")" = "0:0 700" || return 1
|
|
1379
|
+
RECOVERY_PATH=$(sanctuary_revoked_key_recovery_path "$KEY_ID") || return $?
|
|
1380
|
+
test ! -e "$RECOVERY_PATH" || return 1
|
|
1381
|
+
RECOVERY_TMP=$(mktemp "$RECOVERY_ROOT/$KEY_ID.json.tmp.XXXXXX") || return $?
|
|
1382
|
+
trap 'rm -f -- "$RECOVERY_TMP"' EXIT || return $?
|
|
1383
|
+
install -m 0600 -o root -g root "$KEY_SOURCE" "$RECOVERY_TMP" || return $?
|
|
1384
|
+
cmp -s "$KEY_SOURCE" "$RECOVERY_TMP" || return 1
|
|
1385
|
+
mv -f -- "$RECOVERY_TMP" "$RECOVERY_PATH" || return $?
|
|
1386
|
+
sync -f "$RECOVERY_ROOT" || return $?
|
|
1387
|
+
trap - EXIT || return $?
|
|
1388
|
+
}
|
|
1389
|
+
clear_sanctuary_revoked_key() {
|
|
1390
|
+
RECOVERY_PATH=$(sanctuary_revoked_key_recovery_path "$1") || return $?
|
|
1391
|
+
test -f "$RECOVERY_PATH" && test ! -L "$RECOVERY_PATH" || return 1
|
|
1392
|
+
test "$(stat -c '%u:%g %a' "$RECOVERY_PATH")" = "0:0 600" || return 1
|
|
1393
|
+
rm -f -- "$RECOVERY_PATH" || return $?
|
|
1394
|
+
sync -f "$(dirname "$RECOVERY_PATH")" || return $?
|
|
1395
|
+
}
|
|
1396
|
+
verify_vault_backed_unraid_key() {
|
|
1397
|
+
KEY_ID=$1
|
|
1398
|
+
CAPABILITY=$2
|
|
1399
|
+
case "$KEY_ID" in ''|*[!A-Za-z0-9._:-]*) return 1 ;; esac
|
|
1400
|
+
case "$CAPABILITY" in read-only|bounded-write) ;; *) return 1 ;; esac
|
|
1401
|
+
IMAGE_DIGEST=${IMAGE_ID#sha256:}
|
|
1402
|
+
test "$IMAGE_DIGEST" != "$IMAGE_ID" || return 1
|
|
1403
|
+
test "${#IMAGE_DIGEST}" -eq 64 || return 1
|
|
1404
|
+
case "$IMAGE_DIGEST" in *[!0-9a-f]*) return 1 ;; esac
|
|
1405
|
+
test "$(run_sanctuary_docker image inspect --format '{{.Id}}' "$IMAGE_ID")" = "$IMAGE_ID" || return $?
|
|
1406
|
+
KEY_TARGET=$(resolve_sanctuary_unraid_key "$KEY_ID") || return $?
|
|
1407
|
+
KEY_PATH=${KEY_TARGET%% *}
|
|
1408
|
+
test "$KEY_PATH" != "$KEY_TARGET" || return 1
|
|
1409
|
+
test -f "$KEY_PATH" && test ! -L "$KEY_PATH" || return 1
|
|
1410
|
+
VERIFY_RESULT=$(run_sanctuary_docker run --rm --pull=never --network host \
|
|
1411
|
+
--user 0:0 \
|
|
1412
|
+
--mount type=bind,src=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli,dst=/home/ouro/.ouro-cli,readonly \
|
|
1413
|
+
--mount type=bind,src=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro,dst=/home/ouro/AgentBundles/sanctuary.ouro,readonly \
|
|
1414
|
+
--mount type=bind,src="$KEY_PATH",dst=/run/ouro-acceptance/unraid-key.json,readonly \
|
|
1415
|
+
--entrypoint /opt/ouro/deploy/unraid/sanctuary-acceptance-adapter.sh \
|
|
1416
|
+
"$IMAGE_ID" vault-probe "$KEY_ID" "$CAPABILITY") || return $?
|
|
1417
|
+
printf '%s' "$VERIFY_RESULT" | run_sanctuary_node -e '
|
|
1418
|
+
let input = "";
|
|
1419
|
+
process.stdin.setEncoding("utf8");
|
|
1420
|
+
process.stdin.on("data", chunk => { input += chunk; });
|
|
1421
|
+
process.stdin.on("end", () => {
|
|
1422
|
+
const value = JSON.parse(input);
|
|
1423
|
+
const expectedId = process.argv[1];
|
|
1424
|
+
const expectedCapability = process.argv[2];
|
|
1425
|
+
const expectedProof = expectedCapability === "read-only"
|
|
1426
|
+
? "read-authorized-write-denied"
|
|
1427
|
+
: "read-authorized-write-reached-not-found";
|
|
1428
|
+
if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify(["capability", "keyId", "proof", "valid"])) process.exit(1);
|
|
1429
|
+
if (value.valid !== true || value.keyId !== expectedId || value.capability !== expectedCapability) process.exit(1);
|
|
1430
|
+
if (value.proof !== expectedProof) process.exit(1);
|
|
1431
|
+
});
|
|
1432
|
+
' "$KEY_ID" "$CAPABILITY" || return $?
|
|
1433
|
+
}
|
|
1434
|
+
inventory_unraid_key_ids() {
|
|
1435
|
+
IMAGE_DIGEST=${IMAGE_ID#sha256:}
|
|
1436
|
+
test "$IMAGE_DIGEST" != "$IMAGE_ID" || return 1
|
|
1437
|
+
test "${#IMAGE_DIGEST}" -eq 64 || return 1
|
|
1438
|
+
case "$IMAGE_DIGEST" in *[!0-9a-f]*) return 1 ;; esac
|
|
1439
|
+
test "$(run_sanctuary_docker image inspect --format '{{.Id}}' "$IMAGE_ID")" = "$IMAGE_ID" || return $?
|
|
1440
|
+
INVENTORY_RESULT=$(printf '%s' '{"operation":"closed-inventory"}' | \
|
|
1441
|
+
run_sanctuary_docker run --rm -i --pull=never --network none \
|
|
1442
|
+
--user 0:0 --read-only \
|
|
1443
|
+
--mount type=bind,src=/boot/config/plugins/dynamix.my.servers/keys,dst=/boot/config/plugins/dynamix.my.servers/keys,readonly \
|
|
1444
|
+
--entrypoint /opt/ouro/deploy/unraid/sanctuary-acceptance-adapter.sh "$IMAGE_ID") || return $?
|
|
1445
|
+
printf '%s' "$INVENTORY_RESULT" | run_sanctuary_node -e '
|
|
1446
|
+
let input = "";
|
|
1447
|
+
process.stdin.setEncoding("utf8");
|
|
1448
|
+
process.stdin.on("data", chunk => { input += chunk; });
|
|
1449
|
+
process.stdin.on("end", () => {
|
|
1450
|
+
const value = JSON.parse(input);
|
|
1451
|
+
if (!value || JSON.stringify(Object.keys(value)) !== JSON.stringify(["keys"]) || !Array.isArray(value.keys)) process.exit(1);
|
|
1452
|
+
for (const key of value.keys) {
|
|
1453
|
+
if (!key || JSON.stringify(Object.keys(key).sort()) !== JSON.stringify(["id", "roles", "scope"])) process.exit(1);
|
|
1454
|
+
if (!/^[A-Za-z0-9._:-]+$/.test(key.id)) process.exit(1);
|
|
1455
|
+
if (!["read-only", "bounded-write", "legacy-write"].includes(key.scope) || key.roles !== "none") process.exit(1);
|
|
1456
|
+
process.stdout.write(`${key.id}\t${key.scope}\t${key.roles}\n`);
|
|
1457
|
+
}
|
|
1458
|
+
});
|
|
1459
|
+
' || return $?
|
|
1460
|
+
}
|
|
1461
|
+
revoke_unraid_key_exact() {
|
|
1462
|
+
KEY_ID=$1
|
|
1463
|
+
case "$KEY_ID" in ''|*[!A-Za-z0-9._:-]*) return 1 ;; esac
|
|
1464
|
+
test "${REVOKED_KEY_FD_OPEN:-no}" = no || return 1
|
|
1465
|
+
KEY_TARGET=$(resolve_sanctuary_unraid_key "$KEY_ID") || return $?
|
|
1466
|
+
KEY_PATH=${KEY_TARGET%% *}
|
|
1467
|
+
KEY_NAME=${KEY_TARGET#* }
|
|
1468
|
+
test "$KEY_PATH" != "$KEY_TARGET" || return 1
|
|
1469
|
+
test -f "$KEY_PATH" && test ! -L "$KEY_PATH" || return $?
|
|
1470
|
+
preserve_sanctuary_revoked_key "$KEY_ID" "$KEY_PATH" || return $?
|
|
1471
|
+
if ! REVOKE_RESULT=$(run_sanctuary_unraid_api apikey --name "$KEY_NAME" --delete --json); then
|
|
1472
|
+
return 1
|
|
1473
|
+
fi
|
|
1474
|
+
if ! printf '%s' "$REVOKE_RESULT" | run_sanctuary_node -e '
|
|
1475
|
+
let input = "";
|
|
1476
|
+
process.stdin.setEncoding("utf8");
|
|
1477
|
+
process.stdin.on("data", chunk => { input += chunk; });
|
|
1478
|
+
process.stdin.on("end", () => {
|
|
1479
|
+
const value = JSON.parse(input);
|
|
1480
|
+
const expectedId = process.argv[1];
|
|
1481
|
+
const expectedName = process.argv[2];
|
|
1482
|
+
if (value.deleted !== 1 || !Array.isArray(value.keys) || value.keys.length !== 1) process.exit(1);
|
|
1483
|
+
if (value.keys[0].id !== expectedId || value.keys[0].name !== expectedName) process.exit(1);
|
|
1484
|
+
});
|
|
1485
|
+
' "$KEY_ID" "$KEY_NAME"; then
|
|
1486
|
+
return 1
|
|
1487
|
+
fi
|
|
1488
|
+
}
|
|
1489
|
+
verify_revoked_unraid_key_rejected() {
|
|
1490
|
+
KEY_ID=$1
|
|
1491
|
+
case "$KEY_ID" in ''|*[!A-Za-z0-9._:-]*) return 1 ;; esac
|
|
1492
|
+
REVOKED_KEY_RECOVERY_PATH=$(sanctuary_revoked_key_recovery_path "$KEY_ID") || return $?
|
|
1493
|
+
validate_sanctuary_revoked_key_recovery "$REVOKED_KEY_RECOVERY_PATH" || return $?
|
|
1494
|
+
IMAGE_DIGEST=${IMAGE_ID#sha256:}
|
|
1495
|
+
test "$IMAGE_DIGEST" != "$IMAGE_ID" || return 1
|
|
1496
|
+
test "${#IMAGE_DIGEST}" -eq 64 || return 1
|
|
1497
|
+
case "$IMAGE_DIGEST" in *[!0-9a-f]*) return 1 ;; esac
|
|
1498
|
+
test "$(run_sanctuary_docker image inspect --format '{{.Id}}' "$IMAGE_ID")" = "$IMAGE_ID" || return 1
|
|
1499
|
+
REJECT_ATTEMPT=0
|
|
1500
|
+
while test "$REJECT_ATTEMPT" -lt 3; do
|
|
1501
|
+
REJECT_ATTEMPT=$((REJECT_ATTEMPT + 1))
|
|
1502
|
+
if REJECT_RESULT=$(run_sanctuary_docker run --rm -i --pull=never --network host \
|
|
1503
|
+
--user 10001:10001 --read-only \
|
|
1504
|
+
--entrypoint /bin/sh "$IMAGE_ID" -ceu \
|
|
1505
|
+
'exec 3<&0; exec /opt/ouro/deploy/unraid/sanctuary-acceptance-adapter.sh revoked-probe "$1" "$2" 3<&3' \
|
|
1506
|
+
sanctuary-revoked-probe "$KEY_ID" http://127.0.0.1/graphql <"$REVOKED_KEY_RECOVERY_PATH") \
|
|
1507
|
+
&& printf '%s' "$REJECT_RESULT" | run_sanctuary_node -e '
|
|
1508
|
+
let input = "";
|
|
1509
|
+
process.stdin.setEncoding("utf8");
|
|
1510
|
+
process.stdin.on("data", chunk => { input += chunk; });
|
|
1511
|
+
process.stdin.on("end", () => {
|
|
1512
|
+
const value = JSON.parse(input);
|
|
1513
|
+
if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify(["id", "rejected", "status"])) process.exit(1);
|
|
1514
|
+
if (value.rejected !== true || value.id !== process.argv[1] || ![401, 403].includes(value.status)) process.exit(1);
|
|
1515
|
+
});
|
|
1516
|
+
' "$KEY_ID"; then
|
|
1517
|
+
clear_sanctuary_revoked_key "$KEY_ID" || return $?
|
|
1518
|
+
return 0
|
|
1519
|
+
fi
|
|
1520
|
+
test "$REJECT_ATTEMPT" -lt 3 || return 1
|
|
1521
|
+
sleep 2
|
|
1522
|
+
done
|
|
1523
|
+
return 1
|
|
1524
|
+
}
|
|
1525
|
+
Update:
|
|
1526
|
+
Build and verify a new ouro-butler:<version> image first. The tag is only a
|
|
1527
|
+
lookup handle and never authorizes container creation. Resolve and validate
|
|
1528
|
+
the exact local Docker image ID before staging:
|
|
1529
|
+
IMAGE_TAG=ouro-butler:<version>
|
|
1530
|
+
IMAGE_ID=$(docker image inspect --format '{{.Id}}' "$IMAGE_TAG")
|
|
1531
|
+
printf '%s\n' "$IMAGE_ID" | grep -Eq '^sha256:[0-9a-f]{64}$'
|
|
1532
|
+
docker image inspect "$IMAGE_ID" >/dev/null
|
|
1533
|
+
AUDIT_RUNNER_IMAGE_ID=$IMAGE_ID
|
|
1534
|
+
validate_exact_image_id "$AUDIT_RUNNER_IMAGE_ID"
|
|
1535
|
+
Before stopping, renaming, or creating any Butler container, extract the packaged event, template, and runtime-policy assets from that exact image ID. Install the event assets transactionally, bind the staged template to the exact local image ID, and audit both staged deployment files. Do not copy these files from a checkout or another image:
|
|
1536
|
+
EVENT_ASSET_STAGE=$(mktemp -d /mnt/user/appdata/ouro-butler/staging/ouro-events.XXXXXX)
|
|
1537
|
+
chmod 0700 "$EVENT_ASSET_STAGE"
|
|
1538
|
+
EVENT_SCRIPT_STAGE="$EVENT_ASSET_STAGE/ouro-events"
|
|
1539
|
+
mkdir "$EVENT_SCRIPT_STAGE"
|
|
1540
|
+
STAGED_TEMPLATE="$EVENT_ASSET_STAGE/sanctuary.xml"
|
|
1541
|
+
STAGED_RUNTIME_POLICY="$EVENT_ASSET_STAGE/container-runtime.json"
|
|
1542
|
+
EVENT_ASSET_CONTAINER=
|
|
1543
|
+
cleanup_event_asset_stage() {
|
|
1544
|
+
if test -n "$EVENT_ASSET_CONTAINER"; then
|
|
1545
|
+
docker rm --force "$EVENT_ASSET_CONTAINER" >/dev/null 2>&1 || true
|
|
1546
|
+
fi
|
|
1547
|
+
rm -rf -- "$EVENT_ASSET_STAGE"
|
|
1548
|
+
}
|
|
1549
|
+
trap cleanup_event_asset_stage EXIT
|
|
1550
|
+
EVENT_ASSET_CONTAINER=$(docker create --pull=never --network none --read-only --entrypoint /bin/false "$IMAGE_ID")
|
|
1551
|
+
test "$(docker inspect --format '{{.Image}}' "$EVENT_ASSET_CONTAINER")" = "$IMAGE_ID"
|
|
1552
|
+
docker cp "$EVENT_ASSET_CONTAINER:/opt/ouro/deploy/unraid/ouro-events/." "$EVENT_SCRIPT_STAGE/"
|
|
1553
|
+
docker cp "$EVENT_ASSET_CONTAINER:/opt/ouro/deploy/unraid/sanctuary.xml" "$STAGED_TEMPLATE"
|
|
1554
|
+
docker cp "$EVENT_ASSET_CONTAINER:/opt/ouro/deploy/unraid/container-runtime.json" "$STAGED_RUNTIME_POLICY"
|
|
1555
|
+
docker rm "$EVENT_ASSET_CONTAINER"
|
|
1556
|
+
EVENT_ASSET_CONTAINER=
|
|
1557
|
+
EXPECTED_RELEASE_ASSETS=$(printf '%s\n' container-runtime.json ouro-events sanctuary.xml)
|
|
1558
|
+
ACTUAL_RELEASE_ASSETS=$(find "$EVENT_ASSET_STAGE" -mindepth 1 -maxdepth 1 -exec basename {} \; | LC_ALL=C sort)
|
|
1559
|
+
test "$ACTUAL_RELEASE_ASSETS" = "$EXPECTED_RELEASE_ASSETS"
|
|
1560
|
+
test -d "$EVENT_SCRIPT_STAGE" && test ! -L "$EVENT_SCRIPT_STAGE"
|
|
1561
|
+
test -f "$STAGED_TEMPLATE" && test ! -L "$STAGED_TEMPLATE"
|
|
1562
|
+
test -f "$STAGED_RUNTIME_POLICY" && test ! -L "$STAGED_RUNTIME_POLICY"
|
|
1563
|
+
EXPECTED_EVENT_ASSETS=$(printf '%s\n' bootstrap-spool.sh emit-event.mjs emit-usenet-event.sh install-usenet-guard.sh usenet-health.sh)
|
|
1564
|
+
ACTUAL_EVENT_ASSETS=$(find "$EVENT_SCRIPT_STAGE" -mindepth 1 -maxdepth 1 -type f -exec basename {} \; | LC_ALL=C sort)
|
|
1565
|
+
test "$ACTUAL_EVENT_ASSETS" = "$EXPECTED_EVENT_ASSETS"
|
|
1566
|
+
test -z "$(find "$EVENT_SCRIPT_STAGE" -mindepth 1 -maxdepth 1 \( -type l -o ! -type f \) -print -quit)"
|
|
1567
|
+
/bin/bash "$EVENT_SCRIPT_STAGE/install-usenet-guard.sh" --source-root "$EVENT_SCRIPT_STAGE"
|
|
1568
|
+
/bin/bash /boot/config/custom/ouro-events/install-usenet-guard.sh --boot --install-root /boot/config/custom
|
|
1569
|
+
verify_installed_usenet_guard() {
|
|
1570
|
+
test "$(findmnt -n -o FSTYPE --target /boot/config/custom)" = vfat
|
|
1571
|
+
test "$(stat -c '%u:%g:%a' /boot/config/custom/usenet_health.sh)" = 0:0:600
|
|
1572
|
+
test "$(stat -c '%u:%g:%a' /boot/config/custom/ouro-events/bootstrap-spool.sh)" = 0:0:600
|
|
1573
|
+
test "$(stat -c '%u:%g:%a' /boot/config/custom/ouro-events/emit-event.mjs)" = 0:0:600
|
|
1574
|
+
test "$(stat -c '%u:%g:%a' /boot/config/custom/ouro-events/emit-usenet-event.sh)" = 0:0:600
|
|
1575
|
+
test "$(stat -c '%u:%g:%a' /boot/config/custom/ouro-events/install-usenet-guard.sh)" = 0:0:600
|
|
1576
|
+
cmp -s "$EVENT_SCRIPT_STAGE/usenet-health.sh" /boot/config/custom/usenet_health.sh
|
|
1577
|
+
cmp -s "$EVENT_SCRIPT_STAGE/bootstrap-spool.sh" /boot/config/custom/ouro-events/bootstrap-spool.sh
|
|
1578
|
+
cmp -s "$EVENT_SCRIPT_STAGE/emit-event.mjs" /boot/config/custom/ouro-events/emit-event.mjs
|
|
1579
|
+
cmp -s "$EVENT_SCRIPT_STAGE/emit-usenet-event.sh" /boot/config/custom/ouro-events/emit-usenet-event.sh
|
|
1580
|
+
cmp -s "$EVENT_SCRIPT_STAGE/install-usenet-guard.sh" /boot/config/custom/ouro-events/install-usenet-guard.sh
|
|
1581
|
+
test "$(grep -Fxc '/bin/bash /boot/config/custom/ouro-events/install-usenet-guard.sh --boot --install-root /boot/config/custom' /boot/config/go)" = 1
|
|
1582
|
+
test "$(grep -Fxc '/boot/config/custom/ouro-events/bootstrap-spool.sh --mount' /boot/config/go || true)" = 0
|
|
1583
|
+
test "$(grep -Fxc '(crontab -l 2>/dev/null | grep -v usenet_health; echo "*/15 * * * * /bin/bash /boot/config/custom/usenet_health.sh") | crontab -' /boot/config/go || true)" = 0
|
|
1584
|
+
INSTALLED_GUARD_CRON=$(crontab -l)
|
|
1585
|
+
test "$(printf '%s\n' "$INSTALLED_GUARD_CRON" | grep -Fxc '*/15 * * * * /bin/bash /boot/config/custom/usenet_health.sh # ouro:usenet-health')" = 1
|
|
1586
|
+
INSTALLED_LEGACY_GUARD_COUNT=$(printf '%s\n' "$INSTALLED_GUARD_CRON" | grep -Fxc '*/15 * * * * /bin/bash /boot/config/custom/usenet_health.sh' || true)
|
|
1587
|
+
test "$INSTALLED_LEGACY_GUARD_COUNT" = 0
|
|
1588
|
+
test "$(printf '%s\n' "$INSTALLED_GUARD_CRON" | grep -Fc '/bin/bash /boot/config/custom/usenet_health.sh')" = 1
|
|
1589
|
+
test "$(findmnt -n -o FSTYPE --target /boot/config/custom/ouro-events/spool)" = tmpfs
|
|
1590
|
+
test "$(stat -c '%u:%g:%a' /boot/config/custom/ouro-events/spool)" = 0:0:755
|
|
1591
|
+
INSTALLED_SPOOL_OPTIONS=$(findmnt -n -o OPTIONS --target /boot/config/custom/ouro-events/spool)
|
|
1592
|
+
for INSTALLED_SPOOL_OPTION in nodev nosuid noexec; do
|
|
1593
|
+
case ",$INSTALLED_SPOOL_OPTIONS," in *",$INSTALLED_SPOOL_OPTION,"*) ;; *) return 1 ;; esac
|
|
1594
|
+
done
|
|
1595
|
+
INSTALLED_SPOOL_SIZE=$(findmnt -bn -o SIZE --target /boot/config/custom/ouro-events/spool)
|
|
1596
|
+
test "$INSTALLED_SPOOL_SIZE" -le 4194304
|
|
1597
|
+
}
|
|
1598
|
+
verify_installed_usenet_guard
|
|
1599
|
+
/usr/local/bin/node -e '
|
|
1600
|
+
const fs = require("node:fs");
|
|
1601
|
+
const [templatePath, imageId] = process.argv.slice(1);
|
|
1602
|
+
if (!/^sha256:[0-9a-f]{64}$/.test(imageId)) throw new Error("invalid exact image ID");
|
|
1603
|
+
const source = fs.readFileSync(templatePath, "utf8");
|
|
1604
|
+
const repositories = [...source.matchAll(/<Repository>[^<]*<\/Repository>/g)];
|
|
1605
|
+
if (repositories.length !== 1) throw new Error("template must contain exactly one Repository element");
|
|
1606
|
+
const staged = source.replace(repositories[0][0], `<Repository>${imageId}</Repository>`);
|
|
1607
|
+
if ((staged.match(/<Repository>[^<]*<\/Repository>/g) ?? []).length !== 1
|
|
1608
|
+
|| !staged.includes(`<Repository>${imageId}</Repository>`)) throw new Error("exact Repository staging failed");
|
|
1609
|
+
fs.writeFileSync(templatePath, staged);
|
|
1610
|
+
' "$STAGED_TEMPLATE" "$IMAGE_ID"
|
|
1611
|
+
If extraction, transactional installation, boot activation, or verification fails, abort the update before any Butler mutation. The installer restores the previous assets, go file, and cron on transactional failure; production stays running. If boot activation or verification fails after the transaction commits, leave production untouched, repair or rerun this exact-image installation, and do not continue. Container rollback begins only after the later production stop succeeds.
|
|
1612
|
+
Initial install/adoption is a separate terminal path for the verified live
|
|
1613
|
+
legacy state: no production or rollback, exactly one running (possibly
|
|
1614
|
+
unhealthy) ouro-butler-staging, and no legacy-evidence container. After the
|
|
1615
|
+
helper definitions above are loaded and IMAGE_ID is resolved, run this exact
|
|
1616
|
+
sequence. MiniMax credentials are imported from the byte-verified legacy
|
|
1617
|
+
bootstrap envelope; do not place credentials in arguments, shell variables, or
|
|
1618
|
+
history.
|
|
1619
|
+
These provider-readiness commands are adoption-only; do not use them as a
|
|
1620
|
+
normal-update precheck against an active canonical production daemon.
|
|
1621
|
+
Sanctuary legacy adoption commands:
|
|
1622
|
+
prepare_sanctuary_legacy_adoption "$IMAGE_ID"
|
|
1623
|
+
verify_sanctuary_provider_readiness "$IMAGE_ID"
|
|
1624
|
+
install_from_legacy_staging
|
|
1625
|
+
These commands are one terminal path; after install succeeds, stop and do not
|
|
1626
|
+
continue into the normal update below. The final install is noninteractive:
|
|
1627
|
+
it reruns resumable preparation plus fresh readiness, but never authentication.
|
|
1628
|
+
That function never applies the canonical auditor to the known-noncanonical
|
|
1629
|
+
legacy container or restarts it. It verifies and pins the legacy image,
|
|
1630
|
+
provisions the absent canonical roots from the exact target image's packaged
|
|
1631
|
+
Sanctuary skeleton, proves required files plus exact ownership/modes, and runs
|
|
1632
|
+
same-image vault bootstrap and provider readiness before durably snapshotting
|
|
1633
|
+
both exact container and image inspect records. Any preparation, vault, or
|
|
1634
|
+
readiness failure returns while legacy remains running with autostart
|
|
1635
|
+
untouched. It then stops and rechecks the legacy container and renames it to
|
|
1636
|
+
stopped ouro-butler-legacy-evidence. A fresh canonical staging container is
|
|
1637
|
+
created from exact IMAGE_ID and canonical binds, audited, started, proven to
|
|
1638
|
+
be the only running Butler, readiness-checked, then stopped and removed only
|
|
1639
|
+
after its image is rechecked. Only then is fresh canonical production created,
|
|
1640
|
+
audited, started, readiness-checked, and placed in bare production autostart.
|
|
1641
|
+
Failure removes only partial target-image staging/production containers,
|
|
1642
|
+
quarantines the exact legacy container without deletion or restart, leaves
|
|
1643
|
+
autostart disabled, and propagates the original status.
|
|
1644
|
+
For this cutover, preflight either the exact known alpha.742 two-mount source
|
|
1645
|
+
or an already-canonical source before any autostart or container mutation.
|
|
1646
|
+
Production must be the only running Butler poller;
|
|
1647
|
+
staging must be absent; rollback may be absent or one stopped container with
|
|
1648
|
+
the exact production image. A stopped legacy-evidence container is preserved.
|
|
1649
|
+
Disable every Butler name in Unraid's array-autostart file and verify that
|
|
1650
|
+
result before stopping production. First resolve and validate the exact image
|
|
1651
|
+
ID of the known-good production container while it is still running, so a
|
|
1652
|
+
lookup failure cannot strand a renamed container:
|
|
1653
|
+
recover_pending_sanctuary_bundle_migration "$IMAGE_ID"
|
|
1654
|
+
/bin/bash /boot/config/custom/ouro-events/bootstrap-spool.sh --mount
|
|
1655
|
+
test "$(findmnt -n -o FSTYPE --target /boot/config/custom/ouro-events/spool)" = tmpfs
|
|
1656
|
+
test "$(stat -c '%u:%g:%a' /boot/config/custom/ouro-events/spool)" = 0:0:755
|
|
1657
|
+
ROLLBACK_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler)
|
|
1658
|
+
validate_exact_image_id "$ROLLBACK_IMAGE_ID"
|
|
1659
|
+
if assert_update_topology "$ROLLBACK_IMAGE_ID"; then
|
|
1660
|
+
:
|
|
1661
|
+
else
|
|
1662
|
+
UPDATE_PREFLIGHT_STATUS=$?
|
|
1663
|
+
(exit "$UPDATE_PREFLIGHT_STATUS")
|
|
1664
|
+
fi
|
|
1665
|
+
assert_update_source "$ROLLBACK_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID"
|
|
1666
|
+
The earlier exact-image asset stage has already replaced the template's sole
|
|
1667
|
+
Repository element with $IMAGE_ID. After the topology and source gates pass,
|
|
1668
|
+
audit the staged template and runtime policy with the packaged auditor, then
|
|
1669
|
+
remove the private stage before any Butler mutation:
|
|
1670
|
+
docker run --rm --pull=never --network=none \
|
|
1671
|
+
--user 0:0 --read-only --cap-drop=ALL --security-opt=no-new-privileges \
|
|
1672
|
+
--entrypoint /opt/ouro/deploy/unraid/audit-container-spec.sh \
|
|
1673
|
+
--mount "type=bind,src=$STAGED_TEMPLATE,dst=/audit/sanctuary.xml,readonly" \
|
|
1674
|
+
--mount "type=bind,src=$STAGED_RUNTIME_POLICY,dst=/audit/container-runtime.json,readonly" \
|
|
1675
|
+
"$IMAGE_ID" --template /audit/sanctuary.xml --runtime-policy /audit/container-runtime.json --expected-image "$IMAGE_ID"
|
|
1676
|
+
cleanup_event_asset_stage
|
|
1677
|
+
trap - EXIT
|
|
1678
|
+
if provision_sanctuary_sab_credential "$IMAGE_ID" \
|
|
1679
|
+
&& verify_sanctuary_sab_readiness "$IMAGE_ID"; then
|
|
1680
|
+
:
|
|
1681
|
+
else
|
|
1682
|
+
SAB_READINESS_STATUS=$?
|
|
1683
|
+
(exit "$SAB_READINESS_STATUS")
|
|
1684
|
+
fi
|
|
1685
|
+
Guard the atomic autostart disable separately. If it fails, production has not
|
|
1686
|
+
been touched and the captured status is propagated:
|
|
1687
|
+
if disable_butler_autostart; then
|
|
1688
|
+
:
|
|
1689
|
+
else
|
|
1690
|
+
AUTOSTART_DISABLE_STATUS=$?
|
|
1691
|
+
(exit "$AUTOSTART_DISABLE_STATUS")
|
|
1692
|
+
fi
|
|
1693
|
+
Define the stale-rollback cleanup used by the preparation guard:
|
|
1694
|
+
remove_stopped_rollback_if_present() {
|
|
1695
|
+
EXPECTED_STALE_ROLLBACK_IMAGE_ID=$1
|
|
1696
|
+
ROLLBACK_CONTAINER_NAMES=$(docker container ls -a --format '{{.Names}}') || return $?
|
|
1697
|
+
case "
|
|
1698
|
+
$ROLLBACK_CONTAINER_NAMES
|
|
1699
|
+
" in
|
|
1700
|
+
*"
|
|
1701
|
+
ouro-butler-rollback
|
|
1702
|
+
"*)
|
|
1703
|
+
ROLLBACK_RUNNING=$(docker inspect --format '{{.State.Running}}' ouro-butler-rollback) || return $?
|
|
1704
|
+
test "$ROLLBACK_RUNNING" = false || return $?
|
|
1705
|
+
CURRENT_STALE_ROLLBACK_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-rollback) || return $?
|
|
1706
|
+
test "$CURRENT_STALE_ROLLBACK_IMAGE_ID" = "$EXPECTED_STALE_ROLLBACK_IMAGE_ID" || return $?
|
|
1707
|
+
docker rm ouro-butler-rollback || return $?
|
|
1708
|
+
;;
|
|
1709
|
+
esac
|
|
1710
|
+
}
|
|
1711
|
+
Stop production, remove only a stopped stale rollback, rename the known-good
|
|
1712
|
+
container, verify it remains stopped, and apply the exact target image's
|
|
1713
|
+
package-managed bundle migration in one explicit preparation guard.
|
|
1714
|
+
Package-managed files are exactly `provider-readiness.json`,
|
|
1715
|
+
`tool-profiles.json`, `habits/sanctuary-health.md`, and the five canonical
|
|
1716
|
+
files under `psyche/`. The migration also merges the three bundle-meta version
|
|
1717
|
+
fields. It never installs or mutates steward policy authority. It preserves
|
|
1718
|
+
agent.json, all steward policy and audit bytes, relationships, sessions, and
|
|
1719
|
+
every other state path. Repeating it is a no-op.
|
|
1720
|
+
Before its first managed write, the migrator atomically fsyncs one mode-0600
|
|
1721
|
+
rollback record at `.sanctuary-package-managed-rollback.json`. That transient
|
|
1722
|
+
record contains a verified SHA-256 digest over the exact prior bytes, modes,
|
|
1723
|
+
and parent existence for those
|
|
1724
|
+
files plus bundle-meta, bound to distinct exact rollback and target image IDs.
|
|
1725
|
+
It is the migration transaction record, not a second bundle authority. Normal
|
|
1726
|
+
managed-file failure restores it immediately; every
|
|
1727
|
+
later container rollback restores it before the old container restarts, while
|
|
1728
|
+
successful production readiness and autostart commit it. A killed updater is
|
|
1729
|
+
reconciled from the same record before update topology preflight: partial new
|
|
1730
|
+
containers are removed, interrupted atomic-write stages are discarded, the
|
|
1731
|
+
exact prior bundle is restored idempotently, and only the image ID recorded in
|
|
1732
|
+
the transaction may resume as production. A malformed record, wrong image,
|
|
1733
|
+
or restore failure leaves autostart disabled and fails closed. Once target
|
|
1734
|
+
readiness and autostart have passed, commit first durably renames the record;
|
|
1735
|
+
a kill then keeps target production in place and the next run validates its
|
|
1736
|
+
exact topology/readiness before finishing commit. Migration failure then enters the same exact container
|
|
1737
|
+
rollback arm before staging starts:
|
|
1738
|
+
if docker stop ouro-butler \
|
|
1739
|
+
&& remove_stopped_rollback_if_present "$ROLLBACK_IMAGE_ID" \
|
|
1740
|
+
&& docker rename ouro-butler ouro-butler-rollback \
|
|
1741
|
+
&& test "$(docker inspect --format '{{.State.Running}}' ouro-butler-rollback)" = false \
|
|
1742
|
+
&& assert_only_running_butler - \
|
|
1743
|
+
&& migrate_sanctuary_package_managed_bundle "$IMAGE_ID" migrate "$ROLLBACK_IMAGE_ID"; then
|
|
1744
|
+
:
|
|
1745
|
+
else
|
|
1746
|
+
PRODUCTION_PREPARATION_STATUS=$?
|
|
1747
|
+
if docker container inspect ouro-butler >/dev/null 2>&1; then
|
|
1748
|
+
if docker container inspect ouro-butler-rollback >/dev/null 2>&1; then
|
|
1749
|
+
docker stop ouro-butler-rollback >/dev/null 2>&1 || true
|
|
1750
|
+
CURRENT_ROLLBACK_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-rollback)
|
|
1751
|
+
test "$CURRENT_ROLLBACK_IMAGE_ID" = "$ROLLBACK_IMAGE_ID"
|
|
1752
|
+
docker rm --force ouro-butler-rollback >/dev/null 2>&1 || true
|
|
1753
|
+
fi
|
|
1754
|
+
! docker container inspect ouro-butler-rollback >/dev/null 2>&1
|
|
1755
|
+
rollback_sanctuary_bundle_if_pending "$IMAGE_ID"
|
|
1756
|
+
assert_update_source "$ROLLBACK_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID"
|
|
1757
|
+
docker start ouro-butler
|
|
1758
|
+
assert_only_running_butler ouro-butler
|
|
1759
|
+
wait_butler_ready ouro-butler
|
|
1760
|
+
enable_butler_autostart
|
|
1761
|
+
migrate_sanctuary_package_managed_bundle "$IMAGE_ID" commit
|
|
1762
|
+
elif docker container inspect ouro-butler-rollback >/dev/null 2>&1; then
|
|
1763
|
+
docker stop ouro-butler-rollback >/dev/null 2>&1 || true
|
|
1764
|
+
test "$(docker inspect --format '{{.Image}}' ouro-butler-rollback)" = "$ROLLBACK_IMAGE_ID"
|
|
1765
|
+
rollback_sanctuary_bundle_if_pending "$IMAGE_ID"
|
|
1766
|
+
docker rename ouro-butler-rollback ouro-butler
|
|
1767
|
+
assert_update_source "$ROLLBACK_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID"
|
|
1768
|
+
docker start ouro-butler
|
|
1769
|
+
assert_only_running_butler ouro-butler
|
|
1770
|
+
wait_butler_ready ouro-butler
|
|
1771
|
+
enable_butler_autostart
|
|
1772
|
+
migrate_sanctuary_package_managed_bundle "$IMAGE_ID" commit
|
|
1773
|
+
fi
|
|
1774
|
+
(exit "$PRODUCTION_PREPARATION_STATUS")
|
|
1775
|
+
fi
|
|
1776
|
+
Preparation failure therefore either restores the still-named exact production
|
|
1777
|
+
after removing any stale rollback, or renames the exact stopped rollback back;
|
|
1778
|
+
both recoveries revalidate, start, bounded-wait, atomically restore production-only
|
|
1779
|
+
autostart, and propagate the original failure. If neither exact container can
|
|
1780
|
+
be found, the failure propagates with Butler autostart disabled.
|
|
1781
|
+
Put the entire post-rename staging phase in one explicit conditional so
|
|
1782
|
+
`set -eu` cannot bypass rollback at the create, effective-audit, start, or
|
|
1783
|
+
bounded-readiness boundary. Staging uses the exact image ID with no command,
|
|
1784
|
+
environment, port, device, capability, privilege, or extra mount override.
|
|
1785
|
+
A passing staging container is stopped and removed inside the same condition,
|
|
1786
|
+
completing the poller handoff before production creation:
|
|
1787
|
+
if docker create --name ouro-butler-staging --network host --restart unless-stopped --user 10001:10001 \
|
|
1788
|
+
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli,dst=/home/ouro/.ouro-cli" \
|
|
1789
|
+
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
1790
|
+
--mount "type=bind,src=/boot/config/custom/ouro-events/spool,dst=/run/ouro-events,readonly" \
|
|
1791
|
+
"$IMAGE_ID" \
|
|
1792
|
+
&& audit_effective ouro-butler-staging "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" \
|
|
1793
|
+
&& docker start ouro-butler-staging \
|
|
1794
|
+
&& assert_only_running_butler ouro-butler-staging \
|
|
1795
|
+
&& wait_butler_ready ouro-butler-staging \
|
|
1796
|
+
&& docker stop ouro-butler-staging \
|
|
1797
|
+
&& docker rm ouro-butler-staging \
|
|
1798
|
+
&& assert_only_running_butler -; then
|
|
1799
|
+
:
|
|
1800
|
+
else
|
|
1801
|
+
STAGING_ACTIVATION_STATUS=$?
|
|
1802
|
+
if docker container inspect ouro-butler-staging >/dev/null 2>&1; then
|
|
1803
|
+
docker stop ouro-butler-staging >/dev/null 2>&1 || true
|
|
1804
|
+
PARTIAL_STAGING_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-staging)
|
|
1805
|
+
test "$PARTIAL_STAGING_IMAGE_ID" = "$IMAGE_ID"
|
|
1806
|
+
docker rm --force ouro-butler-staging >/dev/null 2>&1 || true
|
|
1807
|
+
fi
|
|
1808
|
+
! docker container inspect ouro-butler-staging >/dev/null 2>&1
|
|
1809
|
+
CURRENT_ROLLBACK_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-rollback)
|
|
1810
|
+
test "$CURRENT_ROLLBACK_IMAGE_ID" = "$ROLLBACK_IMAGE_ID"
|
|
1811
|
+
migrate_sanctuary_package_managed_bundle "$IMAGE_ID" rollback
|
|
1812
|
+
docker rename ouro-butler-rollback ouro-butler
|
|
1813
|
+
assert_update_source "$ROLLBACK_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID"
|
|
1814
|
+
docker start ouro-butler
|
|
1815
|
+
assert_only_running_butler ouro-butler
|
|
1816
|
+
wait_butler_ready ouro-butler
|
|
1817
|
+
enable_butler_autostart
|
|
1818
|
+
migrate_sanctuary_package_managed_bundle "$IMAGE_ID" commit
|
|
1819
|
+
(exit "$STAGING_ACTIVATION_STATUS")
|
|
1820
|
+
fi
|
|
1821
|
+
The failure arm safely handles staging that was never created, remains
|
|
1822
|
+
stopped, is running, or exited: it force-removes any partial staging state,
|
|
1823
|
+
verifies the name is absent, restores and revalidates the old production against
|
|
1824
|
+
its exact pre-recorded image ID, starts and bounded-waits it, atomically
|
|
1825
|
+
restores production-only autostart, and propagates the original failure.
|
|
1826
|
+
At no point may production and staging run together against the same Telegram token.
|
|
1827
|
+
Create and activate production from the same exact image ID and exact authority
|
|
1828
|
+
in one explicit conditional so `set -eu` cannot exit before rollback. Only a
|
|
1829
|
+
successful create, effective audit, start, stopped rollback assertion, and
|
|
1830
|
+
bounded readiness wait may enable production autostart. Any failure captures
|
|
1831
|
+
the activation status, safely removes a partially created new production if it
|
|
1832
|
+
exists, restores the stopped rollback container, audits it against its exact
|
|
1833
|
+
old image ID, proves it ready, atomically enables only production autostart,
|
|
1834
|
+
and then propagates the original activation failure:
|
|
1835
|
+
if docker create --name ouro-butler --network host --restart unless-stopped --user 10001:10001 \
|
|
1836
|
+
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli,dst=/home/ouro/.ouro-cli" \
|
|
1837
|
+
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
1838
|
+
--mount "type=bind,src=/boot/config/custom/ouro-events/spool,dst=/run/ouro-events,readonly" \
|
|
1839
|
+
"$IMAGE_ID" \
|
|
1840
|
+
&& audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" \
|
|
1841
|
+
&& docker start ouro-butler \
|
|
1842
|
+
&& assert_only_running_butler ouro-butler \
|
|
1843
|
+
&& test "$(docker inspect --format '{{.State.Running}}' ouro-butler-rollback)" = false \
|
|
1844
|
+
&& wait_butler_ready ouro-butler \
|
|
1845
|
+
&& enable_butler_autostart; then
|
|
1846
|
+
:
|
|
1847
|
+
else
|
|
1848
|
+
PRODUCTION_ACTIVATION_STATUS=$?
|
|
1849
|
+
if docker container inspect ouro-butler >/dev/null 2>&1; then
|
|
1850
|
+
docker stop ouro-butler >/dev/null 2>&1 || true
|
|
1851
|
+
PARTIAL_PRODUCTION_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler)
|
|
1852
|
+
test "$PARTIAL_PRODUCTION_IMAGE_ID" = "$IMAGE_ID"
|
|
1853
|
+
docker rm --force ouro-butler >/dev/null 2>&1 || true
|
|
1854
|
+
fi
|
|
1855
|
+
! docker container inspect ouro-butler >/dev/null 2>&1
|
|
1856
|
+
CURRENT_ROLLBACK_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-rollback)
|
|
1857
|
+
test "$CURRENT_ROLLBACK_IMAGE_ID" = "$ROLLBACK_IMAGE_ID"
|
|
1858
|
+
migrate_sanctuary_package_managed_bundle "$IMAGE_ID" rollback
|
|
1859
|
+
docker rename ouro-butler-rollback ouro-butler
|
|
1860
|
+
assert_update_source "$ROLLBACK_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID"
|
|
1861
|
+
docker start ouro-butler
|
|
1862
|
+
assert_only_running_butler ouro-butler
|
|
1863
|
+
wait_butler_ready ouro-butler
|
|
1864
|
+
enable_butler_autostart
|
|
1865
|
+
migrate_sanctuary_package_managed_bundle "$IMAGE_ID" commit
|
|
1866
|
+
(exit "$PRODUCTION_ACTIVATION_STATUS")
|
|
1867
|
+
fi
|
|
1868
|
+
migrate_sanctuary_package_managed_bundle "$IMAGE_ID" commit
|
|
1869
|
+
Keep ouro-butler-rollback stopped until the new production container is proven
|
|
1870
|
+
or the explicit rollback arm restores it.
|
|
1871
|
+
Never create a container from the mutable lookup tag.
|
|
1872
|
+
|
|
1873
|
+
Backup:
|
|
1874
|
+
Set BACKUP_ROOT to a new absolute snapshot path on the destination filesystem.
|
|
1875
|
+
Set AUDIT_RUNNER_IMAGE_TAG to the reviewed new image containing the
|
|
1876
|
+
legacy-aware auditor; the alpha.742 source image cannot be the runner.
|
|
1877
|
+
Capture and validate the exact source image before stopping ouro-butler, then
|
|
1878
|
+
build a temporary snapshot with root-only provenance and atomically rename it into place:
|
|
1879
|
+
test -n "${BACKUP_ROOT-}"
|
|
1880
|
+
case "$BACKUP_ROOT" in /*) ;; *) exit 1 ;; esac
|
|
1881
|
+
test "$BACKUP_ROOT" != /
|
|
1882
|
+
test ! -e "$BACKUP_ROOT"
|
|
1883
|
+
BACKUP_PARENT=$(dirname -- "$BACKUP_ROOT")
|
|
1884
|
+
test "$(cd -- "$BACKUP_PARENT" && pwd -P)" = "$BACKUP_PARENT"
|
|
1885
|
+
BACKUP_TMP=$BACKUP_ROOT.tmp.$$
|
|
1886
|
+
test ! -e "$BACKUP_TMP"
|
|
1887
|
+
AUDIT_RUNNER_IMAGE_TAG=ouro-butler:<new-version>
|
|
1888
|
+
AUDIT_RUNNER_IMAGE_ID=$(docker image inspect --format '{{.Id}}' "$AUDIT_RUNNER_IMAGE_TAG")
|
|
1889
|
+
validate_exact_image_id "$AUDIT_RUNNER_IMAGE_ID"
|
|
1890
|
+
test "$AUDIT_RUNNER_IMAGE_ID" != sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d
|
|
1891
|
+
BACKUP_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler)
|
|
1892
|
+
validate_exact_image_id "$BACKUP_IMAGE_ID"
|
|
1893
|
+
assert_update_source "$BACKUP_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID"
|
|
1894
|
+
assert_only_running_butler ouro-butler
|
|
1895
|
+
wait_butler_ready ouro-butler
|
|
1896
|
+
BACKUP_AUTOSTART_COUNTS=$(butler_autostart_counts)
|
|
1897
|
+
case "$BACKUP_AUTOSTART_COUNTS" in "0 0 0 0"|"1 0 0 0") ;; *) exit 1 ;; esac
|
|
1898
|
+
backup_host_file() {
|
|
1899
|
+
BACKUP_HOST_SOURCE=$1
|
|
1900
|
+
BACKUP_HOST_RELATIVE=$2
|
|
1901
|
+
case "$BACKUP_HOST_RELATIVE" in
|
|
1902
|
+
custom/usenet_health.sh|custom/ouro-events/bootstrap-spool.sh|custom/ouro-events/emit-event.mjs|custom/ouro-events/emit-usenet-event.sh|custom/ouro-events/install-usenet-guard.sh) ;;
|
|
1903
|
+
*) return 1 ;;
|
|
1904
|
+
esac
|
|
1905
|
+
if test -e "$BACKUP_HOST_SOURCE" || test -L "$BACKUP_HOST_SOURCE"; then
|
|
1906
|
+
test -f "$BACKUP_HOST_SOURCE" && test ! -L "$BACKUP_HOST_SOURCE" || return 1
|
|
1907
|
+
! host_file_contains_inline_credential "$BACKUP_HOST_SOURCE" || return 1
|
|
1908
|
+
install -D -m 0600 -o 0 -g 0 "$BACKUP_HOST_SOURCE" "$BACKUP_TMP/host/$BACKUP_HOST_RELATIVE" || return $?
|
|
1909
|
+
printf 'present\t%s\n' "$BACKUP_HOST_RELATIVE" >>"$BACKUP_TMP/host/inventory" || return $?
|
|
1910
|
+
else
|
|
1911
|
+
printf 'absent\t%s\n' "$BACKUP_HOST_RELATIVE" >>"$BACKUP_TMP/host/inventory" || return $?
|
|
1912
|
+
fi
|
|
1913
|
+
}
|
|
1914
|
+
host_file_contains_inline_credential() {
|
|
1915
|
+
LC_ALL=C awk '
|
|
1916
|
+
BEGIN { IGNORECASE = 1; found = 0 }
|
|
1917
|
+
/[0-9]{6,}:[A-Za-z0-9_-]{20,}/ { found = 1 }
|
|
1918
|
+
/Authorization:[[:space:]]*(Bearer|Basic)[[:space:]]+["\047]?[A-Za-z0-9][A-Za-z0-9._~+\/=:-]{11,}/ { found = 1 }
|
|
1919
|
+
/--(token|api-key|api_key|secret|password)(=|[[:space:]]+)["\047]?[A-Za-z0-9][A-Za-z0-9._~+\/=:-]{11,}/ { found = 1 }
|
|
1920
|
+
/[A-Za-z][A-Za-z0-9+.-]*:\/\/[^\/@[:space:]]+:[^\/@[:space:]]+@/ { found = 1 }
|
|
1921
|
+
/-----BEGIN ([A-Z0-9]+ )*PRIVATE KEY-----/ { found = 1 }
|
|
1922
|
+
/(token|secret|password|pass|api[_-]?key)[A-Za-z0-9_-]*["\047[:space:]]*[:=][[:space:]]*["\047]?[A-Za-z0-9][A-Za-z0-9._~+\/=:-]{11,}/ { found = 1 }
|
|
1923
|
+
END { exit found ? 0 : 1 }
|
|
1924
|
+
' "$1"
|
|
1925
|
+
}
|
|
1926
|
+
snapshot_butler_go_fragments() {
|
|
1927
|
+
BACKUP_GO_SOURCE=$1
|
|
1928
|
+
BACKUP_GO_TARGET=$2
|
|
1929
|
+
: >"$BACKUP_GO_TARGET" || return $?
|
|
1930
|
+
while IFS= read -r BACKUP_GO_LINE || test -n "$BACKUP_GO_LINE"; do
|
|
1931
|
+
case "$BACKUP_GO_LINE" in
|
|
1932
|
+
"/boot/config/custom/ouro-events/bootstrap-spool.sh --mount"|"/bin/bash /boot/config/custom/ouro-events/bootstrap-spool.sh --mount"|"/bin/bash /boot/config/custom/ouro-events/install-usenet-guard.sh --boot"|"/bin/bash /boot/config/custom/ouro-events/install-usenet-guard.sh --boot --install-root /boot/config/custom")
|
|
1933
|
+
printf '%s\n' "$BACKUP_GO_LINE" >>"$BACKUP_GO_TARGET" || return $?
|
|
1934
|
+
;;
|
|
1935
|
+
esac
|
|
1936
|
+
done <"$BACKUP_GO_SOURCE"
|
|
1937
|
+
}
|
|
1938
|
+
snapshot_butler_cron_fragments() {
|
|
1939
|
+
BACKUP_CRON_INPUT=$1
|
|
1940
|
+
BACKUP_CRON_TARGET=$2
|
|
1941
|
+
: >"$BACKUP_CRON_TARGET" || return $?
|
|
1942
|
+
while IFS= read -r BACKUP_CRON_LINE || test -n "$BACKUP_CRON_LINE"; do
|
|
1943
|
+
case "$BACKUP_CRON_LINE" in
|
|
1944
|
+
"*/15 * * * * /bin/bash /boot/config/custom/usenet_health.sh"|"*/15 * * * * /bin/bash /boot/config/custom/usenet_health.sh # ouro:usenet-health")
|
|
1945
|
+
printf '%s\n' "$BACKUP_CRON_LINE" >>"$BACKUP_CRON_TARGET" || return $?
|
|
1946
|
+
;;
|
|
1947
|
+
esac
|
|
1948
|
+
done <"$BACKUP_CRON_INPUT"
|
|
1949
|
+
}
|
|
1950
|
+
create_private_cron_capture() {
|
|
1951
|
+
BACKUP_CRON_ROOT=$(mktemp -d /tmp/ouro-backup-cron.XXXXXX) || return $?
|
|
1952
|
+
BACKUP_CRON_SOURCE=$BACKUP_CRON_ROOT/stdout
|
|
1953
|
+
BACKUP_CRON_ERROR=$BACKUP_CRON_ROOT/stderr
|
|
1954
|
+
test -d "$BACKUP_CRON_ROOT" && test ! -L "$BACKUP_CRON_ROOT" || return 1
|
|
1955
|
+
test "$(stat -c '%u:%a' "$BACKUP_CRON_ROOT" 2>/dev/null || stat -f '%u:%Lp' "$BACKUP_CRON_ROOT")" = "$(id -u):700" || return 1
|
|
1956
|
+
install -m 0600 /dev/null "$BACKUP_CRON_SOURCE" || return $?
|
|
1957
|
+
install -m 0600 /dev/null "$BACKUP_CRON_ERROR" || return $?
|
|
1958
|
+
test -f "$BACKUP_CRON_SOURCE" && test ! -L "$BACKUP_CRON_SOURCE" || return 1
|
|
1959
|
+
test -f "$BACKUP_CRON_ERROR" && test ! -L "$BACKUP_CRON_ERROR" || return 1
|
|
1960
|
+
test "$(stat -c '%u:%a' "$BACKUP_CRON_SOURCE" 2>/dev/null || stat -f '%u:%Lp' "$BACKUP_CRON_SOURCE")" = "$(id -u):600" || return 1
|
|
1961
|
+
test "$(stat -c '%u:%a' "$BACKUP_CRON_ERROR" 2>/dev/null || stat -f '%u:%Lp' "$BACKUP_CRON_ERROR")" = "$(id -u):600" || return 1
|
|
1962
|
+
}
|
|
1963
|
+
cleanup_private_cron_capture() {
|
|
1964
|
+
rm -f -- "$BACKUP_CRON_SOURCE" "$BACKUP_CRON_ERROR" || return $?
|
|
1965
|
+
rmdir -- "$BACKUP_CRON_ROOT"
|
|
1966
|
+
}
|
|
1967
|
+
snapshot_butler_host_fragments() {
|
|
1968
|
+
create_private_cron_capture || return $?
|
|
1969
|
+
trap 'cleanup_private_cron_capture' RETURN
|
|
1970
|
+
if test -f /boot/config/go && test ! -L /boot/config/go; then
|
|
1971
|
+
BACKUP_GO_STATE=present
|
|
1972
|
+
BACKUP_GO_DIGEST=$(sha256sum /boot/config/go | awk '{print $1}') || return $?
|
|
1973
|
+
snapshot_butler_go_fragments /boot/config/go "$BACKUP_TMP/host/go.butler-lines" || return $?
|
|
1974
|
+
elif test -e /boot/config/go || test -L /boot/config/go; then
|
|
1975
|
+
return 1
|
|
1976
|
+
else
|
|
1977
|
+
BACKUP_GO_STATE=absent
|
|
1978
|
+
BACKUP_GO_DIGEST=-
|
|
1979
|
+
: >"$BACKUP_TMP/host/go.butler-lines" || return $?
|
|
1980
|
+
fi
|
|
1981
|
+
BACKUP_CRON_USER=$(id -un) || return $?
|
|
1982
|
+
if crontab -l >"$BACKUP_CRON_SOURCE" 2>"$BACKUP_CRON_ERROR"; then
|
|
1983
|
+
BACKUP_CRON_STATE=present
|
|
1984
|
+
BACKUP_CRON_DIGEST=$(sha256sum "$BACKUP_CRON_SOURCE" | awk '{print $1}') || return $?
|
|
1985
|
+
else
|
|
1986
|
+
BACKUP_CRON_STATUS=$?
|
|
1987
|
+
if test "$BACKUP_CRON_STATUS" -eq 1 && grep -Fxq "no crontab for $BACKUP_CRON_USER" "$BACKUP_CRON_ERROR"; then
|
|
1988
|
+
BACKUP_CRON_STATE=absent
|
|
1989
|
+
BACKUP_CRON_DIGEST=-
|
|
1990
|
+
: >"$BACKUP_CRON_SOURCE" || return $?
|
|
1991
|
+
else
|
|
1992
|
+
return "$BACKUP_CRON_STATUS"
|
|
1993
|
+
fi
|
|
1994
|
+
fi
|
|
1995
|
+
rm -f -- "$BACKUP_CRON_ERROR" || return $?
|
|
1996
|
+
snapshot_butler_cron_fragments "$BACKUP_CRON_SOURCE" "$BACKUP_TMP/host/crontab.butler-lines" || return $?
|
|
1997
|
+
BACKUP_GO_COUNT=$(awk 'END { print NR + 0 }' "$BACKUP_TMP/host/go.butler-lines") || return $?
|
|
1998
|
+
BACKUP_CRON_COUNT=$(awk 'END { print NR + 0 }' "$BACKUP_TMP/host/crontab.butler-lines") || return $?
|
|
1999
|
+
printf 'go\t%s\t%s\t%s\ncrontab\t%s\t%s\t%s\n' "$BACKUP_GO_STATE" "$BACKUP_GO_DIGEST" "$BACKUP_GO_COUNT" "$BACKUP_CRON_STATE" "$BACKUP_CRON_DIGEST" "$BACKUP_CRON_COUNT" >"$BACKUP_TMP/host/global-state" || return $?
|
|
2000
|
+
chown 0:0 "$BACKUP_TMP/host/go.butler-lines" "$BACKUP_TMP/host/crontab.butler-lines" "$BACKUP_TMP/host/global-state" || return $?
|
|
2001
|
+
chmod 0600 "$BACKUP_TMP/host/go.butler-lines" "$BACKUP_TMP/host/crontab.butler-lines" "$BACKUP_TMP/host/global-state" || return $?
|
|
2002
|
+
printf 'present\t%s\n' go.butler-lines crontab.butler-lines global-state >>"$BACKUP_TMP/host/inventory" || return $?
|
|
2003
|
+
cleanup_private_cron_capture || return $?
|
|
2004
|
+
trap - RETURN
|
|
2005
|
+
}
|
|
2006
|
+
Snapshot both of these directories together:
|
|
2007
|
+
/mnt/user/appdata/ouro-butler/runtime/.ouro-cli
|
|
2008
|
+
/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro
|
|
2009
|
+
A routine backup must not contain container-credentials.json or
|
|
2010
|
+
container-credentials.json.consuming. If either exists, credential migration
|
|
2011
|
+
is pending or failed and must be reconciled before taking the backup.
|
|
2012
|
+
The Telegram control socket is live process state, not durable data. Exclude
|
|
2013
|
+
exactly that socket while preserving every regular file byte-for-byte, then
|
|
2014
|
+
require it to be absent from the stopped backup:
|
|
2015
|
+
BACKUP_OPERATION_STATUS=0
|
|
2016
|
+
if docker stop ouro-butler \
|
|
2017
|
+
&& test "$(docker inspect --format '{{.State.Running}}' ouro-butler)" = false \
|
|
2018
|
+
&& install -d -m 0700 -o 0 -g 0 "$BACKUP_TMP" "$BACKUP_TMP/runtime" "$BACKUP_TMP/agent" "$BACKUP_TMP/host" "$BACKUP_TMP/provenance" \
|
|
2019
|
+
&& install -d -m 0700 -o 0 -g 0 "$BACKUP_TMP/host/custom" "$BACKUP_TMP/host/custom/ouro-events" \
|
|
2020
|
+
&& install -m 0600 -o 0 -g 0 /dev/null "$BACKUP_TMP/host/inventory" \
|
|
2021
|
+
&& backup_host_file /boot/config/custom/usenet_health.sh custom/usenet_health.sh \
|
|
2022
|
+
&& backup_host_file /boot/config/custom/ouro-events/bootstrap-spool.sh custom/ouro-events/bootstrap-spool.sh \
|
|
2023
|
+
&& backup_host_file /boot/config/custom/ouro-events/emit-event.mjs custom/ouro-events/emit-event.mjs \
|
|
2024
|
+
&& backup_host_file /boot/config/custom/ouro-events/emit-usenet-event.sh custom/ouro-events/emit-usenet-event.sh \
|
|
2025
|
+
&& backup_host_file /boot/config/custom/ouro-events/install-usenet-guard.sh custom/ouro-events/install-usenet-guard.sh \
|
|
2026
|
+
&& snapshot_butler_host_fragments \
|
|
2027
|
+
&& test ! -e "$BACKUP_TMP/host/notify.conf" \
|
|
2028
|
+
&& test ! -e "$BACKUP_TMP/host/sabnzbd.ini" \
|
|
2029
|
+
&& rsync -a --exclude='/state/acceptance/telegram-control.sock' /mnt/user/appdata/ouro-butler/runtime/.ouro-cli "$BACKUP_TMP/runtime/" \
|
|
2030
|
+
&& rsync -a --exclude='/state/acceptance/telegram-control.sock' /mnt/user/appdata/ouro-butler/agent/sanctuary.ouro "$BACKUP_TMP/agent/" \
|
|
2031
|
+
&& test ! -S "$BACKUP_TMP/agent/sanctuary.ouro/state/acceptance/telegram-control.sock" \
|
|
2032
|
+
&& validate_sanctuary_roots "$BACKUP_TMP/runtime/.ouro-cli" "$BACKUP_TMP/agent/sanctuary.ouro" \
|
|
2033
|
+
&& docker container inspect ouro-butler >"$BACKUP_TMP/provenance/container-inspect.json" \
|
|
2034
|
+
&& docker image inspect "$BACKUP_IMAGE_ID" >"$BACKUP_TMP/provenance/image-inspect.json" \
|
|
2035
|
+
&& docker run --rm --pull=never --network=none --read-only --entrypoint /usr/local/bin/node "$BACKUP_IMAGE_ID" -p 'require("/opt/ouro/package.json").version' >"$BACKUP_TMP/provenance/package-version" \
|
|
2036
|
+
&& printf '%s\n' "$BACKUP_IMAGE_ID" >"$BACKUP_TMP/provenance/image-id" \
|
|
2037
|
+
&& chown 0:0 "$BACKUP_TMP/provenance/container-inspect.json" "$BACKUP_TMP/provenance/image-inspect.json" "$BACKUP_TMP/provenance/package-version" "$BACKUP_TMP/provenance/image-id" \
|
|
2038
|
+
&& chmod 0600 "$BACKUP_TMP/provenance/container-inspect.json" "$BACKUP_TMP/provenance/image-inspect.json" "$BACKUP_TMP/provenance/package-version" "$BACKUP_TMP/provenance/image-id" \
|
|
2039
|
+
&& (cd "$BACKUP_TMP" && find runtime agent host provenance -xdev -type f ! -path provenance/manifest.sha256 -exec sha256sum -- {} + | LC_ALL=C sort -k2 >provenance/manifest.sha256) \
|
|
2040
|
+
&& chown 0:0 "$BACKUP_TMP/provenance/manifest.sha256" \
|
|
2041
|
+
&& chmod 0600 "$BACKUP_TMP/provenance/manifest.sha256" \
|
|
2042
|
+
&& verify_sanctuary_snapshot_provenance "$BACKUP_TMP" "$BACKUP_IMAGE_ID" \
|
|
2043
|
+
&& sync -f "$BACKUP_TMP/provenance/manifest.sha256" \
|
|
2044
|
+
&& mv -- "$BACKUP_TMP" "$BACKUP_ROOT" \
|
|
2045
|
+
&& sync -f "$BACKUP_PARENT"; then
|
|
2046
|
+
:
|
|
2047
|
+
else
|
|
2048
|
+
BACKUP_OPERATION_STATUS=$?
|
|
2049
|
+
fi
|
|
2050
|
+
if assert_update_source "$BACKUP_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" \
|
|
2051
|
+
&& docker start ouro-butler \
|
|
2052
|
+
&& assert_only_running_butler ouro-butler \
|
|
2053
|
+
&& wait_butler_ready ouro-butler \
|
|
2054
|
+
&& verify_butler_autostart "$BACKUP_AUTOSTART_COUNTS"; then
|
|
2055
|
+
:
|
|
2056
|
+
else
|
|
2057
|
+
BACKUP_RECOVERY_STATUS=$?
|
|
2058
|
+
if test -d "$BACKUP_ROOT"; then
|
|
2059
|
+
printf '%s\n' "CRITICAL: production recovery failed after backup; completed snapshot remains intact at $BACKUP_ROOT" >&2
|
|
2060
|
+
else
|
|
2061
|
+
printf '%s\n' "CRITICAL: production recovery failed and no completed snapshot was published; inspect $BACKUP_TMP" >&2
|
|
2062
|
+
fi
|
|
2063
|
+
(exit "$BACKUP_RECOVERY_STATUS")
|
|
2064
|
+
fi
|
|
2065
|
+
if test "$BACKUP_OPERATION_STATUS" -ne 0; then
|
|
2066
|
+
if test -d "$BACKUP_ROOT"; then
|
|
2067
|
+
printf '%s\n' "Backup publication was not durably confirmed; production was recovered; completed snapshot remains intact at $BACKUP_ROOT" >&2
|
|
2068
|
+
else
|
|
2069
|
+
printf '%s\n' "Backup failed before atomic publication; production was recovered; inspect $BACKUP_TMP" >&2
|
|
2070
|
+
fi
|
|
2071
|
+
(exit "$BACKUP_OPERATION_STATUS")
|
|
2072
|
+
fi
|
|
2073
|
+
|
|
2074
|
+
Restore:
|
|
2075
|
+
Set BACKUP_ROOT to the exact verified snapshot containing `runtime/.ouro-cli`
|
|
2076
|
+
and `agent/sanctuary.ouro`, set IMAGE_ID to its recorded local image ID, and
|
|
2077
|
+
set AUDIT_RUNNER_IMAGE_ID to the exact reviewed new image containing the
|
|
2078
|
+
legacy-aware auditor. The runner is independent from the restored image.
|
|
2079
|
+
AUDIT_RUNNER_IMAGE_TAG=ouro-butler:<new-version>
|
|
2080
|
+
AUDIT_RUNNER_IMAGE_ID=$(docker image inspect --format '{{.Id}}' "$AUDIT_RUNNER_IMAGE_TAG")
|
|
2081
|
+
Before any autostart, root, or container mutation, run the nounset-safe input,
|
|
2082
|
+
backup-root, image, and topology preflight. It requires a nonempty canonical
|
|
2083
|
+
absolute BACKUP_ROOT (not /), both exact required directories, an exact local
|
|
2084
|
+
sha256 image ID, canonical production as the only running Butler poller, no
|
|
2085
|
+
staging or rollback, and at most one exact stopped legacy-evidence container.
|
|
2086
|
+
It also executes the reviewed runner against the live source container before
|
|
2087
|
+
any autostart, root, or container mutation:
|
|
2088
|
+
if assert_restore_preflight; then
|
|
2089
|
+
:
|
|
2090
|
+
else
|
|
2091
|
+
RESTORE_PREFLIGHT_STATUS=$?
|
|
2092
|
+
(exit "$RESTORE_PREFLIGHT_STATUS")
|
|
2093
|
+
fi
|
|
2094
|
+
The verified host snapshot is restored only by this explicit Restore path.
|
|
2095
|
+
Ordinary update failure continues to use the installer's short-lived
|
|
2096
|
+
transaction rollback. Extract the installer from the reviewed runner image,
|
|
2097
|
+
then let that fixed allowlist transaction restore exact presence/absence for
|
|
2098
|
+
the five guard assets and reapply only the captured Butler-owned go/crontab
|
|
2099
|
+
fragments. Unrelated current go/crontab lines are never stored in the durable
|
|
2100
|
+
snapshot and remain untouched by restore. The transaction deactivates the
|
|
2101
|
+
current owned cron before swapping files and restores the entire pre-restore
|
|
2102
|
+
host state if any swap or final crontab activation fails:
|
|
2103
|
+
HOST_RESTORE_INSTALLER=$(mktemp /tmp/ouro-usenet-host-restore.XXXXXX)
|
|
2104
|
+
docker run --rm --pull=never --network=none --entrypoint /bin/cat "$AUDIT_RUNNER_IMAGE_ID" /opt/ouro/deploy/unraid/ouro-events/install-usenet-guard.sh >"$HOST_RESTORE_INSTALLER"
|
|
2105
|
+
chmod 0500 "$HOST_RESTORE_INSTALLER"
|
|
2106
|
+
Guard the atomic autostart disable before stopping or removing any Butler
|
|
2107
|
+
container. Failure propagates while the existing production remains untouched:
|
|
2108
|
+
/bin/bash /boot/config/custom/ouro-events/bootstrap-spool.sh --mount
|
|
2109
|
+
test "$(findmnt -n -o FSTYPE --target /boot/config/custom/ouro-events/spool)" = tmpfs
|
|
2110
|
+
test "$(stat -c '%u:%g:%a' /boot/config/custom/ouro-events/spool)" = 0:0:755
|
|
2111
|
+
if disable_butler_autostart; then
|
|
2112
|
+
:
|
|
2113
|
+
else
|
|
2114
|
+
RESTORE_AUTOSTART_DISABLE_STATUS=$?
|
|
2115
|
+
(exit "$RESTORE_AUTOSTART_DISABLE_STATUS")
|
|
2116
|
+
fi
|
|
2117
|
+
Stop and remove only ouro-butler, restore both roots without stale files,
|
|
2118
|
+
restore exact ownership, validate the image, and create/audit/start/bounded-wait
|
|
2119
|
+
the restored production inside one explicit condition. The initial tolerant
|
|
2120
|
+
stop plus inspected force-removal safely handles an already absent or stopped
|
|
2121
|
+
container:
|
|
2122
|
+
if { docker stop ouro-butler >/dev/null 2>&1 || true; } \
|
|
2123
|
+
&& {
|
|
2124
|
+
if docker container inspect ouro-butler >/dev/null 2>&1; then
|
|
2125
|
+
docker rm --force ouro-butler
|
|
2126
|
+
else
|
|
2127
|
+
:
|
|
2128
|
+
fi
|
|
2129
|
+
} \
|
|
2130
|
+
&& ! docker container inspect ouro-butler >/dev/null 2>&1 \
|
|
2131
|
+
&& rsync -a --delete "$BACKUP_ROOT/runtime/.ouro-cli/" /mnt/user/appdata/ouro-butler/runtime/.ouro-cli/ \
|
|
2132
|
+
&& rsync -a --delete "$BACKUP_ROOT/agent/sanctuary.ouro/" /mnt/user/appdata/ouro-butler/agent/sanctuary.ouro/ \
|
|
2133
|
+
&& chown -R 10001:10001 /mnt/user/appdata/ouro-butler/runtime/.ouro-cli /mnt/user/appdata/ouro-butler/agent/sanctuary.ouro \
|
|
2134
|
+
&& validate_sanctuary_roots /mnt/user/appdata/ouro-butler/runtime/.ouro-cli /mnt/user/appdata/ouro-butler/agent/sanctuary.ouro \
|
|
2135
|
+
&& /bin/bash "$HOST_RESTORE_INSTALLER" --restore-root "$BACKUP_ROOT/host" \
|
|
2136
|
+
&& rm -f "$HOST_RESTORE_INSTALLER" \
|
|
2137
|
+
&& docker image inspect "$IMAGE_ID" >/dev/null \
|
|
2138
|
+
&& docker create --name ouro-butler --network host --restart unless-stopped --user 10001:10001 \
|
|
2139
|
+
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli,dst=/home/ouro/.ouro-cli" \
|
|
2140
|
+
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
2141
|
+
--mount "type=bind,src=/boot/config/custom/ouro-events/spool,dst=/run/ouro-events,readonly" \
|
|
2142
|
+
"$IMAGE_ID" \
|
|
2143
|
+
&& audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" \
|
|
2144
|
+
&& docker start ouro-butler \
|
|
2145
|
+
&& assert_only_running_butler ouro-butler \
|
|
2146
|
+
&& wait_butler_ready ouro-butler \
|
|
2147
|
+
&& enable_butler_autostart; then
|
|
2148
|
+
:
|
|
2149
|
+
else
|
|
2150
|
+
RESTORE_ACTIVATION_STATUS=$?
|
|
2151
|
+
if docker container inspect ouro-butler >/dev/null 2>&1; then
|
|
2152
|
+
docker stop ouro-butler >/dev/null 2>&1 || true
|
|
2153
|
+
PARTIAL_RESTORE_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler)
|
|
2154
|
+
test "$PARTIAL_RESTORE_IMAGE_ID" = "$IMAGE_ID"
|
|
2155
|
+
docker rm --force ouro-butler >/dev/null 2>&1 || true
|
|
2156
|
+
fi
|
|
2157
|
+
! docker container inspect ouro-butler >/dev/null 2>&1
|
|
2158
|
+
rm -f "$HOST_RESTORE_INSTALLER"
|
|
2159
|
+
(exit "$RESTORE_ACTIVATION_STATUS")
|
|
2160
|
+
fi
|
|
2161
|
+
A restore has no retained old container or pre-restore data root to roll back
|
|
2162
|
+
to. Any stop/remove, data replacement, ownership, image validation, create,
|
|
2163
|
+
audit, start, or readiness failure therefore reaches the same cleanup arm,
|
|
2164
|
+
leaves all Butler autostart entries disabled, and propagates the captured
|
|
2165
|
+
failure for operator repair or a verified restore retry.
|
|
2166
|
+
|
|
2167
|
+
Credential recovery:
|
|
2168
|
+
The one canonical Telegram identity is display name `Mendelow Cloud Butler`,
|
|
2169
|
+
username `MendelowCloudButlerBot`, numeric bot ID `8541786263`. Treat any
|
|
2170
|
+
different display name, username, numeric ID, or fallback handle as a hard
|
|
2171
|
+
preflight failure; do not start polling or send a test message.
|
|
2172
|
+
Run `bootstrap_sanctuary_vault "$IMAGE_ID"` interactively before installing
|
|
2173
|
+
credentials. It uses only fresh `--rm` containers from that exact image as
|
|
2174
|
+
UID/GID 10001 with the canonical two binds; create/unlock is the sole `-it`
|
|
2175
|
+
action so the master secret remains on the hidden terminal prompt.
|
|
2176
|
+
Its first read-only `vault status` is authoritative: it runs `vault create`
|
|
2177
|
+
only when the locator is absent, runs `vault unlock` only when the locator
|
|
2178
|
+
exists but local unlock is missing, and does not prompt when local unlock is
|
|
2179
|
+
already available. It then requires the configured locator, available local
|
|
2180
|
+
unlock, and successful auth verification. Never guess from a failed status
|
|
2181
|
+
command.
|
|
2182
|
+
Initial legacy adoption passes the exact legacy envelope path as the helper's
|
|
2183
|
+
second argument. After vault availability, the helper validates and atomically
|
|
2184
|
+
copies that envelope into the canonical runtime root, imports it with a fresh
|
|
2185
|
+
same-image one-shot container, and requires both canonical source and claim to
|
|
2186
|
+
be absent before provider readiness. The exact legacy source remains untouched
|
|
2187
|
+
as rollback evidence until canonical acceptance completes.
|
|
2188
|
+
Restore or unlock the Sanctuary agent vault, then run provider refresh.
|
|
2189
|
+
/mnt/user/appdata/ouro-butler/runtime/.ouro-cli/container-credentials.json
|
|
2190
|
+
exists only for a one-time migration: if
|
|
2191
|
+
used, install it atomically as root with ownership 10001:10001 and mode 0600.
|
|
2192
|
+
Startup atomically claims it as container-credentials.json.consuming and
|
|
2193
|
+
writes every credential class into the unlocked Sanctuary vault before deleting the claimed envelope.
|
|
2194
|
+
Only then does it refresh the daemon's in-memory credentials.
|
|
2195
|
+
If validation or any vault write fails, startup fails closed and preserves
|
|
2196
|
+
container-credentials.json.consuming for the next startup to reconcile.
|
|
2197
|
+
Restore or create/unlock the configured agent vault, then restart the same
|
|
2198
|
+
reviewed image and let it retry the preserved claim. If both the source and
|
|
2199
|
+
claimed envelope exist and they are safe regular 0600 files owned by the
|
|
2200
|
+
runtime user, startup resumes automatically only when they are byte-for-byte identical:
|
|
2201
|
+
it durably removes the redundant unclaimed source and continues the existing claim.
|
|
2202
|
+
If they differ, the repair is human-required: stop the Butler, securely compare and quarantine
|
|
2203
|
+
the two envelopes, then restore exactly one reviewed envelope at the source path.
|
|
2204
|
+
Never print either envelope's contents or place them in logs or command arguments.
|
|
2205
|
+
After successful import, both files
|
|
2206
|
+
are absent and the vault is the only credential source of truth.
|
|
2207
|
+
`provision_sanctuary_sab_credential "$IMAGE_ID"` is the one-shot migration for
|
|
2208
|
+
the existing SAB key. Root reads the fixed host INI, writes only a mode-0600
|
|
2209
|
+
`ouro.runtimeCredentialBootstrap` envelope owned by 10001:10001, and a
|
|
2210
|
+
same-image one-shot container merges `sabnzbdApiKey` into
|
|
2211
|
+
`runtime/machines/sanctuary/config`; the key never appears in argv, the
|
|
2212
|
+
environment, logs, the Unraid template, or the Butler mounts. The bootstrap
|
|
2213
|
+
reconciler is idempotent for the same value and fails closed on a conflict.
|
|
2214
|
+
`verify_sanctuary_sab_readiness "$IMAGE_ID"` then reloads that machine item
|
|
2215
|
+
from the vault and performs a real bounded queue read before any poller
|
|
2216
|
+
cutover. The runtime and agent roots are persistent binds and package-managed
|
|
2217
|
+
updates do not replace the machine vault item, so the credential survives
|
|
2218
|
+
template and image updates.
|
|
2219
|
+
Bootstrap is not the rotation path. After SAB rotates its key, use the
|
|
2220
|
+
existing hidden-prompt merge command
|
|
2221
|
+
`ouro vault config set --agent sanctuary --key sabnzbdApiKey --scope machine`,
|
|
2222
|
+
restart the reviewed Butler container so its machine-config cache refreshes,
|
|
2223
|
+
and rerun `verify_sanctuary_sab_readiness "$IMAGE_ID"`; a conflicting
|
|
2224
|
+
bootstrap is expected to stop rather than overwrite canonical vault state.
|
|
2225
|
+
Never print or place credential values in logs, templates, command arguments,
|
|
2226
|
+
backups, or this runbook.
|
|
2227
|
+
|
|
2228
|
+
Packaged deployment-target containment gates:
|
|
2229
|
+
Unit 16 is fixed to the `staging` profile and therefore targets only
|
|
2230
|
+
`ouro-butler-staging`; it does not require `ouro-butler` to exist. Before the
|
|
2231
|
+
broker starts, the packaged deployment auditor captures all present canonical
|
|
2232
|
+
production/staging/rollback identities in one Docker inspect, verifies the
|
|
2233
|
+
target image, `unless-stopped`, host networking, matching GraphQL and durable
|
|
2234
|
+
file autostart identity/state for every present canonical container,
|
|
2235
|
+
and exactly one running Butler, then repeats the canonical snapshot to reject
|
|
2236
|
+
races. It binds the target PID and exact Docker cgroup-v2 path to the inspected
|
|
2237
|
+
container ID; Unit 16 carries that immutable ID into every launcher and broker
|
|
2238
|
+
Docker operation and binds it to the GraphQL PrefixedID suffix. It scans every
|
|
2239
|
+
cgroup process and requires stable before/after process, owned-socket, and
|
|
2240
|
+
listener inventories. Between the two canonical topology captures, it verifies
|
|
2241
|
+
the exact immutable-ID target is running and unpaused, pauses that ID without
|
|
2242
|
+
changing restart or autostart state, and performs every cgroup, all-thread
|
|
2243
|
+
descriptor, TCP, UDP, and Unix observation inside that one quiesced window,
|
|
2244
|
+
including two matching complete terminal snapshots; nonconvergence fails
|
|
2245
|
+
closed after eight attempts. It always makes bounded exact-ID unpause attempts,
|
|
2246
|
+
verifies the same ID and PID resumed their original running, unpaused state,
|
|
2247
|
+
and only then performs the final full-profile topology capture. It
|
|
2248
|
+
rejects every owned UDP listener and every externally reachable owned TCP listener. Only loopback
|
|
2249
|
+
Mailbox port 6876 plus stream-listening endpoints at the fixed daemon and
|
|
2250
|
+
acceptance Unix control paths are permitted; all other named Unix endpoint
|
|
2251
|
+
types (including datagrams) fail closed. Inherited descriptors for the same
|
|
2252
|
+
socket inode are deduplicated, while unknown loopback ports, wildcard/host-address
|
|
2253
|
+
listeners, process/socket/listener drift, or ambiguous ownership fail closed.
|
|
2254
|
+
Unit 18 uses the separately packaged fixed final command after activation.
|
|
2255
|
+
It accepts either the legacy-adoption topology with no canonical rollback or
|
|
2256
|
+
exactly one stopped, non-autostarted rollback; running/autostarted rollback,
|
|
2257
|
+
staging presence, and ambiguous canonical identities always fail closed:
|
|
2258
|
+
UNIT18_TARGET_TMP=$(mktemp /run/sanctuary-unit18-target-audit.sh.XXXXXX)
|
|
2259
|
+
/usr/bin/timeout -s KILL 20 /usr/bin/docker run --rm --pull=never --network none \
|
|
2260
|
+
--entrypoint /bin/cat "$IMAGE_ID" /opt/ouro/deploy/unraid/sanctuary-unit18-target-audit.sh >"$UNIT18_TARGET_TMP"
|
|
2261
|
+
chmod 0500 "$UNIT18_TARGET_TMP"
|
|
2262
|
+
"$UNIT18_TARGET_TMP" "$IMAGE_ID"
|
|
2263
|
+
rm -f -- "$UNIT18_TARGET_TMP"
|
|
2264
|
+
Neither packaged command accepts a container name or an open-ended profile.
|
|
2265
|
+
|
|
2266
|
+
Packaged Unit 16 acceptance execution:
|
|
2267
|
+
Use only the launcher packaged in the exact reviewed image. Install it without
|
|
2268
|
+
network access, then create the two private roots with the runtime UID/GID:
|
|
2269
|
+
UNIT16_ROOT=/mnt/user/appdata/ouro-butler/acceptance
|
|
2270
|
+
install -d -m 0700 -o 10001 -g 10001 "$UNIT16_ROOT/configs" "$UNIT16_ROOT/evidence"
|
|
2271
|
+
UNIT16_LAUNCHER_TMP=$(mktemp "$UNIT16_ROOT/sanctuary-unit16-run.sh.tmp.XXXXXX")
|
|
2272
|
+
/usr/bin/timeout -s KILL 20 /usr/bin/docker run --rm --pull=never --network none \
|
|
2273
|
+
--entrypoint /bin/cat "$IMAGE_ID" /opt/ouro/deploy/unraid/sanctuary-unit16-run.sh >"$UNIT16_LAUNCHER_TMP"
|
|
2274
|
+
install -m 0755 -o root -g root "$UNIT16_LAUNCHER_TMP" "$UNIT16_ROOT/sanctuary-unit16-run.sh"
|
|
2275
|
+
rm -f -- "$UNIT16_LAUNCHER_TMP"
|
|
2276
|
+
After final activation, create the disjoint final-profile roots and invoke the
|
|
2277
|
+
same immutable launcher with its only non-staging selector. The selector is a
|
|
2278
|
+
closed `staging|final` enum; it never accepts a container name, ID, or path:
|
|
2279
|
+
UNIT18_ROOT=/mnt/user/appdata/ouro-butler/acceptance/final
|
|
2280
|
+
install -d -m 0700 -o 10001 -g 10001 "$UNIT18_ROOT/configs" "$UNIT18_ROOT/evidence"
|
|
2281
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final materialize evidence-snapshot
|
|
2282
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final evidence-snapshot evidence-snapshot.json
|
|
2283
|
+
Use `--profile final` on every final pre-reboot and post-reboot materialize/run
|
|
2284
|
+
command. Omitting it remains the byte-for-byte Unit 16 staging path. Never mix
|
|
2285
|
+
staging and final config/evidence roots or reuse a config across profiles.
|
|
2286
|
+
Never hand-author a Unit 16 config. Materialize it from the exact image, then
|
|
2287
|
+
execute it in the order below. Before every execution the launcher regenerates
|
|
2288
|
+
the config from the packaged fixed contract and requires byte-for-byte equality.
|
|
2289
|
+
Unit 16d-2 stops at the pre-model quarantine boundary: use a genuinely distinct private Telegram sender, confirm the fixed acknowledgement and owner admission card, and do not approve the contact during this scenario. The production-identical allow-to-one-turn continuation is covered by the Telegram admission integration suite when a second live account is unavailable. Unit 16h is acceptance-only: it exercises the delivery path against isolated state, restores exact health and cron bytes, and does not activate a production daily digest.
|
|
2290
|
+
The cursor snapshot is deliberately materialized and executed twice around the
|
|
2291
|
+
live scenario. Telegram bootstrap reads the new bot token from host file
|
|
2292
|
+
descriptor 3; callback injection reads the reviewed saved callback-update JSON
|
|
2293
|
+
from the same descriptor. The launcher explicitly maps host fd 3 to Docker
|
|
2294
|
+
stdin and then to in-container fd 3 only for those two commands. Neither value
|
|
2295
|
+
belongs in argv, shell history, a config file, or an unrelated command's stdin.
|
|
2296
|
+
Define this Bash helper in the root shell. It disables terminal echo while
|
|
2297
|
+
reading the token, opens an anonymous descriptor for the launcher, and unsets
|
|
2298
|
+
the short-lived shell value on either launcher success or failure:
|
|
2299
|
+
run_unit16_telegram_bootstrap() {
|
|
2300
|
+
local UNIT16_BOT_TOKEN UNIT16_BOT_STATUS
|
|
2301
|
+
printf 'Telegram bot token: ' >&2
|
|
2302
|
+
IFS= read -r -s UNIT16_BOT_TOKEN || return $?
|
|
2303
|
+
printf '\n' >&2
|
|
2304
|
+
if "$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final telegram-bootstrap telegram-bootstrap.json \
|
|
2305
|
+
3< <(printf '%s\n' "$UNIT16_BOT_TOKEN"); then
|
|
2306
|
+
UNIT16_BOT_STATUS=0
|
|
2307
|
+
else
|
|
2308
|
+
UNIT16_BOT_STATUS=$?
|
|
2309
|
+
fi
|
|
2310
|
+
unset UNIT16_BOT_TOKEN
|
|
2311
|
+
return "$UNIT16_BOT_STATUS"
|
|
2312
|
+
}
|
|
2313
|
+
Stage the reviewed callback JSON at the fixed path below in the root-owned
|
|
2314
|
+
tmpfs inbox, then use this single fail-closed helper. It opens the input once,
|
|
2315
|
+
validates the opened descriptor and its original path refer to the same
|
|
2316
|
+
root-owned mode-0600 regular file, and invokes the launcher only afterward.
|
|
2317
|
+
Success overwrites, fsyncs, and unlinks the raw input. Any failure allocates a
|
|
2318
|
+
collision-free quarantine file atomically, copies and fsyncs the input into it,
|
|
2319
|
+
locks it to mode 000, then overwrites and removes the tmpfs source;
|
|
2320
|
+
no raw callback update remains loose in `/root` or the inbox.
|
|
2321
|
+
UNIT16_CALLBACK_INBOX=/run/ouro-unit16-callback-input
|
|
2322
|
+
UNIT16_CALLBACK_FILE=$UNIT16_CALLBACK_INBOX/callback-update.json
|
|
2323
|
+
UNIT16_CALLBACK_QUARANTINE_ROOT=$UNIT16_ROOT/callback-quarantine
|
|
2324
|
+
install -d -m 0700 -o root -g root "$UNIT16_CALLBACK_INBOX" "$UNIT16_CALLBACK_QUARANTINE_ROOT"
|
|
2325
|
+
run_unit16_callback_inject() {
|
|
2326
|
+
(
|
|
2327
|
+
UNIT16_CALLBACK_VALIDATED=no
|
|
2328
|
+
UNIT16_CALLBACK_STATUS=1
|
|
2329
|
+
unit16_callback_cleanup() {
|
|
2330
|
+
UNIT16_CALLBACK_STATUS=$?
|
|
2331
|
+
trap - EXIT HUP INT TERM
|
|
2332
|
+
if test -e "$UNIT16_CALLBACK_FILE" || test -L "$UNIT16_CALLBACK_FILE"; then
|
|
2333
|
+
if test "$UNIT16_CALLBACK_STATUS" -eq 0 && test "$UNIT16_CALLBACK_VALIDATED" = yes; then
|
|
2334
|
+
if ! truncate -s 0 /proc/self/fd/3 || ! sync -f /proc/self/fd/3 \
|
|
2335
|
+
|| ! rm -f -- "$UNIT16_CALLBACK_FILE" || ! sync -f "$UNIT16_CALLBACK_INBOX"; then
|
|
2336
|
+
UNIT16_CALLBACK_STATUS=1
|
|
2337
|
+
fi
|
|
2338
|
+
elif test -f "$UNIT16_CALLBACK_FILE" && test ! -L "$UNIT16_CALLBACK_FILE"; then
|
|
2339
|
+
UNIT16_CALLBACK_QUARANTINE_FILE=$(mktemp "$UNIT16_CALLBACK_QUARANTINE_ROOT/failed.XXXXXX") || UNIT16_CALLBACK_QUARANTINE_FILE=
|
|
2340
|
+
if test -n "$UNIT16_CALLBACK_QUARANTINE_FILE" \
|
|
2341
|
+
&& chmod 0600 "$UNIT16_CALLBACK_QUARANTINE_FILE" \
|
|
2342
|
+
&& cp -- "$UNIT16_CALLBACK_FILE" "$UNIT16_CALLBACK_QUARANTINE_FILE" \
|
|
2343
|
+
&& sync -f "$UNIT16_CALLBACK_QUARANTINE_FILE" \
|
|
2344
|
+
&& chmod 000 "$UNIT16_CALLBACK_QUARANTINE_FILE" \
|
|
2345
|
+
&& sync -f "$UNIT16_CALLBACK_QUARANTINE_ROOT"; then
|
|
2346
|
+
:
|
|
2347
|
+
else
|
|
2348
|
+
test -z "${UNIT16_CALLBACK_QUARANTINE_FILE-}" || rm -f -- "$UNIT16_CALLBACK_QUARANTINE_FILE"
|
|
2349
|
+
UNIT16_CALLBACK_STATUS=1
|
|
2350
|
+
fi
|
|
2351
|
+
if ! truncate -s 0 "$UNIT16_CALLBACK_FILE" || ! sync -f "$UNIT16_CALLBACK_FILE" \
|
|
2352
|
+
|| ! rm -f -- "$UNIT16_CALLBACK_FILE" || ! sync -f "$UNIT16_CALLBACK_INBOX"; then
|
|
2353
|
+
UNIT16_CALLBACK_STATUS=1
|
|
2354
|
+
fi
|
|
2355
|
+
else
|
|
2356
|
+
rm -f -- "$UNIT16_CALLBACK_FILE" || UNIT16_CALLBACK_STATUS=1
|
|
2357
|
+
sync -f "$UNIT16_CALLBACK_INBOX" || UNIT16_CALLBACK_STATUS=1
|
|
2358
|
+
fi
|
|
2359
|
+
fi
|
|
2360
|
+
exec 3<&-
|
|
2361
|
+
exit "$UNIT16_CALLBACK_STATUS"
|
|
2362
|
+
}
|
|
2363
|
+
trap unit16_callback_cleanup EXIT
|
|
2364
|
+
trap 'exit 129' HUP
|
|
2365
|
+
trap 'exit 130' INT
|
|
2366
|
+
trap 'exit 143' TERM
|
|
2367
|
+
test -d "$UNIT16_CALLBACK_INBOX" && test ! -L "$UNIT16_CALLBACK_INBOX" || exit 1
|
|
2368
|
+
test "$(stat -c '%F %u:%g %a' "$UNIT16_CALLBACK_INBOX")" = "directory 0:0 700" || exit 1
|
|
2369
|
+
test -d "$UNIT16_CALLBACK_QUARANTINE_ROOT" && test ! -L "$UNIT16_CALLBACK_QUARANTINE_ROOT" || exit 1
|
|
2370
|
+
test "$(stat -c '%F %u:%g %a' "$UNIT16_CALLBACK_QUARANTINE_ROOT")" = "directory 0:0 700" || exit 1
|
|
2371
|
+
test -f "$UNIT16_CALLBACK_FILE" && test ! -L "$UNIT16_CALLBACK_FILE" || exit 1
|
|
2372
|
+
exec 3<"$UNIT16_CALLBACK_FILE"
|
|
2373
|
+
test "$(stat -Lc '%F' /proc/self/fd/3)" = "regular file" || exit 1
|
|
2374
|
+
test "$(stat -c '%d:%i' "$UNIT16_CALLBACK_FILE")" = "$(stat -Lc '%d:%i' /proc/self/fd/3)" || exit 1
|
|
2375
|
+
UNIT16_CALLBACK_VALIDATED=yes
|
|
2376
|
+
test "$(stat -Lc '%F %u:%g %a' /proc/self/fd/3)" = "regular file 0:0 600" || exit 1
|
|
2377
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final callback-inject callback-inject.json 3<&3
|
|
2378
|
+
)
|
|
2379
|
+
}
|
|
2380
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final materialize telegram-bootstrap
|
|
2381
|
+
run_unit16_telegram_bootstrap
|
|
2382
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final materialize cursor-snapshot before
|
|
2383
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final cursor-snapshot cursor-snapshot.json
|
|
2384
|
+
# Perform the live scenario whose cursor movement is being measured.
|
|
2385
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final materialize cursor-snapshot after
|
|
2386
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final cursor-snapshot cursor-snapshot.json
|
|
2387
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final materialize cursor-delta
|
|
2388
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final cursor-delta cursor-delta.json
|
|
2389
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final materialize callback-inject
|
|
2390
|
+
run_unit16_callback_inject
|
|
2391
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final materialize unraid-key-rotate
|
|
2392
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final unraid-key-rotate unraid-key-rotate.json
|
|
2393
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final materialize reboot-request
|
|
2394
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final reboot-request reboot-request.json
|
|
2395
|
+
# The preceding command captures and seals preflight evidence before it asks
|
|
2396
|
+
# the broker to stage the reboot, then seals the requested-phase evidence and
|
|
2397
|
+
# fsyncs the requested checkpoint before invoking the host reboot. Reconnect
|
|
2398
|
+
# only after Unraid and Docker are ready.
|
|
2399
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final materialize reboot-resume
|
|
2400
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final reboot-resume reboot-resume.json
|
|
2401
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final materialize evidence-snapshot
|
|
2402
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final evidence-snapshot evidence-snapshot.json
|
|
2403
|
+
# reboot-resume seals the changed boot identity and recovery milestones.
|
|
2404
|
+
# evidence-snapshot then runs the remaining scenarios sequentially with a
|
|
2405
|
+
# 72-minute aggregate bound; each completed scenario records provenance at
|
|
2406
|
+
# capture time and always clears its public gate, private marker, and receipt.
|
|
2407
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final materialize evidence-bundle-index
|
|
2408
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final evidence-bundle-index evidence-bundle-index.json
|
|
2409
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final materialize evidence-bundle-verify
|
|
2410
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final evidence-bundle-verify evidence-bundle-verify.json
|
|
2411
|
+
Each execution revalidates the immutable image and the production container's
|
|
2412
|
+
exact image ID. The main one-shot gets a read-only root filesystem, one writable
|
|
2413
|
+
evidence mount, narrowly selected runtime/bundle modes, and individual read-only
|
|
2414
|
+
image/container/health/boot fact files. Host-only operations cross an ephemeral
|
|
2415
|
+
root-owned private Unix socket (root:10001 mode 0660) to a root broker extracted
|
|
2416
|
+
from the same exact image. The broker accepts only fixed Unraid inventory/create/
|
|
2417
|
+
exact-revoke/rejection operations, exact fixed-target snapshots, and a
|
|
2418
|
+
two-phase reboot reservation/final commit. Reservation drains queued owner
|
|
2419
|
+
mutations and rejects new ones; after the requested checkpoint is fsynced, the
|
|
2420
|
+
broker repeats the array/parity/mover/durable-health preflight and invokes the
|
|
2421
|
+
fixed host reboot itself exactly once. The Unit 16 broker inspects only the
|
|
2422
|
+
deployment profile's attested immutable container ID (staging is
|
|
2423
|
+
`ouro-butler-staging`), requires its immutable image ID, writes that exact typed/redacted
|
|
2424
|
+
snapshot before accepting requests, and exposes a nonnegative Docker restart
|
|
2425
|
+
count. Both initial and refreshed snapshots share the same exact schema. The
|
|
2426
|
+
broker derives `autostartExact` from both the Unraid GraphQL `autoStart` value
|
|
2427
|
+
and exactly one staging entry in `/var/lib/docker/unraid-autostart`, derives
|
|
2428
|
+
`updaterDisabled` from `/opt/ouro/container-runtime.json` inside the exact image,
|
|
2429
|
+
and derives `vaultUnlocked`/`manualAuthRequired` from a bounded live Sanctuary
|
|
2430
|
+
vault-status command only when its non-secret summary proves the required
|
|
2431
|
+
Telegram runtime fields and both configured provider records were freshly read.
|
|
2432
|
+
Scenario handles remain private to the scenario adapter. The main one-shot
|
|
2433
|
+
never receives the Docker socket, Unraid key directory, or a host-root mount.
|
|
2434
|
+
Telegram bootstrap additionally brackets its one-shot with a host-controlled
|
|
2435
|
+
poller quiescence guard: it verifies the exact healthy staging container,
|
|
2436
|
+
assumes recovery responsibility before attempting the stop, stops it with a
|
|
2437
|
+
30-second grace bound, proves it is stopped, and mounts a
|
|
2438
|
+
root-owned typed zero-poller fact. Its exit/signal trap restarts that same exact
|
|
2439
|
+
container and waits up to 120 seconds for healthy recovery on both success and
|
|
2440
|
+
failure. It never reads or changes Unraid autostart configuration. Every command
|
|
2441
|
+
also receives a freshly generated, redacted typed container-inspect snapshot;
|
|
2442
|
+
raw container environment or credential values are never captured.
|
|
2443
|
+
Evidence-snapshot keeps the bundle mount read-only and overlays only the exact
|
|
2444
|
+
canonical `state/acceptance` directory as writable. It first stops the exact
|
|
2445
|
+
healthy staging container under recovery responsibility, then opens the
|
|
2446
|
+
directory with `O_DIRECTORY|O_NOFOLLOW` and bind-mounts from the inherited fd
|
|
2447
|
+
while no Sanctuary UID process is running. The launcher bind-pins that inode
|
|
2448
|
+
behind a root-only alias and back onto the canonical path against rename swaps,
|
|
2449
|
+
restarts the same exact image healthy, and compares the alias, canonical path,
|
|
2450
|
+
and staging container's inode before and after capture. Cleanup unmounts the
|
|
2451
|
+
canonical pin and then the alias. The staging daemon therefore sees the same
|
|
2452
|
+
inode used by the one-shot for marker correlation.
|
|
2453
|
+
|
|
2454
|
+
Audit and safety verification:
|
|
2455
|
+
Inspect AgentBundles/sanctuary.ouro/state/approvals for durable approval and
|
|
2456
|
+
restart-attempt receipts. Confirm no Docker socket/device/host-root mounts and
|
|
2457
|
+
no published ports with docker inspect. Confirm Config.Image equals the exact
|
|
2458
|
+
reviewed local image ID, not the build tag. The read key must reject Docker
|
|
2459
|
+
stop and restart mutations; only the separate write key may perform the one
|
|
2460
|
+
typed approved restart action.
|
|
2461
|
+
The packaged `unraid-key-rotate` command is the only canonical key-rotation
|
|
2462
|
+
authority. It inventories the two occupied canonical names by exact immutable
|
|
2463
|
+
ID, creates collision-safe `Butler RO Rotation <suffix>` and
|
|
2464
|
+
`Butler RW Rotation <suffix>` keys, stores and capability-probes both through
|
|
2465
|
+
the Sanctuary machine vault, and requires an exact four-key inventory. It then
|
|
2466
|
+
revokes both old IDs and proves each old credential receives a 401 or 403.
|
|
2467
|
+
Only after an exact inventory contains the temporary pair alone does it create,
|
|
2468
|
+
store, and probe the canonical `Butler RO` and `Butler RW` pair. It requires an
|
|
2469
|
+
exact four-key temporary-plus-canonical inventory, revokes and rejection-probes
|
|
2470
|
+
both temporary IDs, and finally requires exactly the canonical pair. Every
|
|
2471
|
+
unexpected key, duplicate, malformed permission set, role, name, inventory
|
|
2472
|
+
delta, or probe response fails closed.
|
|
2473
|
+
|
|
2474
|
+
Record only IDs, names, and permission sets in evidence; never raw key values.
|
|
2475
|
+
Raw credentials must not appear in argv, shell history, stdout, logs, or
|
|
2476
|
+
acceptance evidence. They may cross only the ephemeral private Unit 16 Unix
|
|
2477
|
+
socket in process memory. The narrow revocation-retry recovery records under
|
|
2478
|
+
`/mnt/user/appdata/ouro-butler/acceptance/revoked-key-proof` are the sole file
|
|
2479
|
+
exception: they are root-owned mode 0600 beneath a root-owned mode 0700
|
|
2480
|
+
directory, excluded from backup/evidence, streamed only on standard input to
|
|
2481
|
+
the bounded rejection probe, retained across failed retries, and durably
|
|
2482
|
+
deleted only after the exact authentication rejection is validated. Never
|
|
2483
|
+
substitute names for IDs when revoking keys.
|