@ouro.bot/cli 0.1.0-alpha.92 → 0.1.0-alpha.921
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/assets/sanctuary-host-launcher.sh +16 -0
- package/changelog.json +5826 -16
- package/deploy/unraid/Dockerfile +58 -0
- package/deploy/unraid/README.txt +2997 -0
- package/deploy/unraid/audit-container-spec.sh +4 -0
- package/deploy/unraid/container-runtime.json +1 -0
- package/deploy/unraid/docker-man-template-transaction.mjs +711 -0
- package/deploy/unraid/docker-man-template-xml.cjs +105 -0
- package/deploy/unraid/migrate-sanctuary-bundle.mjs +60 -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 +43 -0
- package/deploy/unraid/sanctuary-acceptance-contract.json +198 -0
- package/deploy/unraid/sanctuary-acceptance-harness.sh +23 -0
- package/deploy/unraid/sanctuary-authority-installation.json +36 -0
- package/deploy/unraid/sanctuary-authority-service.sh +30 -0
- package/deploy/unraid/sanctuary-butler-upgrade.mjs +1254 -0
- package/deploy/unraid/sanctuary-deployment-target.mjs +710 -0
- package/deploy/unraid/sanctuary-replay-gate.mjs +1564 -0
- package/deploy/unraid/sanctuary-safe-rename.py +24 -0
- package/deploy/unraid/sanctuary-unit16-host-broker.mjs +2086 -0
- package/deploy/unraid/sanctuary-unit16-run.sh +560 -0
- package/deploy/unraid/sanctuary-unit18-target-audit.sh +27 -0
- package/deploy/unraid/sanctuary.ouro/agent.json +66 -0
- package/deploy/unraid/sanctuary.ouro/arc/README.md +3 -0
- package/deploy/unraid/sanctuary.ouro/awaiting/house-care-sweep.md +17 -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/mcp/media-mcp.mjs +2231 -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 +33 -0
- package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +27 -0
- package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +22 -0
- package/deploy/unraid/sanctuary.ouro/state/policy/steward.json +7 -0
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +232 -0
- package/deploy/unraid/sanctuary.xml +30 -0
- package/dist/a2a/card.js +57 -0
- package/dist/a2a/client.js +250 -0
- package/dist/a2a/config.js +50 -0
- package/dist/a2a/delegated-command-grants.js +93 -0
- package/dist/a2a/delegated-command.js +197 -0
- package/dist/a2a/delegation-stores.js +56 -0
- package/dist/a2a/did-resolution.js +93 -0
- package/dist/a2a/escalation-grants.js +84 -0
- package/dist/a2a/escalation-wiring.js +60 -0
- package/dist/a2a/identity.js +258 -0
- package/dist/a2a/inbound-share.js +110 -0
- package/dist/a2a/mission-result-wire.js +196 -0
- package/dist/a2a/onboarding.js +133 -0
- package/dist/a2a/operator-trust.js +382 -0
- package/dist/a2a/outbox-client.js +57 -0
- package/dist/a2a/outbox-store.js +188 -0
- package/dist/a2a/outbox-wire.js +95 -0
- package/dist/a2a/pin-store.js +132 -0
- package/dist/a2a/replay-grant-holders.js +33 -0
- package/dist/a2a/replay-harness.js +188 -0
- package/dist/a2a/resolution-proof.js +53 -0
- package/dist/a2a/sealed-chat.js +113 -0
- package/dist/a2a/seen-ledger.js +120 -0
- package/dist/a2a/server.js +771 -0
- package/dist/a2a/task-store.js +69 -0
- package/dist/a2a/transport.js +45 -0
- package/dist/a2a/trust-commands.js +203 -0
- package/dist/a2a/trusted-files.js +185 -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 +798 -40
- package/dist/heart/agent-entry.js +252 -3
- package/dist/heart/approval-files.js +113 -0
- package/dist/heart/approval-store.js +960 -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/a2a-await-delivery.js +109 -0
- package/dist/heart/awaiting/await-alert.js +152 -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 +152 -0
- package/dist/heart/awaiting/await-runtime-state.js +111 -0
- package/dist/heart/awaiting/await-scheduler.js +391 -0
- package/dist/heart/background-operations.js +281 -0
- package/dist/heart/bluebubbles-host-account.js +78 -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 +44 -18
- package/dist/heart/config-registry.js +331 -0
- package/dist/heart/config.js +174 -131
- package/dist/heart/context-loss-gauntlet.js +387 -0
- package/dist/heart/context-loss-sentinel.js +1159 -0
- package/dist/heart/core.js +2364 -578
- 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 +283 -3
- package/dist/heart/daemon/agent-home.js +319 -0
- 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-arg-redaction.js +26 -0
- package/dist/heart/daemon/cli-defaults.js +876 -0
- package/dist/heart/daemon/cli-desk.js +322 -0
- package/dist/heart/daemon/cli-exec.js +9199 -0
- package/dist/heart/daemon/cli-help.js +735 -0
- package/dist/heart/daemon/cli-parse.js +3211 -0
- package/dist/heart/daemon/cli-render-doctor.js +57 -0
- package/dist/heart/daemon/cli-render.js +819 -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 +112 -0
- package/dist/heart/daemon/container-runtime.js +97 -0
- package/dist/heart/daemon/container-spec-auditor-main.js +171 -0
- package/dist/heart/daemon/container-spec-auditor.js +235 -0
- package/dist/heart/daemon/daemon-bootstrap-startup.js +115 -0
- package/dist/heart/daemon/daemon-cli.js +29 -2350
- package/dist/heart/daemon/daemon-entry.js +953 -11
- 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 +2084 -105
- package/dist/heart/daemon/dns-workflow.js +394 -0
- package/dist/heart/daemon/doctor-types.js +8 -0
- package/dist/heart/daemon/doctor.js +1766 -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 +154 -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 +65 -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 +6 -3
- package/dist/heart/daemon/ouro-entry.js +5 -2
- package/dist/heart/daemon/plugin-cli.js +432 -0
- package/dist/heart/daemon/process-manager.js +760 -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 +2654 -0
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +1830 -0
- package/dist/heart/daemon/sanctuary-acceptance-marker.js +267 -0
- package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +955 -0
- package/dist/heart/daemon/sanctuary-authority-codec.js +86 -0
- package/dist/heart/daemon/sanctuary-authority-epoch.js +281 -0
- package/dist/heart/daemon/sanctuary-authority-installation.js +161 -0
- package/dist/heart/daemon/sanctuary-authority-ledger.js +241 -0
- package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +1267 -0
- package/dist/heart/daemon/sanctuary-authority-vault-migration.js +79 -0
- package/dist/heart/daemon/sanctuary-bundle-migration.js +730 -0
- package/dist/heart/daemon/sanctuary-host-authority.js +1008 -0
- package/dist/heart/daemon/sanctuary-host-detached-supervisor.js +493 -0
- package/dist/heart/daemon/sanctuary-host-executor.js +670 -0
- package/dist/heart/daemon/sanctuary-host-linux-kernel.js +334 -0
- package/dist/heart/daemon/sanctuary-host-supervisor-entry.js +207 -0
- package/dist/heart/daemon/sanctuary-host-supervisor.js +95 -0
- package/dist/heart/daemon/sanctuary-package-management.js +103 -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/sanctuary-telegram-authority-entry.js +475 -0
- package/dist/heart/daemon/sanctuary-telegram-authority-gateway.js +585 -0
- package/dist/heart/daemon/sanctuary-telegram-authority-service.js +676 -0
- package/dist/heart/daemon/sense-manager.js +733 -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 +339 -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 +503 -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 +1516 -0
- package/dist/heart/failure-reports.js +336 -0
- package/dist/heart/file-lock.js +109 -0
- package/dist/heart/frontend-approval-runtime.js +506 -0
- package/dist/heart/frontend-journal.js +215 -0
- package/dist/heart/frontend-session-service.js +237 -0
- package/dist/heart/frontend-socket-client.js +164 -0
- package/dist/heart/frontend-socket.js +400 -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 +230 -59
- package/dist/heart/kept-notes.js +289 -0
- package/dist/heart/kicks.js +1 -1
- package/dist/heart/machine-identity.js +175 -0
- package/dist/heart/mail-import-discovery.js +356 -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 +272 -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 +231 -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 +83 -16
- 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 +260 -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 +38 -39
- 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/reply-style.js +52 -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 +24 -4
- package/dist/heart/session-activity.js +92 -24
- package/dist/heart/session-events.js +1504 -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-redaction-repair-cli-main.js +31 -0
- package/dist/heart/session-redaction-repair.js +552 -0
- package/dist/heart/session-stats-cli-main.js +5 -0
- package/dist/heart/session-stats.js +183 -0
- package/dist/heart/session-transcript.js +133 -0
- package/dist/heart/shelf-claims.js +78 -0
- package/dist/heart/shepherd-config.js +113 -0
- package/dist/heart/source-grounding.js +297 -0
- package/dist/heart/start-of-turn-packet.js +372 -0
- package/dist/heart/steward-policy.js +656 -0
- package/dist/heart/streaming.js +609 -207
- package/dist/heart/structured-output.js +196 -0
- package/dist/heart/sync-classification.js +176 -0
- package/dist/heart/sync-health.js +152 -0
- package/dist/heart/sync.js +726 -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 +382 -0
- package/dist/heart/tool-description.js +155 -0
- package/dist/heart/tool-friction.js +55 -0
- package/dist/heart/tool-loop.js +206 -0
- package/dist/heart/turn-context.js +496 -0
- package/dist/heart/turn-execution-lease.js +30 -0
- package/dist/heart/{daemon → versioning}/ouro-bot-global-installer.js +6 -5
- package/dist/heart/{daemon → versioning}/ouro-bot-wrapper.js +3 -2
- 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 +432 -0
- package/dist/heart/versioning/release-sources.js +173 -0
- package/dist/heart/versioning/self-update-defaults.js +294 -0
- package/dist/heart/versioning/self-update.js +196 -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/update-state.js +126 -0
- package/dist/heart/versioning/updater-launchd.js +150 -0
- package/dist/heart/versioning/version-intent.js +114 -0
- package/dist/heart/work-card.js +380 -0
- package/dist/mailbox-ui/assets/index-Cyv4mw3Y.js +1 -0
- package/dist/mailbox-ui/assets/index-Du_9G9WO.css +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 +307 -139
- 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/friend-link-codes.js +209 -0
- package/dist/mind/friend-merge.js +330 -0
- package/dist/mind/{associative-recall.js → note-search.js} +47 -58
- package/dist/mind/obligation-steering.js +199 -3
- package/dist/mind/pending.js +36 -10
- package/dist/mind/prompt-budget.js +479 -0
- package/dist/mind/prompt-refresh.js +3 -2
- package/dist/mind/prompt.js +1085 -198
- 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 +564 -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 +1201 -0
- package/dist/repertoire/bundle-templates.js +71 -0
- package/dist/repertoire/bw-installer.js +231 -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 +174 -32
- package/dist/repertoire/coding/index.js +15 -3
- package/dist/repertoire/coding/manager.js +159 -10
- package/dist/repertoire/coding/spawner.js +40 -11
- package/dist/repertoire/coding/tools.js +182 -18
- 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 +183 -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 +968 -20
- package/dist/repertoire/house-sweep.js +469 -0
- package/dist/repertoire/mcp-client.js +58 -5
- package/dist/repertoire/mcp-manager.js +427 -139
- package/dist/repertoire/mcp-tools.js +132 -0
- package/dist/repertoire/mcp-write-guard.js +46 -0
- package/dist/repertoire/plugin-mcp.js +175 -0
- package/dist/repertoire/plugins.js +253 -0
- package/dist/repertoire/relationship-authorization.js +322 -0
- package/dist/repertoire/sanctuary-container-policy.js +46 -0
- package/dist/repertoire/shell-sessions.js +134 -0
- package/dist/repertoire/skills.js +48 -4
- package/dist/repertoire/stripe-client.js +131 -0
- package/dist/repertoire/tool-arguments.js +182 -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 +872 -0
- package/dist/repertoire/tools-base.js +83 -1168
- package/dist/repertoire/tools-bluebubbles.js +2 -0
- package/dist/repertoire/tools-bridge.js +144 -0
- package/dist/repertoire/tools-bundle.js +995 -0
- package/dist/repertoire/tools-commerce.js +253 -0
- package/dist/repertoire/tools-config.js +186 -0
- package/dist/repertoire/tools-continuity.js +575 -0
- package/dist/repertoire/tools-credential.js +383 -0
- package/dist/repertoire/tools-escalation.js +84 -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-friend-link.js +57 -0
- package/dist/repertoire/tools-github.js +3 -8
- package/dist/repertoire/tools-habits.js +103 -0
- package/dist/repertoire/tools-house-care.js +174 -0
- package/dist/repertoire/tools-mail.js +1828 -0
- package/dist/repertoire/tools-notes.js +496 -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 +238 -0
- package/dist/repertoire/tools-sanctuary-host.js +202 -0
- package/dist/repertoire/tools-session.js +1133 -0
- package/dist/repertoire/tools-shell.js +149 -0
- package/dist/repertoire/tools-shepherd.js +262 -0
- package/dist/repertoire/tools-steward-policy.js +147 -0
- package/dist/repertoire/tools-stripe.js +224 -0
- package/dist/repertoire/tools-surface.js +369 -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 +556 -0
- package/dist/repertoire/tools-user-profile.js +146 -0
- package/dist/repertoire/tools-vault.js +40 -0
- package/dist/repertoire/tools-voice-local.js +198 -0
- package/dist/repertoire/tools-voice.js +145 -0
- package/dist/repertoire/tools.js +514 -156
- package/dist/repertoire/travel-api-client.js +360 -0
- package/dist/repertoire/unraid-client.js +155 -0
- package/dist/repertoire/unraid-restart.js +339 -0
- package/dist/repertoire/user-profile.js +131 -0
- package/dist/repertoire/vault-setup.js +246 -0
- package/dist/repertoire/vault-unlock.js +686 -0
- package/dist/rsvp/aisleplanner-client.js +354 -0
- package/dist/rsvp/cli.js +936 -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 +132 -0
- package/dist/senses/acp-server.js +386 -0
- package/dist/senses/attention-queue.js +186 -0
- package/dist/senses/await-turn-message.js +65 -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} +379 -41
- 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/host-account-verifier.js +88 -0
- package/dist/senses/{bluebubbles-inbound-log.js → bluebubbles/inbound-log.js} +21 -3
- package/dist/senses/bluebubbles/index.js +3878 -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 +257 -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 +601 -304
- 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 +1158 -204
- package/dist/senses/private-runtime-worker.js +859 -0
- package/dist/senses/private-runtime.js +1627 -0
- package/dist/senses/proactive-content-guard.js +51 -0
- package/dist/senses/root-host-approval-port.js +345 -0
- package/dist/senses/root-host-approval-runtime.js +393 -0
- package/dist/senses/sanctuary-authority-resident.js +102 -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 +104 -0
- package/dist/senses/sanctuary-health.js +422 -0
- package/dist/senses/sanctuary-install-state-contract.js +169 -0
- package/dist/senses/sanctuary-interactive-control.js +410 -0
- package/dist/senses/sanctuary-media-catalog-contract.js +259 -0
- package/dist/senses/sanctuary-media-optimization.js +514 -0
- package/dist/senses/sanctuary-runtime.js +369 -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 +793 -0
- package/dist/senses/shepherd/client.js +196 -0
- package/dist/senses/shepherd/cmux.js +352 -0
- package/dist/senses/shepherd/herdr.js +315 -0
- package/dist/senses/shepherd/host.js +14 -0
- package/dist/senses/shepherd/judge.js +137 -0
- package/dist/senses/shepherd/owner-notice.js +251 -0
- package/dist/senses/shepherd/redact.js +66 -0
- package/dist/senses/shepherd/returns.js +79 -0
- package/dist/senses/shepherd/sense.js +310 -0
- package/dist/senses/shepherd/terminal-process.js +99 -0
- package/dist/senses/shepherd-entry.js +78 -0
- package/dist/senses/surface-tool.js +109 -0
- package/dist/senses/teams-entry.js +68 -8
- package/dist/senses/teams.js +598 -233
- package/dist/senses/telegram-admission.js +718 -0
- package/dist/senses/telegram-approval-runtime.js +793 -0
- package/dist/senses/telegram-attachments.js +186 -0
- package/dist/senses/telegram-audit-ledger.js +220 -0
- package/dist/senses/telegram-authority-transport.js +231 -0
- package/dist/senses/telegram-client.js +1749 -0
- package/dist/senses/telegram-effect-adapter.js +900 -0
- package/dist/senses/telegram-entry.js +83 -0
- package/dist/senses/telegram-handoff.js +80 -0
- package/dist/senses/telegram.js +2405 -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/call-auth.js +250 -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/local-audio-control.js +344 -0
- package/dist/senses/voice/local-audio-devices.js +516 -0
- package/dist/senses/voice/local-audio-disclosure.js +174 -0
- package/dist/senses/voice/local-audio-identity.js +103 -0
- package/dist/senses/voice/local-audio-join.js +325 -0
- package/dist/senses/voice/local-audio-mute.js +199 -0
- package/dist/senses/voice/local-audio-notify.js +121 -0
- package/dist/senses/voice/local-audio-prompts.js +116 -0
- package/dist/senses/voice/local-audio-transport.js +427 -0
- package/dist/senses/voice/local-audio.js +281 -0
- package/dist/senses/voice/local-media-stream.js +145 -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/process-guards.js +51 -0
- package/dist/senses/voice/realtime-eval.js +496 -0
- package/dist/senses/voice/realtime-tool-schema.js +195 -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 +836 -0
- package/dist/senses/voice/twilio-phone.js +5454 -0
- package/dist/senses/voice/types.js +2 -0
- package/dist/senses/voice/whisper.js +161 -0
- package/dist/senses/voice-entry.js +83 -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 +8100 -0
- package/package.json +74 -13
- 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 -260
- package/dist/heart/daemon/ouro-version-manager.js +0 -171
- package/dist/heart/daemon/update-hooks.js +0 -138
- package/dist/heart/obligations.js +0 -191
- package/dist/heart/safe-workspace.js +0 -362
- 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 -1181
- package/dist/senses/debug-activity.js +0 -154
- 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,1267 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var _a;
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.SanctuaryAuthorityRootLifecycle = exports.SANCTUARY_UPGRADE_STEPS = exports.SANCTUARY_KEEPER_BOOT_LINES = void 0;
|
|
38
|
+
exports.recordSanctuaryRootLifecycleFailure = recordSanctuaryRootLifecycleFailure;
|
|
39
|
+
exports.runSanctuaryAuthorityRootCli = runSanctuaryAuthorityRootCli;
|
|
40
|
+
const node_child_process_1 = require("node:child_process");
|
|
41
|
+
const node_crypto_1 = require("node:crypto");
|
|
42
|
+
const fs = __importStar(require("node:fs"));
|
|
43
|
+
const path = __importStar(require("node:path"));
|
|
44
|
+
const telegram_client_1 = require("../../senses/telegram-client");
|
|
45
|
+
const sanctuary_authority_installation_1 = require("./sanctuary-authority-installation");
|
|
46
|
+
const sanctuary_authority_epoch_1 = require("./sanctuary-authority-epoch");
|
|
47
|
+
const sanctuary_telegram_authority_gateway_1 = require("./sanctuary-telegram-authority-gateway");
|
|
48
|
+
const sanctuary_authority_resident_1 = require("../../senses/sanctuary-authority-resident");
|
|
49
|
+
const sanctuary_authority_vault_migration_1 = require("./sanctuary-authority-vault-migration");
|
|
50
|
+
const session_transaction_1 = require("../../mind/session-transaction");
|
|
51
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
52
|
+
const ROOT = "/mnt/user/appdata/ouro-authority";
|
|
53
|
+
const BUNDLE = "/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro";
|
|
54
|
+
const RUNTIME = "/mnt/user/appdata/ouro-butler/runtime/.ouro-cli";
|
|
55
|
+
const OFFSET = `${BUNDLE}/state/senses/telegram/offset.json`;
|
|
56
|
+
const SOCKET = "/run/ouro-authority";
|
|
57
|
+
// Operator-set grants, root-owned on the host and mounted read-only into the Butler at /etc/ouro/trust/sanctuary.
|
|
58
|
+
const TRUST = "/mnt/user/appdata/ouro-butler/trust/sanctuary";
|
|
59
|
+
const STAGING = "/var/lib/ouro-authority/staging";
|
|
60
|
+
const CGROUP = "/sys/fs/cgroup/ouro-authority";
|
|
61
|
+
const BOOT = "/boot/config/custom/ouro-authority/start.sh";
|
|
62
|
+
const BOOT_LINE = `/bin/sh ${BOOT} --boot & # ouro-authority`;
|
|
63
|
+
// On Unraid the host keeper owns boot instead (it runs BOOT --boot itself, D-032), and the
|
|
64
|
+
// upgrade orchestrator installs exactly these lines in place of BOOT_LINE (D-045).
|
|
65
|
+
const KEEPER_SUPERVISOR = "/boot/config/custom/ouro-authority/gateway-supervisor.sh";
|
|
66
|
+
const KEEPER_WATCHDOG = "/boot/config/custom/ouro-authority/gateway-keeper-watchdog.sh";
|
|
67
|
+
exports.SANCTUARY_KEEPER_BOOT_LINES = {
|
|
68
|
+
supervisor: `setsid /bin/sh ${KEEPER_SUPERVISOR} >/dev/null 2>&1 & # ouro-authority-gateway`,
|
|
69
|
+
watchdog: `(crontab -l 2>/dev/null | grep -v gateway-keeper-watchdog; echo "*/2 * * * * /bin/sh ${KEEPER_WATCHDOG} # ouro-authority-gateway-watchdog") | crontab - # ouro-authority-gateway-watchdog`,
|
|
70
|
+
};
|
|
71
|
+
const BOOT_OWNERS = [BOOT_LINE, exports.SANCTUARY_KEEPER_BOOT_LINES.supervisor];
|
|
72
|
+
const KNOWN_BOOT_LINES = [...BOOT_OWNERS, exports.SANCTUARY_KEEPER_BOOT_LINES.watchdog];
|
|
73
|
+
const DIGEST = /^sha256:[a-f0-9]{64}$/u;
|
|
74
|
+
// The gateway verifies every pinned package file before it reports ready. With a cold
|
|
75
|
+
// page cache on Unraid's array that takes minutes, so readiness gets a generous budget
|
|
76
|
+
// (a 120 s budget failed a real rollback on 2026-09-24).
|
|
77
|
+
const GATEWAY_READY_TIMEOUT_MS = 900_000;
|
|
78
|
+
const GATEWAY_START_BACKOFF_MS = [2_000, 10_000, 30_000];
|
|
79
|
+
const GATEWAY_LOG_TAIL_CHARS = 2_000;
|
|
80
|
+
// Read more than is shown, so masking sees whole tokens before the tail is cut.
|
|
81
|
+
const GATEWAY_LOG_READ_BYTES = 4_096;
|
|
82
|
+
const GATEWAY_LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
83
|
+
const TEMPLATE_JOURNAL = "/boot/config/custom/ouro-butler/docker-man-template-transaction.json";
|
|
84
|
+
// In-place upgrade of an installed authority: same epoch (token, issuer, cursor,
|
|
85
|
+
// gateway state), new reviewed package and resident image. Every root record it
|
|
86
|
+
// rewrites is backed up first so an interrupted or failed upgrade rolls back exactly.
|
|
87
|
+
const UPGRADE = `${ROOT}/upgrade.json`;
|
|
88
|
+
const PREVIOUS = `${ROOT}/upgrade-previous`;
|
|
89
|
+
const NEXT_PACKAGE = `${ROOT}/package-next`;
|
|
90
|
+
const INCOMING_REQUEST = `${ROOT}/incoming-request.json`;
|
|
91
|
+
const INCOMING_MANIFEST = `${ROOT}/incoming-package-manifest.json`;
|
|
92
|
+
const EVENTS = "/boot/config/custom/ouro-events/spool";
|
|
93
|
+
const ICON = "https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png";
|
|
94
|
+
const IMAGE_REFERENCE = /^ghcr\.io\/ourostack\/ouroboros-butler:[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/u;
|
|
95
|
+
exports.SANCTUARY_UPGRADE_STEPS = ["stop", "switch", "resident", "migrate", "start"];
|
|
96
|
+
const ROOT_RECORDS = [["request.json", 1024 * 1024], ["package-manifest.json", 8 * 1024 * 1024], ["active.json", 1024 * 1024], ["activation.json", 1024 * 1024]];
|
|
97
|
+
const EPOCH_RECORDS = ["migration.json", "stage.json", "configured.json"];
|
|
98
|
+
const digest = (value) => `sha256:${(0, node_crypto_1.createHash)("sha256").update(value).digest("hex")}`;
|
|
99
|
+
function validateRequest(value) {
|
|
100
|
+
const request = value;
|
|
101
|
+
if (!request || typeof request !== "object" || Object.keys(request).sort().join(",") !== "botId,epochId,nodeDigest,ownerChatId,ownerUserId,packageDigest,prlimitDigest,schemaVersion,setsidDigest,shellDigest"
|
|
102
|
+
|| request.schemaVersion !== 1 || !/^[A-Za-z0-9_-]{1,128}$/u.test(request.epochId)
|
|
103
|
+
|| ![request.botId, request.ownerUserId, request.ownerChatId].every((id) => typeof id === "string" && /^[1-9][0-9]*$/u.test(id))
|
|
104
|
+
|| request.ownerUserId !== request.ownerChatId
|
|
105
|
+
|| ![request.packageDigest, request.nodeDigest, request.prlimitDigest, request.setsidDigest, request.shellDigest].every((pin) => typeof pin === "string" && DIGEST.test(pin)))
|
|
106
|
+
throw new Error("Sanctuary root lifecycle request is invalid");
|
|
107
|
+
return request;
|
|
108
|
+
}
|
|
109
|
+
// The prefix is a filesystem-fixture seam; the production CLI never accepts it.
|
|
110
|
+
class SanctuaryAuthorityRootLifecycle {
|
|
111
|
+
#request;
|
|
112
|
+
#transaction;
|
|
113
|
+
#prefix;
|
|
114
|
+
#uid;
|
|
115
|
+
#gid;
|
|
116
|
+
#socketGid;
|
|
117
|
+
#options;
|
|
118
|
+
constructor(transaction, options = {}) {
|
|
119
|
+
this.#options = options;
|
|
120
|
+
this.#prefix = options.prefix ?? "";
|
|
121
|
+
this.#uid = options.expectedUid ?? 0;
|
|
122
|
+
this.#gid = options.expectedGid ?? 0;
|
|
123
|
+
this.#socketGid = options.socketGroupId ?? 10001;
|
|
124
|
+
if (process.getuid() !== this.#uid || process.getgid() !== this.#gid)
|
|
125
|
+
throw new Error("Sanctuary root lifecycle requires root");
|
|
126
|
+
if (!DIGEST.test(transaction.targetImageId) || !DIGEST.test(transaction.rollbackImageId) || transaction.targetImageId === transaction.rollbackImageId)
|
|
127
|
+
throw new Error("Sanctuary root lifecycle image identity is invalid");
|
|
128
|
+
this.#transaction = { targetImageId: transaction.targetImageId, rollbackImageId: transaction.rollbackImageId };
|
|
129
|
+
this.#directory(ROOT);
|
|
130
|
+
this.#request = validateRequest(JSON.parse(this.#private(`${ROOT}/request.json`)));
|
|
131
|
+
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.sanctuary_root_lifecycle_loaded", message: "Sanctuary root lifecycle identity loaded", meta: { epochId: this.#request.epochId, targetImageId: transaction.targetImageId } });
|
|
132
|
+
}
|
|
133
|
+
plan() {
|
|
134
|
+
const { epochId, botId, ownerUserId, ownerChatId, packageDigest } = this.#request;
|
|
135
|
+
return { schemaVersion: 1, epochId, botId, ownerUserId, ownerChatId, packageDigest, publicKeyDigest: null, predecessorContract: "canonical-pre-gateway", targetContract: "canonical-gateway" };
|
|
136
|
+
}
|
|
137
|
+
async boot() {
|
|
138
|
+
if (!fs.existsSync(this.#p(`${ROOT}/activation.json`)))
|
|
139
|
+
return false;
|
|
140
|
+
// An upgrade that never finished (process killed, host rebooted) is rolled back
|
|
141
|
+
// here, so the Butler comes back on its known-good version without a human.
|
|
142
|
+
if (fs.existsSync(this.#p(UPGRADE)))
|
|
143
|
+
return this.rollbackUpgrade();
|
|
144
|
+
this.#record(`${ROOT}/activation.json`, { ...this.#transaction, state: "active", epochId: this.#request.epochId });
|
|
145
|
+
if (fs.existsSync(this.#p("/boot/config/custom/ouro-butler/docker-man-template-transaction.json")))
|
|
146
|
+
throw new Error("Sanctuary pending installation requires recovery, not boot");
|
|
147
|
+
this.#assertNoForeignPoller();
|
|
148
|
+
this.#target();
|
|
149
|
+
if (this.#containers().some((container) => container.Name !== "/ouro-butler" && container.State.Running))
|
|
150
|
+
throw new Error("Sanctuary rollback resident is running");
|
|
151
|
+
this.#runtimeDirectories();
|
|
152
|
+
this.#verifyPackage(undefined, false);
|
|
153
|
+
// Boot may already have a tokenless resident under Docker's restart policy.
|
|
154
|
+
// Only handoff requires us to start it strictly after gateway readiness.
|
|
155
|
+
await this.#startGateway(true);
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
repinExecution(prlimitDigest, setsidDigest) {
|
|
159
|
+
if (![prlimitDigest, setsidDigest].every(pin => DIGEST.test(pin)))
|
|
160
|
+
throw new Error("Reviewed execution digests are required");
|
|
161
|
+
if (fs.existsSync(this.#p("/boot/config/custom/ouro-butler/docker-man-template-transaction.json")))
|
|
162
|
+
throw new Error("Finish the pending installation before re-pinning");
|
|
163
|
+
this.#record(`${ROOT}/activation.json`, { ...this.#transaction, state: "active", epochId: this.#request.epochId });
|
|
164
|
+
this.#verifyPackage(undefined, false);
|
|
165
|
+
if (this.#pid() !== null)
|
|
166
|
+
throw new Error("Stop the verified gateway process before re-pinning");
|
|
167
|
+
for (const name of ["executions", "supervisors"]) {
|
|
168
|
+
const directory = `${this.#epochRoot()}/${name}`;
|
|
169
|
+
if (fs.existsSync(this.#p(directory))) {
|
|
170
|
+
this.#directory(directory);
|
|
171
|
+
if (fs.readdirSync(this.#p(directory)).length !== 0)
|
|
172
|
+
throw new Error("Resolve pending execution state before re-pinning");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if ((0, sanctuary_authority_installation_1.sanctuaryCgroupChildren)(this.#p(CGROUP)).length !== 0)
|
|
176
|
+
throw new Error("Resolve pending cgroups before re-pinning");
|
|
177
|
+
this.#verifyPrimitive("/usr/bin/prlimit", prlimitDigest);
|
|
178
|
+
this.#verifyPrimitive("/usr/bin/setsid", setsidDigest);
|
|
179
|
+
const currentRequest = JSON.parse(this.#private(`${ROOT}/request.json`));
|
|
180
|
+
if (JSON.stringify(currentRequest) !== JSON.stringify(this.#request))
|
|
181
|
+
throw new Error("Sanctuary re-pin request changed");
|
|
182
|
+
const configuration = { ...this.#configuration(), hostPrlimitDigest: prlimitDigest, hostSetsidDigest: setsidDigest };
|
|
183
|
+
const current = JSON.parse(this.#private(`${ROOT}/active.json`));
|
|
184
|
+
if (JSON.stringify({ ...current, hostPrlimitDigest: prlimitDigest, hostSetsidDigest: setsidDigest }) !== JSON.stringify(configuration))
|
|
185
|
+
throw new Error("Sanctuary re-pin configuration changed");
|
|
186
|
+
const request = { ...this.#request, prlimitDigest, setsidDigest };
|
|
187
|
+
// Request-first interruption leaves the old runtime pins fail-closed; repeating the command completes publication.
|
|
188
|
+
this.#write(`${ROOT}/request.json`, JSON.stringify(request));
|
|
189
|
+
this.#write(`${ROOT}/active.json`, JSON.stringify(configuration));
|
|
190
|
+
this.#request.prlimitDigest = prlimitDigest;
|
|
191
|
+
this.#request.setsidDigest = setsidDigest;
|
|
192
|
+
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.sanctuary_execution_pins_refreshed", message: "Reviewed Sanctuary execution pins refreshed; gateway restart required", meta: { prlimitDigest, setsidDigest } });
|
|
193
|
+
}
|
|
194
|
+
effect(step) {
|
|
195
|
+
const effects = {
|
|
196
|
+
"freeze-resident": { read: () => this.#frozen(), apply: () => this.#freeze() },
|
|
197
|
+
"stage-authority": { read: () => this.#staged(), apply: () => this.#stage() },
|
|
198
|
+
"verify-token-rotation": { read: () => this.#rotated(), apply: () => this.#rotate() },
|
|
199
|
+
"transfer-cursor": { read: () => this.#transferred(), apply: () => this.#transfer() },
|
|
200
|
+
"remove-resident-token": { read: () => this.#tokensAbsent(), apply: () => { this.#requireStopped(); this.#vault("remove"); } },
|
|
201
|
+
"start-gateway": { read: () => this.#ready(), apply: () => this.#startGateway() },
|
|
202
|
+
"configure-resident": { read: () => this.#configured(), apply: () => this.#configure() },
|
|
203
|
+
"start-resident": { read: () => this.#target().State.Running, apply: () => this.#startResident() },
|
|
204
|
+
"verify-install": { read: () => this.#active(), apply: () => this.#activate() },
|
|
205
|
+
"rollback:freeze-resident": { read: () => this.#frozen() && !fs.existsSync(this.#p(`${ROOT}/activation.json`)), apply: () => { this.#remove(`${ROOT}/activation.json`); this.#freeze(); } },
|
|
206
|
+
"rollback:retire-registrations": { read: () => this.#retiredProcess(), apply: () => this.#retireProcess() },
|
|
207
|
+
"rollback:reconcile-executions": { read: () => this.#retiredProcess(), apply: () => this.#retireProcess() },
|
|
208
|
+
"rollback:stop-gateway": { read: () => this.#retiredProcess() && this.#pid() === null, apply: () => this.#stopGateway() },
|
|
209
|
+
"rollback:end-epoch": { read: () => this.#epoch().state === "retired", apply: () => this.#endEpoch() },
|
|
210
|
+
"rollback:restore-token-cursor": { read: () => this.#restoredToken(), apply: () => this.#restoreToken() },
|
|
211
|
+
"rollback:restore-resident": { read: () => this.#rollback().State.Running, apply: () => this.#restoreResident() },
|
|
212
|
+
"rollback:verify-rollback": { read: () => this.#rollbackVerified(), apply: () => this.#verifyRollback() },
|
|
213
|
+
};
|
|
214
|
+
const effect = effects[step];
|
|
215
|
+
if (!effect)
|
|
216
|
+
throw new Error("Sanctuary root lifecycle effect is invalid");
|
|
217
|
+
const beforeDigest = digest(`${this.#request.epochId}:${step}:before`);
|
|
218
|
+
const afterDigest = digest(`${this.#request.epochId}:${step}:after`);
|
|
219
|
+
return {
|
|
220
|
+
beforeDigest, afterDigest,
|
|
221
|
+
readback: async () => await effect.read() ? afterDigest : beforeDigest,
|
|
222
|
+
apply: async () => { await effect.apply(); },
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
#frozen() {
|
|
226
|
+
this.#assertNoForeignPoller();
|
|
227
|
+
if (!this.#containers().every((container) => !container.State.Running) || !fs.existsSync(this.#p(`${this.#epochRoot()}/migration.json`)))
|
|
228
|
+
return false;
|
|
229
|
+
this.#migration();
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
#freeze() {
|
|
233
|
+
this.#assertNoForeignPoller();
|
|
234
|
+
for (const container of this.#containers()) {
|
|
235
|
+
if (![this.#transaction.targetImageId, this.#transaction.rollbackImageId].includes(container.Image))
|
|
236
|
+
throw new Error("Sanctuary resident image changed");
|
|
237
|
+
if (container.State.Running)
|
|
238
|
+
this.#docker(["stop", "--time", "30", container.Name.slice(1)]);
|
|
239
|
+
}
|
|
240
|
+
if (this.#containers().some((container) => container.State.Running))
|
|
241
|
+
throw new Error("Sanctuary resident did not stop");
|
|
242
|
+
if (fs.existsSync(this.#p(`${this.#epochRoot()}/migration.json`))) {
|
|
243
|
+
this.#migration();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (fs.existsSync(this.#p(`${ROOT}/active.json`))) {
|
|
247
|
+
const previous = JSON.parse(this.#private(`${ROOT}/active.json`));
|
|
248
|
+
const epoch = (0, sanctuary_authority_epoch_1.readSanctuaryAuthorityEpoch)(previous.epochRoot, { expectedUid: this.#uid, expectedGid: this.#gid });
|
|
249
|
+
if (epoch.state !== "retired" || !fs.existsSync(path.join(previous.epochRoot, "handoff.json"))
|
|
250
|
+
|| fs.existsSync(this.#p(`${ROOT}/activation.json`)))
|
|
251
|
+
throw new Error("Sanctuary previous epoch handoff is incomplete");
|
|
252
|
+
}
|
|
253
|
+
// A missing cursor is not a fresh install: it is lost handoff evidence.
|
|
254
|
+
fs.lstatSync(this.#p(OFFSET));
|
|
255
|
+
const predecessorCursor = new telegram_client_1.FileTelegramOffsetStore(this.#p(OFFSET)).load();
|
|
256
|
+
const snapshot = this.#vault("snapshot");
|
|
257
|
+
if (["botId", "ownerUserId", "ownerChatId"].some((key) => snapshot[key] !== this.#request[key]))
|
|
258
|
+
throw new Error("Sanctuary predecessor identity changed");
|
|
259
|
+
this.#write(`${ROOT}/epochs/${this.#request.epochId}/previous-token`, snapshot.token);
|
|
260
|
+
this.#write(`${this.#epochRoot()}/migration.json`, JSON.stringify({ ...this.plan(), ...this.#transaction, predecessorCursor }));
|
|
261
|
+
}
|
|
262
|
+
#epochRoot() { return `${ROOT}/epochs/${this.#request.epochId}`; }
|
|
263
|
+
#migration() {
|
|
264
|
+
const value = JSON.parse(this.#private(`${this.#epochRoot()}/migration.json`));
|
|
265
|
+
for (const [key, expected] of Object.entries({ ...this.plan(), ...this.#transaction })) {
|
|
266
|
+
if (value[key] !== expected)
|
|
267
|
+
throw new Error("Sanctuary root migration identity changed");
|
|
268
|
+
}
|
|
269
|
+
if (!Number.isSafeInteger(value.predecessorCursor) || value.predecessorCursor < 0)
|
|
270
|
+
throw new Error("Sanctuary root migration cursor is invalid");
|
|
271
|
+
return value;
|
|
272
|
+
}
|
|
273
|
+
#directory(absolute, mode = 0o700, gid = this.#gid) {
|
|
274
|
+
const directory = this.#p(absolute);
|
|
275
|
+
if (!fs.existsSync(directory)) {
|
|
276
|
+
fs.mkdirSync(directory, { recursive: true, mode });
|
|
277
|
+
fs.chownSync(directory, this.#uid, gid);
|
|
278
|
+
fs.chmodSync(directory, mode);
|
|
279
|
+
}
|
|
280
|
+
const stat = fs.lstatSync(directory);
|
|
281
|
+
if (!stat.isDirectory() || stat.uid !== this.#uid || stat.gid !== gid || (stat.mode & 0o7777) !== mode || fs.realpathSync(directory) !== directory)
|
|
282
|
+
throw new Error("Sanctuary root lifecycle directory is unsafe");
|
|
283
|
+
}
|
|
284
|
+
#verifyPackage(packageRoot = `${ROOT}/package`, hostExecution = true, manifestPath = `${ROOT}/package-manifest.json`, request = this.#request) {
|
|
285
|
+
(0, sanctuary_authority_installation_1.verifySanctuaryAuthorityInstallation)({
|
|
286
|
+
packageRoot: this.#p(packageRoot), manifestPath: this.#p(manifestPath), manifestDigest: request.packageDigest,
|
|
287
|
+
stateRoot: this.#p(this.#epochRoot()), stagingRoot: this.#p(STAGING), socketRoot: this.#p(SOCKET), cgroupRoot: this.#p(CGROUP),
|
|
288
|
+
expectedUid: this.#uid, expectedGid: this.#gid, socketGroupId: this.#socketGid, mountInfo: fs.readFileSync(this.#p("/proc/self/mountinfo"), "utf8"),
|
|
289
|
+
});
|
|
290
|
+
const manifest = JSON.parse(this.#private(manifestPath, 0o600, 8 * 1024 * 1024));
|
|
291
|
+
for (const program of ["dist/heart/daemon/sanctuary-telegram-authority-entry.js", "dist/heart/daemon/sanctuary-authority-root-lifecycle.js", "dist/heart/daemon/sanctuary-host-supervisor-entry.js", "deploy/unraid/sanctuary-host-launcher.sh", "deploy/unraid/sanctuary-authority-service.sh"]) {
|
|
292
|
+
if (!Object.hasOwn(manifest.files, program))
|
|
293
|
+
throw new Error("Sanctuary authority package program is absent");
|
|
294
|
+
}
|
|
295
|
+
for (const [file, expected] of [
|
|
296
|
+
["/usr/local/bin/node", request.nodeDigest], ["/bin/sh", request.shellDigest],
|
|
297
|
+
...(hostExecution ? [["/usr/bin/prlimit", request.prlimitDigest], ["/usr/bin/setsid", request.setsidDigest]] : []),
|
|
298
|
+
]) {
|
|
299
|
+
this.#verifyPrimitive(file, expected);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
#verifyPrimitive(file, expected) {
|
|
303
|
+
const actual = this.#p(file);
|
|
304
|
+
const stat = fs.statSync(actual);
|
|
305
|
+
if (!stat.isFile() || stat.uid !== this.#uid || stat.gid !== this.#gid || (stat.mode & 0o022) !== 0 || (stat.mode & 0o111) === 0 || digest(fs.readFileSync(actual)) !== expected)
|
|
306
|
+
throw new Error("Sanctuary host primitive pin changed");
|
|
307
|
+
}
|
|
308
|
+
#staged(hostExecution = true) {
|
|
309
|
+
if (!fs.existsSync(this.#p(`${this.#epochRoot()}/stage.json`)))
|
|
310
|
+
return false;
|
|
311
|
+
this.#record(`${this.#epochRoot()}/stage.json`, { packageDigest: this.#request.packageDigest });
|
|
312
|
+
this.#verifyPackage(undefined, hostExecution);
|
|
313
|
+
if (this.#bootScript().text.split("\n").filter((line) => BOOT_OWNERS.includes(line)).length !== 1
|
|
314
|
+
|| digest(this.#private(BOOT)) !== digest(fs.readFileSync(this.#p(`${ROOT}/package/deploy/unraid/sanctuary-authority-service.sh`))))
|
|
315
|
+
throw new Error("Sanctuary installed boot lifecycle changed");
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
/** Create the authority cgroup with its keep child. The reaper can remove a freshly
|
|
319
|
+
* made (still empty) cgroup before the child exists, so retry that race a few times. */
|
|
320
|
+
#cgroup() {
|
|
321
|
+
const keep = this.#p(`${CGROUP}/${sanctuary_authority_installation_1.SANCTUARY_CGROUP_KEEP}`);
|
|
322
|
+
if (!this.#keepSupported()) {
|
|
323
|
+
// A package from before the keep child refuses an unknown child cgroup and relies on
|
|
324
|
+
// the host keeper holding Unraid's reaper paused (a rollback to 830 hit exactly that).
|
|
325
|
+
this.#freezeUnraidReaper();
|
|
326
|
+
if (fs.existsSync(keep))
|
|
327
|
+
fs.rmdirSync(keep);
|
|
328
|
+
this.#directory(CGROUP);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
for (let attempt = 1; !fs.existsSync(keep); attempt += 1) {
|
|
332
|
+
try {
|
|
333
|
+
this.#directory(CGROUP);
|
|
334
|
+
fs.mkdirSync(keep, { mode: 0o700 });
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
if (error.code !== "ENOENT" || attempt >= 5)
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
this.#directory(CGROUP);
|
|
342
|
+
}
|
|
343
|
+
/** Whether the installed package's gateway understands the keep child (0.1.0-alpha.837+). */
|
|
344
|
+
#keepSupported() {
|
|
345
|
+
const installation = this.#p(`${ROOT}/package/dist/heart/daemon/sanctuary-authority-installation.js`);
|
|
346
|
+
return fs.existsSync(installation) && fs.readFileSync(installation, "utf8").includes(sanctuary_authority_installation_1.SANCTUARY_CGROUP_KEEP);
|
|
347
|
+
}
|
|
348
|
+
#freezeUnraidReaper() {
|
|
349
|
+
const pidFile = this.#p("/run/cgroup2-unraid.pid");
|
|
350
|
+
if (!fs.existsSync(pidFile))
|
|
351
|
+
return;
|
|
352
|
+
const pid = Number(fs.readFileSync(pidFile, "utf8").trim());
|
|
353
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
354
|
+
return;
|
|
355
|
+
try {
|
|
356
|
+
process.kill(pid, "SIGSTOP");
|
|
357
|
+
}
|
|
358
|
+
catch { /* not running: nothing to hold */ }
|
|
359
|
+
}
|
|
360
|
+
#runtimeDirectories() {
|
|
361
|
+
for (const directory of [this.#epochRoot(), STAGING])
|
|
362
|
+
this.#directory(directory);
|
|
363
|
+
this.#cgroup();
|
|
364
|
+
this.#directory(SOCKET, 0o750, this.#socketGid);
|
|
365
|
+
// Add only the required controllers; never replace another owner's controls.
|
|
366
|
+
for (const root of ["/sys/fs/cgroup", CGROUP]) {
|
|
367
|
+
const control = this.#p(`${root}/cgroup.subtree_control`);
|
|
368
|
+
if (fs.existsSync(control)) {
|
|
369
|
+
const enabled = fs.readFileSync(control, "utf8").trim().split(/\s+/u);
|
|
370
|
+
const missing = ["cpu", "memory", "pids"].filter((name) => !enabled.includes(name));
|
|
371
|
+
if (missing.length)
|
|
372
|
+
fs.writeFileSync(control, missing.map((name) => `+${name}`).join(" "));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
#stage(hostExecution = true) {
|
|
377
|
+
this.#migration();
|
|
378
|
+
this.#runtimeDirectories();
|
|
379
|
+
this.#verifyPackage(`${ROOT}/incoming-package`, hostExecution);
|
|
380
|
+
const manifest = JSON.parse(this.#private(`${ROOT}/package-manifest.json`, 0o600, 8 * 1024 * 1024));
|
|
381
|
+
this.#directory(`${ROOT}/package`);
|
|
382
|
+
for (const [relative, pin] of Object.entries(manifest.files)) {
|
|
383
|
+
const destination = `${ROOT}/package/${relative}`;
|
|
384
|
+
this.#directory(path.dirname(destination));
|
|
385
|
+
if (fs.existsSync(this.#p(destination))) {
|
|
386
|
+
const stat = fs.lstatSync(this.#p(destination));
|
|
387
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.uid !== this.#uid || stat.gid !== this.#gid || (stat.mode & 0o7777) !== pin.mode || fs.realpathSync(this.#p(destination)) !== this.#p(destination))
|
|
388
|
+
throw new Error("Sanctuary installed package destination is unsafe");
|
|
389
|
+
if (digest(fs.readFileSync(this.#p(destination))) !== pin.digest)
|
|
390
|
+
throw new Error("Sanctuary installed package conflicts with its pin");
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
fs.copyFileSync(this.#p(`${ROOT}/incoming-package/${relative}`), this.#p(destination), fs.constants.COPYFILE_EXCL);
|
|
394
|
+
}
|
|
395
|
+
fs.chmodSync(this.#p(destination), pin.mode);
|
|
396
|
+
}
|
|
397
|
+
this.#verifyPackage(undefined, hostExecution);
|
|
398
|
+
const { text: boot, mode: bootMode } = this.#bootScript();
|
|
399
|
+
if (boot.split("\n").some((line) => line.includes("ouro-authority") && !KNOWN_BOOT_LINES.includes(line)))
|
|
400
|
+
throw new Error("Sanctuary boot ownership is ambiguous");
|
|
401
|
+
this.#write(BOOT, fs.readFileSync(this.#p(`${ROOT}/package/deploy/unraid/sanctuary-authority-service.sh`), "utf8"));
|
|
402
|
+
if (!boot.split("\n").some((line) => BOOT_OWNERS.includes(line)))
|
|
403
|
+
this.#write("/boot/config/go", `${boot}${boot.endsWith("\n") ? "" : "\n"}${BOOT_LINE}\n`, bootMode);
|
|
404
|
+
this.#write(`${this.#epochRoot()}/stage.json`, JSON.stringify({ packageDigest: this.#request.packageDigest }));
|
|
405
|
+
}
|
|
406
|
+
#epoch() {
|
|
407
|
+
const epoch = (0, sanctuary_authority_epoch_1.readSanctuaryAuthorityEpoch)(this.#p(this.#epochRoot()), { expectedUid: this.#uid, expectedGid: this.#gid });
|
|
408
|
+
if (epoch.epochId !== this.#request.epochId || epoch.botId !== this.#request.botId || epoch.ownerUserId !== this.#request.ownerUserId
|
|
409
|
+
|| epoch.ownerChatId !== this.#request.ownerChatId || epoch.packageDigest !== this.#request.packageDigest
|
|
410
|
+
|| epoch.predecessorCursor !== this.#migration().predecessorCursor)
|
|
411
|
+
throw new Error("Sanctuary root epoch identity changed");
|
|
412
|
+
return epoch;
|
|
413
|
+
}
|
|
414
|
+
#rotated() {
|
|
415
|
+
if (!fs.existsSync(this.#p(`${this.#epochRoot()}/epoch.json`)))
|
|
416
|
+
return false;
|
|
417
|
+
if (this.#epoch().state !== "prepared")
|
|
418
|
+
throw new Error("Sanctuary root epoch is retired");
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
async #rotate(hostExecution = true) {
|
|
422
|
+
if (!this.#frozen() || !this.#staged(hostExecution))
|
|
423
|
+
throw new Error("Sanctuary root installation must be frozen and staged");
|
|
424
|
+
const token = this.#private(`${ROOT}/incoming-token`).trim();
|
|
425
|
+
this.#write(`${this.#epochRoot()}/current-token`, token);
|
|
426
|
+
await (0, sanctuary_authority_epoch_1.prepareSanctuaryAuthorityEpoch)({
|
|
427
|
+
root: this.#p(this.#epochRoot()), epochId: this.#request.epochId, tokenPath: this.#p(`${this.#epochRoot()}/current-token`),
|
|
428
|
+
previousTokenPath: this.#p(`${this.#epochRoot()}/previous-token`), botId: this.#request.botId,
|
|
429
|
+
ownerUserId: this.#request.ownerUserId, ownerChatId: this.#request.ownerChatId,
|
|
430
|
+
predecessorCursor: this.#migration().predecessorCursor, packageDigest: this.#request.packageDigest,
|
|
431
|
+
}, {
|
|
432
|
+
expectedUid: this.#uid, expectedGid: this.#gid, now: () => new Date().toISOString(),
|
|
433
|
+
probe: async (value) => {
|
|
434
|
+
const response = await fetch(`https://api.telegram.org/bot${value}/getMe`, { signal: AbortSignal.timeout(15_000) });
|
|
435
|
+
const body = await response.json();
|
|
436
|
+
return { status: response.status, botId: body.ok === true && Number.isSafeInteger(body.result?.id) ? String(body.result.id) : null };
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
#gateway() {
|
|
441
|
+
const epoch = this.#epoch();
|
|
442
|
+
return new sanctuary_telegram_authority_gateway_1.FileSanctuaryTelegramAuthorityGateway(this.#p(`${this.#epochRoot()}/agent`), {
|
|
443
|
+
targetHost: "sanctuary", botId: epoch.botId, ownerUserId: epoch.ownerUserId, ownerChatId: epoch.ownerChatId,
|
|
444
|
+
keyId: epoch.epochId, publicKeyDigest: epoch.publicKeyDigest, privateKey: (0, node_crypto_1.createPrivateKey)(this.#private(`${this.#epochRoot()}/issuer.pem`)),
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
#transferred() {
|
|
448
|
+
if (!fs.existsSync(this.#p(`${ROOT}/active.json`)))
|
|
449
|
+
return false;
|
|
450
|
+
if (!fs.existsSync((0, sanctuary_telegram_authority_gateway_1.sanctuaryTelegramAuthorityStatePath)(this.#p(`${this.#epochRoot()}/agent`))))
|
|
451
|
+
return false;
|
|
452
|
+
if (JSON.stringify(JSON.parse(this.#private(`${ROOT}/active.json`))) !== JSON.stringify(this.#configuration()))
|
|
453
|
+
throw new Error("Sanctuary root configuration changed during handoff");
|
|
454
|
+
if (this.#gateway().cursor() !== this.#migration().predecessorCursor)
|
|
455
|
+
throw new Error("Sanctuary root handoff cursor changed");
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
#transfer() {
|
|
459
|
+
if (!this.#frozen() || !this.#rotated())
|
|
460
|
+
throw new Error("Sanctuary root epoch is not ready for cursor handoff");
|
|
461
|
+
this.#gateway().initializeCursor(this.#epoch().predecessorCursor);
|
|
462
|
+
this.#write(`${ROOT}/active.json`, JSON.stringify(this.#configuration()));
|
|
463
|
+
}
|
|
464
|
+
#configuration() {
|
|
465
|
+
const epoch = this.#epoch();
|
|
466
|
+
const program = `${ROOT}/package/dist/heart/daemon/sanctuary-host-supervisor-entry.js`;
|
|
467
|
+
const launcher = `${ROOT}/package/deploy/unraid/sanctuary-host-launcher.sh`;
|
|
468
|
+
const config = {
|
|
469
|
+
schemaVersion: 1, targetHost: "sanctuary", botId: epoch.botId, ownerUserId: epoch.ownerUserId, ownerChatId: epoch.ownerChatId,
|
|
470
|
+
keyId: epoch.epochId, publicKeyDigest: epoch.publicKeyDigest, socketGroupId: this.#socketGid,
|
|
471
|
+
agentRoot: this.#p(`${this.#epochRoot()}/agent`), epochRoot: this.#p(this.#epochRoot()),
|
|
472
|
+
tokenPath: epoch.tokenPath, privateKeyPath: this.#p(`${this.#epochRoot()}/issuer.pem`),
|
|
473
|
+
socketPath: this.#p(`${SOCKET}/authority.sock`), readinessPath: this.#p(`${this.#epochRoot()}/readiness.json`), lockPath: this.#p(`${this.#epochRoot()}/authority.lock`),
|
|
474
|
+
hostStagingRoot: this.#p(STAGING), hostExecutionStateRoot: this.#p(`${this.#epochRoot()}/executions`), hostSupervisorStateRoot: this.#p(`${this.#epochRoot()}/supervisors`),
|
|
475
|
+
hostCgroupRoot: this.#p(CGROUP), hostSupervisorProgramPath: this.#p(program), hostSupervisorProgramDigest: digest(fs.readFileSync(this.#p(program))),
|
|
476
|
+
hostLauncherPath: this.#p(launcher), hostLauncherDigest: digest(fs.readFileSync(this.#p(launcher))),
|
|
477
|
+
hostPrlimitPath: this.#p("/usr/bin/prlimit"), hostPrlimitDigest: this.#request.prlimitDigest,
|
|
478
|
+
hostSetsidPath: this.#p("/usr/bin/setsid"), hostSetsidDigest: this.#request.setsidDigest, hostShellPath: this.#p("/bin/sh"), hostShellDigest: this.#request.shellDigest,
|
|
479
|
+
packageRoot: this.#p(`${ROOT}/package`), packageManifestPath: this.#p(`${ROOT}/package-manifest.json`), packageManifestDigest: this.#request.packageDigest,
|
|
480
|
+
};
|
|
481
|
+
return config;
|
|
482
|
+
}
|
|
483
|
+
#requireStopped() {
|
|
484
|
+
this.#assertNoForeignPoller();
|
|
485
|
+
if (this.#containers().some((container) => container.State.Running))
|
|
486
|
+
throw new Error("Sanctuary resident must be stopped");
|
|
487
|
+
}
|
|
488
|
+
#tokensAbsent() {
|
|
489
|
+
const presence = this.#vault("presence");
|
|
490
|
+
if (!presence || Object.keys(presence).join(",") !== "tokenPresent" || typeof presence.tokenPresent !== "boolean")
|
|
491
|
+
throw new Error("Sanctuary vault presence readback is invalid");
|
|
492
|
+
return !presence.tokenPresent;
|
|
493
|
+
}
|
|
494
|
+
#pid() {
|
|
495
|
+
const lock = `${this.#epochRoot()}/authority.lock`;
|
|
496
|
+
if (!fs.existsSync(this.#p(lock)))
|
|
497
|
+
return null;
|
|
498
|
+
const pid = Number(this.#private(lock));
|
|
499
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
500
|
+
throw new Error("Sanctuary gateway process lock is invalid");
|
|
501
|
+
if (!fs.existsSync(this.#p(`/proc/${pid}`)))
|
|
502
|
+
return null;
|
|
503
|
+
const status = fs.readFileSync(this.#p(`/proc/${pid}/status`), "utf8");
|
|
504
|
+
const command = fs.readFileSync(this.#p(`/proc/${pid}/cmdline`), "utf8");
|
|
505
|
+
const expected = [this.#p("/usr/local/bin/node"), this.#p(`${ROOT}/package/dist/heart/daemon/sanctuary-telegram-authority-entry.js`), "--config", this.#p(`${ROOT}/active.json`), ""].join("\0");
|
|
506
|
+
if (!status.split("\n").includes(`Uid:\t${this.#uid}\t${this.#uid}\t${this.#uid}\t${this.#uid}`) || command !== expected)
|
|
507
|
+
throw new Error("Sanctuary gateway process identity changed");
|
|
508
|
+
return pid;
|
|
509
|
+
}
|
|
510
|
+
async #ready() {
|
|
511
|
+
if (!fs.existsSync(this.#p(`${this.#epochRoot()}/readiness.json`)))
|
|
512
|
+
return false;
|
|
513
|
+
if (this.#pid() === null)
|
|
514
|
+
throw new Error("Sanctuary gateway readiness has no live process");
|
|
515
|
+
const epoch = this.#epoch();
|
|
516
|
+
const readiness = JSON.parse(this.#private(`${this.#epochRoot()}/readiness.json`));
|
|
517
|
+
if (readiness.status !== "ready" || readiness.botId !== epoch.botId || readiness.publicKeyDigest !== epoch.publicKeyDigest)
|
|
518
|
+
throw new Error("Sanctuary gateway readiness identity changed");
|
|
519
|
+
const authority = (0, sanctuary_authority_resident_1.openSanctuaryResidentAuthority)({}, {}, { configPath: this.#p(`${SOCKET}/resident.json`), expectedUid: this.#uid, expectedGid: this.#socketGid });
|
|
520
|
+
try {
|
|
521
|
+
const snapshot = await authority.cursorSnapshot();
|
|
522
|
+
if (snapshot.keyId !== epoch.epochId || snapshot.botId !== epoch.botId || snapshot.publicKeyDigest !== epoch.publicKeyDigest || snapshot.cursor < epoch.predecessorCursor)
|
|
523
|
+
throw new Error("Sanctuary gateway signed readiness changed");
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
finally {
|
|
527
|
+
authority.authorityTransport.api.stop();
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
async #wait(check, timeoutMs = 120_000) {
|
|
531
|
+
const until = Date.now() + timeoutMs;
|
|
532
|
+
while (!await check()) {
|
|
533
|
+
if (Date.now() >= until)
|
|
534
|
+
throw new Error("Sanctuary root lifecycle readback timed out");
|
|
535
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
async #startGateway(boot = false) {
|
|
539
|
+
if (!boot)
|
|
540
|
+
this.#requireStopped();
|
|
541
|
+
this.#verifyPackage(undefined, !boot);
|
|
542
|
+
if (!this.#tokensAbsent() || !this.#rotated())
|
|
543
|
+
throw new Error("Sanctuary root token custody is not exclusive");
|
|
544
|
+
if (this.#pid() === null) {
|
|
545
|
+
// The gateway can exit within seconds of launch (a transient Telegram network error at
|
|
546
|
+
// getMe did, 863 -> 864). Retry the launch with backoff, and when every attempt fails
|
|
547
|
+
// say so now, with the gateway's own output, instead of waiting out the readiness budget.
|
|
548
|
+
// One deadline bounds every launch: a relaunch only gets the budget that remains.
|
|
549
|
+
const deadline = Date.now() + GATEWAY_READY_TIMEOUT_MS;
|
|
550
|
+
for (let attempt = 0;; attempt += 1) {
|
|
551
|
+
const failure = await this.#launchGatewayOnce(deadline - Date.now());
|
|
552
|
+
if (failure === null)
|
|
553
|
+
return;
|
|
554
|
+
if (attempt >= GATEWAY_START_BACKOFF_MS.length || Date.now() + GATEWAY_START_BACKOFF_MS[attempt] >= deadline) {
|
|
555
|
+
throw new Error(`Sanctuary gateway ${failure} before readiness after ${attempt + 1} attempts; gateway.log tail:\n${this.#gatewayLogTail()}`);
|
|
556
|
+
}
|
|
557
|
+
(0, runtime_1.emitNervesEvent)({ level: "warn", component: "daemon", event: "daemon.sanctuary_gateway_start_retry", message: "Sanctuary gateway exited before readiness; retrying launch", meta: { attempt: attempt + 1, delayMs: GATEWAY_START_BACKOFF_MS[attempt], failure } });
|
|
558
|
+
await new Promise((resolve) => setTimeout(resolve, GATEWAY_START_BACKOFF_MS[attempt]));
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
else
|
|
562
|
+
await this.#wait(() => this.#ready(), GATEWAY_READY_TIMEOUT_MS);
|
|
563
|
+
}
|
|
564
|
+
/** One launch: resolves null once the gateway is ready, or "exited"/"failed to launch" if the process ends first. */
|
|
565
|
+
async #launchGatewayOnce(budgetMs) {
|
|
566
|
+
this.#remove(`${this.#epochRoot()}/readiness.json`);
|
|
567
|
+
const log = this.#gatewayLog();
|
|
568
|
+
let child;
|
|
569
|
+
try {
|
|
570
|
+
child = (0, node_child_process_1.spawn)(this.#p("/usr/local/bin/node"), [this.#p(`${ROOT}/package/dist/heart/daemon/sanctuary-telegram-authority-entry.js`), "--config", this.#p(`${ROOT}/active.json`)], {
|
|
571
|
+
cwd: "/", env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" }, detached: true, stdio: ["ignore", log, log],
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
finally {
|
|
575
|
+
fs.closeSync(log);
|
|
576
|
+
}
|
|
577
|
+
child.unref();
|
|
578
|
+
let ended = null;
|
|
579
|
+
child.once("error", () => { ended = "failed to launch"; });
|
|
580
|
+
child.once("exit", (code, signal) => { ended = `exited (${signal ?? `code ${code}`})`; });
|
|
581
|
+
let readiness = "ready";
|
|
582
|
+
await this.#wait(async () => {
|
|
583
|
+
if (ended !== null) {
|
|
584
|
+
readiness = "ended";
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
return this.#ready();
|
|
588
|
+
}, budgetMs);
|
|
589
|
+
return readiness === "ended" ? ended : null;
|
|
590
|
+
}
|
|
591
|
+
/** The last of gateway.log, with anything token-shaped masked, for an error message. */
|
|
592
|
+
#gatewayLogTail() {
|
|
593
|
+
const file = this.#p(`${ROOT}/gateway.log`);
|
|
594
|
+
if (!fs.existsSync(file))
|
|
595
|
+
return "(no gateway.log)";
|
|
596
|
+
const size = fs.statSync(file).size;
|
|
597
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
598
|
+
try {
|
|
599
|
+
const length = Math.min(size, GATEWAY_LOG_READ_BYTES);
|
|
600
|
+
const buffer = Buffer.alloc(length);
|
|
601
|
+
fs.readSync(fd, buffer, 0, length, size - length);
|
|
602
|
+
return buffer.toString("utf8").replace(/[0-9]{5,}:[A-Za-z0-9_-]{20,}/gu, "[token]").slice(-GATEWAY_LOG_TAIL_CHARS).trim() || "(gateway.log is empty)";
|
|
603
|
+
}
|
|
604
|
+
finally {
|
|
605
|
+
fs.closeSync(fd);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
/** The gateway's own output, kept root-only beside the token it guards and capped at two
|
|
609
|
+
* generations. It used to go nowhere, so a gateway that exited at startup said nothing
|
|
610
|
+
* (D-036: finding "host cgroup entry is invalid" took a hand-run gateway). */
|
|
611
|
+
#gatewayLog() {
|
|
612
|
+
const file = this.#p(`${ROOT}/gateway.log`);
|
|
613
|
+
if (fs.existsSync(file) && fs.statSync(file).size > GATEWAY_LOG_MAX_BYTES)
|
|
614
|
+
fs.renameSync(file, `${file}.1`);
|
|
615
|
+
const fd = fs.openSync(file, fs.constants.O_WRONLY | fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_NOFOLLOW, 0o600);
|
|
616
|
+
fs.fchmodSync(fd, 0o600);
|
|
617
|
+
return fd;
|
|
618
|
+
}
|
|
619
|
+
#target() {
|
|
620
|
+
const target = this.#containers().find((container) => container.Name === "/ouro-butler");
|
|
621
|
+
if (!target || target.Image !== this.#transaction.targetImageId || target.Config.User !== "10001:10001"
|
|
622
|
+
|| target.Config.Env.some((entry) => /telegram.*token|bot.*token/iu.test(entry))
|
|
623
|
+
|| target.Mounts.filter((mount) => mount.Source === SOCKET && mount.Destination === SOCKET && mount.RW === false).length !== 1
|
|
624
|
+
|| target.Mounts.some((mount) => mount.Source === "/var/run/docker.sock" || mount.Source.startsWith(ROOT)))
|
|
625
|
+
throw new Error("Sanctuary target resident is not tokenless and gateway-mounted");
|
|
626
|
+
return target;
|
|
627
|
+
}
|
|
628
|
+
#configured() {
|
|
629
|
+
if (!fs.existsSync(this.#p(`${this.#epochRoot()}/configured.json`)))
|
|
630
|
+
return false;
|
|
631
|
+
this.#record(`${this.#epochRoot()}/configured.json`, { epochId: this.#request.epochId, targetImageId: this.#transaction.targetImageId });
|
|
632
|
+
this.#target();
|
|
633
|
+
return this.#tokensAbsent() && this.#directAutostartDisabled();
|
|
634
|
+
}
|
|
635
|
+
async #configure() {
|
|
636
|
+
this.#requireStopped();
|
|
637
|
+
this.#target();
|
|
638
|
+
if (!this.#tokensAbsent() || !this.#directAutostartDisabled() || !await this.#ready())
|
|
639
|
+
throw new Error("Sanctuary gateway must be ready and direct autostart disabled before configuring the resident");
|
|
640
|
+
this.#write(`${this.#epochRoot()}/configured.json`, JSON.stringify({ epochId: this.#request.epochId, targetImageId: this.#transaction.targetImageId }));
|
|
641
|
+
}
|
|
642
|
+
async #startResident() {
|
|
643
|
+
this.#assertNoForeignPoller();
|
|
644
|
+
if (!this.#configured() || !await this.#ready())
|
|
645
|
+
throw new Error("Sanctuary gateway must be ready before resident startup");
|
|
646
|
+
if (this.#containers().some((container) => container.Name !== "/ouro-butler" && container.State.Running))
|
|
647
|
+
throw new Error("Sanctuary rollback resident is running");
|
|
648
|
+
if (!this.#target().State.Running)
|
|
649
|
+
this.#docker(["start", "ouro-butler"]);
|
|
650
|
+
await this.#wait(() => this.#target().State.Running && this.#target().State.Health?.Status === "healthy");
|
|
651
|
+
}
|
|
652
|
+
async #active() {
|
|
653
|
+
if (!fs.existsSync(this.#p(`${ROOT}/activation.json`)))
|
|
654
|
+
return false;
|
|
655
|
+
this.#record(`${ROOT}/activation.json`, { ...this.#transaction, state: "active", epochId: this.#request.epochId });
|
|
656
|
+
return this.#configured() && this.#target().State.Running && this.#target().State.Health?.Status === "healthy" && await this.#ready();
|
|
657
|
+
}
|
|
658
|
+
async #activate() {
|
|
659
|
+
if (!this.#configured() || !this.#target().State.Running || this.#target().State.Health?.Status !== "healthy" || !await this.#ready())
|
|
660
|
+
throw new Error("Sanctuary activation readback failed");
|
|
661
|
+
this.#write(`${ROOT}/activation.json`, JSON.stringify({ ...this.#transaction, state: "active", epochId: this.#request.epochId }));
|
|
662
|
+
}
|
|
663
|
+
#retiredProcess() {
|
|
664
|
+
const retirement = `${this.#epochRoot()}/retirement.json`;
|
|
665
|
+
if (!fs.existsSync(this.#p(retirement)))
|
|
666
|
+
return false;
|
|
667
|
+
const proof = JSON.parse(this.#private(retirement));
|
|
668
|
+
const epoch = this.#epoch();
|
|
669
|
+
if (proof.schemaVersion !== 1 || proof.keyId !== epoch.epochId || proof.publicKeyDigest !== epoch.publicKeyDigest || proof.quiescent !== true
|
|
670
|
+
|| !Number.isSafeInteger(proof.cursor) || proof.cursor < epoch.predecessorCursor
|
|
671
|
+
|| (0, sanctuary_authority_installation_1.sanctuaryCgroupChildren)(this.#p(CGROUP)).length !== 0)
|
|
672
|
+
throw new Error("Sanctuary gateway retirement cleanup is unproven");
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
async #retireProcess() {
|
|
676
|
+
this.#requireStopped();
|
|
677
|
+
if (this.#retiredProcess())
|
|
678
|
+
return;
|
|
679
|
+
// An interrupted installation may never have published an epoch/config.
|
|
680
|
+
// Complete only the root prerequisites needed to retire it; never start
|
|
681
|
+
// ingress or a resident while recovering this rollback intent.
|
|
682
|
+
if (!fs.existsSync(this.#p(`${this.#epochRoot()}/epoch.json`))) {
|
|
683
|
+
if (!this.#staged(false))
|
|
684
|
+
this.#stage(false);
|
|
685
|
+
await this.#rotate(false);
|
|
686
|
+
}
|
|
687
|
+
if (!fs.existsSync(this.#p(`${ROOT}/active.json`)) || JSON.parse(this.#private(`${ROOT}/active.json`)).keyId !== this.#request.epochId)
|
|
688
|
+
this.#transfer();
|
|
689
|
+
this.#verifyPackage(undefined, false);
|
|
690
|
+
const pid = this.#pid();
|
|
691
|
+
if (pid !== null) {
|
|
692
|
+
process.kill(pid, "SIGUSR2");
|
|
693
|
+
await this.#wait(() => this.#retiredProcess(), 1_020_000);
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
(0, node_child_process_1.execFileSync)(this.#p("/usr/local/bin/node"), [this.#p(`${ROOT}/package/dist/heart/daemon/sanctuary-telegram-authority-entry.js`), "--config", this.#p(`${ROOT}/active.json`), "--retire-only"], {
|
|
697
|
+
encoding: "utf8", timeout: 1_020_000, stdio: ["ignore", "pipe", "pipe"], env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" }, cwd: "/",
|
|
698
|
+
});
|
|
699
|
+
if (!this.#retiredProcess())
|
|
700
|
+
throw new Error("Sanctuary gateway retirement readback failed");
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
async #stopGateway() {
|
|
704
|
+
if (!this.#retiredProcess())
|
|
705
|
+
throw new Error("Sanctuary gateway must retire before stopping");
|
|
706
|
+
const pid = this.#pid();
|
|
707
|
+
if (pid !== null) {
|
|
708
|
+
process.kill(pid, "SIGTERM");
|
|
709
|
+
await this.#wait(() => this.#pid() === null);
|
|
710
|
+
}
|
|
711
|
+
this.#remove(`${this.#epochRoot()}/readiness.json`);
|
|
712
|
+
}
|
|
713
|
+
#endEpoch() {
|
|
714
|
+
if (!this.#retiredProcess() || this.#pid() !== null)
|
|
715
|
+
throw new Error("Sanctuary gateway must retire and stop before ending its epoch");
|
|
716
|
+
const proof = JSON.parse(this.#private(`${this.#epochRoot()}/retirement.json`));
|
|
717
|
+
(0, sanctuary_authority_epoch_1.retireSanctuaryAuthorityEpoch)(this.#p(this.#epochRoot()), { expectedUid: this.#uid, expectedGid: this.#gid, quiescent: true, cursor: proof.cursor });
|
|
718
|
+
}
|
|
719
|
+
#restoredToken() {
|
|
720
|
+
if (this.#epoch().state !== "retired" || !fs.existsSync(this.#p(`${this.#epochRoot()}/restored.json`))
|
|
721
|
+
|| !fs.existsSync(this.#p(`${this.#epochRoot()}/handoff.json`)) || fs.existsSync(this.#p(`${this.#epochRoot()}/current-token`)) || fs.existsSync(this.#p(`${ROOT}/incoming-token`)))
|
|
722
|
+
return false;
|
|
723
|
+
this.#record(`${this.#epochRoot()}/restored.json`, { tokenDigest: this.#epoch().tokenDigest, cursor: this.#epoch().terminalCursor });
|
|
724
|
+
const token = this.#vault("snapshot");
|
|
725
|
+
return digest(token.token) === this.#epoch().tokenDigest && new telegram_client_1.FileTelegramOffsetStore(this.#p(OFFSET)).load() >= this.#epoch().terminalCursor;
|
|
726
|
+
}
|
|
727
|
+
#restoreToken() {
|
|
728
|
+
this.#requireStopped();
|
|
729
|
+
const epoch = this.#epoch();
|
|
730
|
+
if (epoch.state !== "retired" || this.#pid() !== null || !this.#retiredProcess())
|
|
731
|
+
throw new Error("Sanctuary epoch must retire before token handoff");
|
|
732
|
+
if (this.#restoredToken())
|
|
733
|
+
return;
|
|
734
|
+
const token = fs.existsSync(this.#p(`${this.#epochRoot()}/current-token`))
|
|
735
|
+
? this.#private(`${this.#epochRoot()}/current-token`).trim() : this.#vault("snapshot").token;
|
|
736
|
+
if (digest(token) !== epoch.tokenDigest)
|
|
737
|
+
throw new Error("Sanctuary current token handoff identity changed");
|
|
738
|
+
this.#vault("restore", { token, botId: epoch.botId, ownerUserId: epoch.ownerUserId, ownerChatId: epoch.ownerChatId });
|
|
739
|
+
new telegram_client_1.FileTelegramOffsetStore(this.#p(OFFSET)).save(epoch.terminalCursor);
|
|
740
|
+
// Restore the original resident ownership; root must not strand a root-owned cursor.
|
|
741
|
+
fs.chownSync(this.#p(OFFSET), this.#uid === 0 ? 10001 : this.#uid, this.#uid === 0 ? 10001 : this.#gid);
|
|
742
|
+
this.#write(`${this.#epochRoot()}/restored.json`, JSON.stringify({ tokenDigest: epoch.tokenDigest, cursor: epoch.terminalCursor }));
|
|
743
|
+
const restored = this.#vault("snapshot");
|
|
744
|
+
if (digest(restored.token) !== epoch.tokenDigest || new telegram_client_1.FileTelegramOffsetStore(this.#p(OFFSET)).load() !== epoch.terminalCursor)
|
|
745
|
+
throw new Error("Sanctuary token and cursor handoff readback failed");
|
|
746
|
+
(0, sanctuary_authority_epoch_1.releaseSanctuaryAuthorityToken)(this.#p(this.#epochRoot()), { expectedUid: this.#uid, expectedGid: this.#gid, tokenDigest: epoch.tokenDigest, cursor: epoch.terminalCursor });
|
|
747
|
+
if (fs.existsSync(this.#p(`${ROOT}/incoming-token`))) {
|
|
748
|
+
if (digest(this.#private(`${ROOT}/incoming-token`).trim()) !== epoch.tokenDigest)
|
|
749
|
+
throw new Error("Sanctuary incoming token changed during handoff");
|
|
750
|
+
this.#remove(`${ROOT}/incoming-token`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
#rollback() {
|
|
754
|
+
const resident = this.#containers().find((container) => container.Name === "/ouro-butler");
|
|
755
|
+
if (!resident || resident.Image !== this.#transaction.rollbackImageId || resident.Config.User !== "10001:10001"
|
|
756
|
+
|| resident.Mounts.some((mount) => mount.Source === SOCKET || mount.Destination === SOCKET))
|
|
757
|
+
throw new Error("Sanctuary rollback image or socket mount has not been restored");
|
|
758
|
+
return resident;
|
|
759
|
+
}
|
|
760
|
+
async #restoreResident() {
|
|
761
|
+
this.#assertNoForeignPoller();
|
|
762
|
+
if (!this.#restoredToken() || this.#pid() !== null)
|
|
763
|
+
throw new Error("Sanctuary rollback token handoff is incomplete");
|
|
764
|
+
const resident = this.#rollback();
|
|
765
|
+
if (!resident.State.Running)
|
|
766
|
+
this.#docker(["start", "ouro-butler"]);
|
|
767
|
+
await this.#wait(() => this.#rollback().State.Health?.Status === "healthy" && this.#rollback().State.Running);
|
|
768
|
+
}
|
|
769
|
+
#rollbackVerified() {
|
|
770
|
+
return fs.existsSync(this.#p(`${this.#epochRoot()}/rollback.json`)) && this.#restoredToken() && this.#pid() === null && this.#rollback().State.Running && this.#rollback().State.Health?.Status === "healthy";
|
|
771
|
+
}
|
|
772
|
+
#verifyRollback() {
|
|
773
|
+
if (!this.#restoredToken() || this.#pid() !== null || !this.#rollback().State.Running || this.#rollback().State.Health?.Status !== "healthy")
|
|
774
|
+
throw new Error("Sanctuary rollback readback failed");
|
|
775
|
+
this.#write(`${this.#epochRoot()}/rollback.json`, JSON.stringify({ epochId: this.#request.epochId, state: "retired" }));
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Upgrade the installed authority in place to a reviewed package and resident image.
|
|
779
|
+
* The epoch (token, issuer, cursor, gateway state) is kept; only the package, its
|
|
780
|
+
* pins and the resident image change. Resumable from its journal; any failure is
|
|
781
|
+
* left for `rollbackUpgrade` (or the next boot) to undo exactly.
|
|
782
|
+
*/
|
|
783
|
+
async upgrade(input) {
|
|
784
|
+
if (!DIGEST.test(input.targetImageId) || !IMAGE_REFERENCE.test(input.imageReference)
|
|
785
|
+
|| (input.failAfter !== undefined && !exports.SANCTUARY_UPGRADE_STEPS.includes(input.failAfter)))
|
|
786
|
+
throw new Error("Sanctuary upgrade request is invalid");
|
|
787
|
+
if (fs.existsSync(this.#p(TEMPLATE_JOURNAL)))
|
|
788
|
+
throw new Error("Finish the pending installation before upgrading");
|
|
789
|
+
let journal = fs.existsSync(this.#p(UPGRADE)) ? this.#upgradeJournal() : this.#beginUpgrade(input);
|
|
790
|
+
// A rollback that stopped partway has already restored some records; resuming
|
|
791
|
+
// forward from its step list would act on a predecessor that the list calls switched.
|
|
792
|
+
if (journal.rollingBack)
|
|
793
|
+
throw new Error("A Sanctuary upgrade rollback is pending; run upgrade-rollback to finish it");
|
|
794
|
+
if (journal.to.imageId !== input.targetImageId || journal.to.imageReference !== input.imageReference)
|
|
795
|
+
throw new Error("A different Sanctuary upgrade is pending; roll it back first");
|
|
796
|
+
const target = () => new _a({ targetImageId: journal.to.imageId, rollbackImageId: journal.from.imageId }, this.#options);
|
|
797
|
+
const steps = {
|
|
798
|
+
stop: () => this.#halt(),
|
|
799
|
+
switch: async () => { await this.#halt(); this.#switchPackage(journal); },
|
|
800
|
+
resident: () => this.#recreateResident(journal.to.imageReference, journal.to.imageId),
|
|
801
|
+
migrate: () => target().#bundle(["--operation", "migrate", "--rollback-image-id", journal.from.imageId, "--target-image-id", journal.to.imageId]),
|
|
802
|
+
start: () => target().#startUpgraded(),
|
|
803
|
+
};
|
|
804
|
+
for (const step of exports.SANCTUARY_UPGRADE_STEPS) {
|
|
805
|
+
if (!journal.completed.includes(step)) {
|
|
806
|
+
await steps[step]();
|
|
807
|
+
journal = { ...journal, completed: [...journal.completed, step] };
|
|
808
|
+
this.#write(UPGRADE, JSON.stringify(journal));
|
|
809
|
+
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.sanctuary_upgrade_step", message: "Sanctuary upgrade step completed", meta: { step, to: journal.to.imageReference } });
|
|
810
|
+
}
|
|
811
|
+
if (input.failAfter === step)
|
|
812
|
+
throw new Error(`Sanctuary upgrade rehearsal stopped after ${step}`);
|
|
813
|
+
}
|
|
814
|
+
// A held upgrade is live but uncommitted: its journal and the predecessor's records stay, so `upgrade-rollback`
|
|
815
|
+
// still undoes it exactly. Running `upgrade` again without the hold commits it. A completed upgrade cannot be rolled back.
|
|
816
|
+
if (input.holdCommit)
|
|
817
|
+
return;
|
|
818
|
+
target().#bundle(["--operation", "commit"]);
|
|
819
|
+
// A copy of the live token must not linger as if it were a fresh rotation (D-044).
|
|
820
|
+
this.#remove(`${ROOT}/incoming-token`);
|
|
821
|
+
this.#remove(UPGRADE);
|
|
822
|
+
this.#removeTree(PREVIOUS);
|
|
823
|
+
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.sanctuary_upgraded", message: "Sanctuary authority upgraded in place", meta: { from: journal.from.imageReference, to: journal.to.imageReference } });
|
|
824
|
+
}
|
|
825
|
+
/** Undo a pending in-place upgrade exactly, back to the recorded predecessor. */
|
|
826
|
+
async rollbackUpgrade() {
|
|
827
|
+
if (!fs.existsSync(this.#p(UPGRADE)))
|
|
828
|
+
return false;
|
|
829
|
+
let journal = this.#upgradeJournal();
|
|
830
|
+
if (!journal.rollingBack) {
|
|
831
|
+
journal = { ...journal, rollingBack: true };
|
|
832
|
+
this.#write(UPGRADE, JSON.stringify(journal));
|
|
833
|
+
}
|
|
834
|
+
await this.#halt();
|
|
835
|
+
if (journal.completed.includes("switch")) {
|
|
836
|
+
// A migration interrupted before the journal recorded it may or may not be
|
|
837
|
+
// pending; only a recorded migration must roll back. Once the bundle is back,
|
|
838
|
+
// record that so a retried rollback does not demand it again.
|
|
839
|
+
try {
|
|
840
|
+
this.#bundle(["--operation", "finalize-rollback"]);
|
|
841
|
+
}
|
|
842
|
+
catch (error) {
|
|
843
|
+
if (journal.completed.includes("migrate"))
|
|
844
|
+
throw error;
|
|
845
|
+
}
|
|
846
|
+
journal = { ...journal, completed: journal.completed.filter((step) => step !== "migrate") };
|
|
847
|
+
this.#write(UPGRADE, JSON.stringify(journal));
|
|
848
|
+
}
|
|
849
|
+
if (fs.existsSync(this.#p(`${PREVIOUS}/package`))) {
|
|
850
|
+
this.#removeTree(`${ROOT}/package`);
|
|
851
|
+
fs.renameSync(this.#p(`${PREVIOUS}/package`), this.#p(`${ROOT}/package`));
|
|
852
|
+
}
|
|
853
|
+
this.#removeTree(NEXT_PACKAGE);
|
|
854
|
+
for (const [name, max] of ROOT_RECORDS)
|
|
855
|
+
this.#write(`${ROOT}/${name}`, this.#private(`${PREVIOUS}/${name}`, 0o600, max));
|
|
856
|
+
(0, sanctuary_authority_epoch_1.rebindSanctuaryAuthorityEpochPackage)(this.#p(this.#epochRoot()), { expectedUid: this.#uid, expectedGid: this.#gid, from: journal.to.packageDigest, to: journal.from.packageDigest });
|
|
857
|
+
for (const name of EPOCH_RECORDS)
|
|
858
|
+
this.#write(`${this.#epochRoot()}/${name}`, this.#private(`${PREVIOUS}/epoch/${name}`));
|
|
859
|
+
const predecessor = new _a({ targetImageId: journal.from.imageId, rollbackImageId: journal.from.rollbackImageId }, this.#options);
|
|
860
|
+
predecessor.#publishBoot();
|
|
861
|
+
if (this.#containers().find((container) => container.Name === "/ouro-butler")?.Image !== journal.from.imageId) {
|
|
862
|
+
predecessor.#recreateResident(journal.from.imageReference, journal.from.imageId);
|
|
863
|
+
}
|
|
864
|
+
await predecessor.#startUpgraded();
|
|
865
|
+
this.#remove(UPGRADE);
|
|
866
|
+
this.#removeTree(PREVIOUS);
|
|
867
|
+
(0, runtime_1.emitNervesEvent)({ level: "warn", component: "daemon", event: "daemon.sanctuary_upgrade_rolled_back", message: "Sanctuary upgrade rolled back to its predecessor", meta: { from: journal.to.imageReference, to: journal.from.imageReference } });
|
|
868
|
+
return true;
|
|
869
|
+
}
|
|
870
|
+
#upgradeJournal() {
|
|
871
|
+
const journal = JSON.parse(this.#private(UPGRADE));
|
|
872
|
+
const sides = [journal?.from, journal?.to];
|
|
873
|
+
if (!journal || journal.schemaVersion !== 1 || journal.epochId !== this.#request.epochId || !Array.isArray(journal.completed)
|
|
874
|
+
|| !journal.completed.every((step) => exports.SANCTUARY_UPGRADE_STEPS.includes(step))
|
|
875
|
+
|| (journal.rollingBack !== undefined && journal.rollingBack !== true)
|
|
876
|
+
|| !sides.every((side) => side && DIGEST.test(side.imageId) && DIGEST.test(side.packageDigest) && IMAGE_REFERENCE.test(side.imageReference))
|
|
877
|
+
|| !DIGEST.test(journal.from.rollbackImageId))
|
|
878
|
+
throw new Error("Sanctuary upgrade journal is invalid");
|
|
879
|
+
return journal;
|
|
880
|
+
}
|
|
881
|
+
#beginUpgrade(input) {
|
|
882
|
+
this.#record(`${ROOT}/activation.json`, { ...this.#transaction, state: "active", epochId: this.#request.epochId });
|
|
883
|
+
const current = this.#target();
|
|
884
|
+
if (input.targetImageId === this.#transaction.targetImageId)
|
|
885
|
+
throw new Error("Sanctuary already runs the requested image");
|
|
886
|
+
if (this.#docker(["image", "inspect", "--format", "{{.Id}}", input.imageReference]).trim() !== input.targetImageId)
|
|
887
|
+
throw new Error("Sanctuary upgrade image identity does not match its reference");
|
|
888
|
+
const request = validateRequest(JSON.parse(this.#private(INCOMING_REQUEST)));
|
|
889
|
+
if (["epochId", "botId", "ownerUserId", "ownerChatId"].some((key) => request[key] !== this.#request[key]))
|
|
890
|
+
throw new Error("Sanctuary upgrade would change the authority identity");
|
|
891
|
+
if (request.packageDigest === this.#request.packageDigest)
|
|
892
|
+
throw new Error("Sanctuary upgrade package is already installed");
|
|
893
|
+
this.#verifyPackage(`${ROOT}/incoming-package`, true, INCOMING_MANIFEST, request);
|
|
894
|
+
this.#removeTree(PREVIOUS);
|
|
895
|
+
this.#directory(PREVIOUS);
|
|
896
|
+
this.#directory(`${PREVIOUS}/epoch`);
|
|
897
|
+
for (const [name, max] of ROOT_RECORDS)
|
|
898
|
+
this.#write(`${PREVIOUS}/${name}`, this.#private(`${ROOT}/${name}`, 0o600, max));
|
|
899
|
+
for (const name of EPOCH_RECORDS)
|
|
900
|
+
this.#write(`${PREVIOUS}/epoch/${name}`, this.#private(`${this.#epochRoot()}/${name}`));
|
|
901
|
+
const journal = {
|
|
902
|
+
schemaVersion: 1, epochId: this.#request.epochId, completed: [],
|
|
903
|
+
from: { imageId: this.#transaction.targetImageId, imageReference: current.Config.Image, packageDigest: this.#request.packageDigest, rollbackImageId: this.#transaction.rollbackImageId },
|
|
904
|
+
to: { imageId: input.targetImageId, imageReference: input.imageReference, packageDigest: request.packageDigest },
|
|
905
|
+
};
|
|
906
|
+
if (!IMAGE_REFERENCE.test(journal.from.imageReference))
|
|
907
|
+
throw new Error("Sanctuary current resident image reference is not a reviewed release");
|
|
908
|
+
this.#write(UPGRADE, JSON.stringify(journal));
|
|
909
|
+
return journal;
|
|
910
|
+
}
|
|
911
|
+
/** Stop the resident, then the gateway, without retiring anything. */
|
|
912
|
+
async #halt() {
|
|
913
|
+
this.#assertNoForeignPoller();
|
|
914
|
+
for (const container of this.#containers())
|
|
915
|
+
if (container.State.Running)
|
|
916
|
+
this.#docker(["stop", "--time", "30", container.Name.slice(1)]);
|
|
917
|
+
if (this.#containers().some((container) => container.State.Running))
|
|
918
|
+
throw new Error("Sanctuary resident did not stop");
|
|
919
|
+
const pid = this.#pid();
|
|
920
|
+
if (pid !== null) {
|
|
921
|
+
process.kill(pid, "SIGTERM");
|
|
922
|
+
await this.#wait(() => this.#pid() === null);
|
|
923
|
+
}
|
|
924
|
+
this.#remove(`${this.#epochRoot()}/readiness.json`);
|
|
925
|
+
}
|
|
926
|
+
#switchPackage(journal) {
|
|
927
|
+
const manifestText = this.#private(INCOMING_MANIFEST, 0o600, 8 * 1024 * 1024);
|
|
928
|
+
if (!fs.existsSync(this.#p(`${PREVIOUS}/package`))) {
|
|
929
|
+
this.#populate(NEXT_PACKAGE, `${ROOT}/incoming-package`, JSON.parse(manifestText));
|
|
930
|
+
fs.renameSync(this.#p(`${ROOT}/package`), this.#p(`${PREVIOUS}/package`));
|
|
931
|
+
}
|
|
932
|
+
if (!fs.existsSync(this.#p(`${ROOT}/package`)))
|
|
933
|
+
fs.renameSync(this.#p(NEXT_PACKAGE), this.#p(`${ROOT}/package`));
|
|
934
|
+
this.#write(`${ROOT}/package-manifest.json`, manifestText);
|
|
935
|
+
this.#write(`${ROOT}/request.json`, this.#private(INCOMING_REQUEST));
|
|
936
|
+
(0, sanctuary_authority_epoch_1.rebindSanctuaryAuthorityEpochPackage)(this.#p(this.#epochRoot()), { expectedUid: this.#uid, expectedGid: this.#gid, from: journal.from.packageDigest, to: journal.to.packageDigest });
|
|
937
|
+
const target = new _a({ targetImageId: journal.to.imageId, rollbackImageId: journal.from.imageId }, this.#options);
|
|
938
|
+
target.#rebindRecords();
|
|
939
|
+
}
|
|
940
|
+
/** Copy a reviewed package into a fresh private tree, pin by pin. */
|
|
941
|
+
#populate(destinationRoot, sourceRoot, manifest) {
|
|
942
|
+
this.#removeTree(destinationRoot);
|
|
943
|
+
this.#directory(destinationRoot);
|
|
944
|
+
for (const [relative, pin] of Object.entries(manifest.files)) {
|
|
945
|
+
const destination = `${destinationRoot}/${relative}`;
|
|
946
|
+
this.#directory(path.dirname(destination));
|
|
947
|
+
fs.copyFileSync(this.#p(`${sourceRoot}/${relative}`), this.#p(destination), fs.constants.COPYFILE_EXCL);
|
|
948
|
+
fs.chmodSync(this.#p(destination), pin.mode);
|
|
949
|
+
if (digest(fs.readFileSync(this.#p(destination))) !== pin.digest)
|
|
950
|
+
throw new Error("Sanctuary upgrade package conflicts with its pin");
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
/** Point every epoch record at this lifecycle's (new) package and image, keeping the cursor. */
|
|
954
|
+
#rebindRecords() {
|
|
955
|
+
const migration = JSON.parse(this.#private(`${this.#epochRoot()}/migration.json`));
|
|
956
|
+
if (!Number.isSafeInteger(migration.predecessorCursor) || migration.predecessorCursor < 0)
|
|
957
|
+
throw new Error("Sanctuary root migration cursor is invalid");
|
|
958
|
+
this.#write(`${this.#epochRoot()}/migration.json`, JSON.stringify({ ...this.plan(), ...this.#transaction, predecessorCursor: migration.predecessorCursor }));
|
|
959
|
+
this.#write(`${this.#epochRoot()}/stage.json`, JSON.stringify({ packageDigest: this.#request.packageDigest }));
|
|
960
|
+
this.#write(`${this.#epochRoot()}/configured.json`, JSON.stringify({ epochId: this.#request.epochId, targetImageId: this.#transaction.targetImageId }));
|
|
961
|
+
this.#verifyPackage();
|
|
962
|
+
this.#publishBoot();
|
|
963
|
+
this.#write(`${ROOT}/active.json`, JSON.stringify(this.#configuration()));
|
|
964
|
+
this.#write(`${ROOT}/activation.json`, JSON.stringify({ ...this.#transaction, state: "active", epochId: this.#request.epochId }));
|
|
965
|
+
}
|
|
966
|
+
/** Point the boot service script at the installed package. On Unraid the host keeper
|
|
967
|
+
* (not a direct go line) invokes it, so the go file is left as the operator has it. */
|
|
968
|
+
#publishBoot() {
|
|
969
|
+
this.#write(BOOT, fs.readFileSync(this.#p(`${ROOT}/package/deploy/unraid/sanctuary-authority-service.sh`), "utf8"));
|
|
970
|
+
}
|
|
971
|
+
#recreateResident(reference, imageId) {
|
|
972
|
+
if (this.#containers().some((container) => container.Name === "/ouro-butler"))
|
|
973
|
+
this.#docker(["rm", "-f", "ouro-butler"]);
|
|
974
|
+
this.#docker(["create", "--name", "ouro-butler", "--network", "host", "--restart", "unless-stopped", "--user", "10001:10001",
|
|
975
|
+
"-l", "net.unraid.docker.managed=dockerman", "-l", `net.unraid.docker.icon=${ICON}`, "-l", "org.opencontainers.image.source=https://github.com/ourostack/ouroboros",
|
|
976
|
+
"-v", `${RUNTIME}:/home/ouro/.ouro-cli:rw`, "-v", `${BUNDLE}:/home/ouro/AgentBundles/sanctuary.ouro:rw`,
|
|
977
|
+
"-v", `${EVENTS}:/run/ouro-events:ro`, "-v", `${SOCKET}:${SOCKET}:ro`,
|
|
978
|
+
"-v", `${BUNDLE}/psyche:/home/ouro/AgentBundles/sanctuary.ouro/psyche:ro`,
|
|
979
|
+
"-v", `${TRUST}:/etc/ouro/trust/sanctuary:ro`, reference]);
|
|
980
|
+
const resident = this.#containers().find((container) => container.Name === "/ouro-butler");
|
|
981
|
+
if (resident?.Image !== imageId || resident.State.Running)
|
|
982
|
+
throw new Error("Sanctuary resident recreation readback failed");
|
|
983
|
+
}
|
|
984
|
+
/** Run the installed package's bundle migration CLI, then return the bundle to the resident. */
|
|
985
|
+
#bundle(operation) {
|
|
986
|
+
(0, node_child_process_1.execFileSync)(this.#p("/usr/local/bin/node"), [this.#p(`${ROOT}/package/deploy/unraid/migrate-sanctuary-bundle.mjs`),
|
|
987
|
+
"--package-root", this.#p(`${ROOT}/package/deploy/unraid/sanctuary.ouro`), "--agent-root", this.#p(BUNDLE), ...operation], {
|
|
988
|
+
encoding: "utf8", timeout: 300_000, stdio: ["ignore", "pipe", "pipe"], env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" }, cwd: "/",
|
|
989
|
+
});
|
|
990
|
+
(0, node_child_process_1.execFileSync)("/bin/chown", ["-R", "10001:10001", this.#p(BUNDLE)], { stdio: "ignore" });
|
|
991
|
+
this.#restoreReplayRoot();
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* The recursive restore above hands the whole bundle to the resident, including state/replay. That directory is the
|
|
995
|
+
* replay gate's trust anchor: the resident only believes a replay window while the directory is root-owned and not
|
|
996
|
+
* writable by it. Put it back under root after every restore (the resident is stopped here), and drop any window
|
|
997
|
+
* file, since the gate writes its own for each run and a file the resident could have written must not be kept as root's.
|
|
998
|
+
*/
|
|
999
|
+
/**
|
|
1000
|
+
* `state/<name>` as a real directory inside this bundle's own state directory, or null (silently when absent, with a
|
|
1001
|
+
* warning when it is a link or resolves anywhere else). The resident owns the bundle during the recursive restore, so
|
|
1002
|
+
* `state` itself may be a link too: compare realpaths before any chown or chmod ever runs.
|
|
1003
|
+
*/
|
|
1004
|
+
#stateChild(name) {
|
|
1005
|
+
const bundle = this.#p(BUNDLE);
|
|
1006
|
+
const directory = path.join(bundle, "state", name);
|
|
1007
|
+
let stat;
|
|
1008
|
+
try {
|
|
1009
|
+
stat = fs.lstatSync(directory);
|
|
1010
|
+
}
|
|
1011
|
+
catch {
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
if (!stat.isDirectory()) {
|
|
1015
|
+
this.#refuseRestore(directory, stat.isSymbolicLink() ? "symlink" : "not a directory");
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
let inside = false;
|
|
1019
|
+
try {
|
|
1020
|
+
inside = fs.realpathSync(directory) === path.join(fs.realpathSync(bundle), "state", name);
|
|
1021
|
+
}
|
|
1022
|
+
catch { /* unresolvable: refused below */ }
|
|
1023
|
+
if (!inside) {
|
|
1024
|
+
this.#refuseRestore(directory, "resolves outside the bundle's state directory");
|
|
1025
|
+
return null;
|
|
1026
|
+
}
|
|
1027
|
+
return directory;
|
|
1028
|
+
}
|
|
1029
|
+
#refuseRestore(target, reason) {
|
|
1030
|
+
(0, runtime_1.emitNervesEvent)({ level: "warn", component: "daemon", event: "daemon.sanctuary_escalation_root_refused", message: "Sanctuary root restore refused a path it will not follow", meta: { target, reason } });
|
|
1031
|
+
}
|
|
1032
|
+
#restoreReplayRoot() {
|
|
1033
|
+
const directory = this.#stateChild("replay");
|
|
1034
|
+
if (!directory)
|
|
1035
|
+
return;
|
|
1036
|
+
// Ownership first: nothing the Butler left in the directory may stop it being put back under root.
|
|
1037
|
+
(0, node_child_process_1.execFileSync)("/bin/chown", ["-h", "0:0", directory], { stdio: "ignore" });
|
|
1038
|
+
fs.chmodSync(directory, 0o755);
|
|
1039
|
+
fs.rmSync(path.join(directory, "window.json"), { force: true, recursive: true });
|
|
1040
|
+
}
|
|
1041
|
+
/** Start the gateway, then the resident, and prove both. */
|
|
1042
|
+
async #startUpgraded() {
|
|
1043
|
+
this.#runtimeDirectories();
|
|
1044
|
+
await this.#startGateway();
|
|
1045
|
+
this.#docker(["start", "ouro-butler"]);
|
|
1046
|
+
await this.#wait(async () => this.#target().State.Running && this.#target().State.Health?.Status === "healthy" && await this.#ready(), 300_000);
|
|
1047
|
+
}
|
|
1048
|
+
#removeTree(absolute) {
|
|
1049
|
+
fs.rmSync(this.#p(absolute), { recursive: true, force: true });
|
|
1050
|
+
}
|
|
1051
|
+
#remove(absolute) {
|
|
1052
|
+
if (!fs.existsSync(this.#p(absolute)))
|
|
1053
|
+
return;
|
|
1054
|
+
this.#private(absolute);
|
|
1055
|
+
fs.unlinkSync(this.#p(absolute));
|
|
1056
|
+
const directory = fs.openSync(path.dirname(this.#p(absolute)), fs.constants.O_RDONLY);
|
|
1057
|
+
try {
|
|
1058
|
+
fs.fsyncSync(directory);
|
|
1059
|
+
}
|
|
1060
|
+
finally {
|
|
1061
|
+
fs.closeSync(directory);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
#directAutostartDisabled() {
|
|
1065
|
+
const file = this.#p("/var/lib/docker/unraid-autostart");
|
|
1066
|
+
const stat = fs.lstatSync(file);
|
|
1067
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== this.#uid || stat.gid !== this.#gid || (stat.mode & 0o7777) !== 0o644)
|
|
1068
|
+
throw new Error("Sanctuary DockerMan autostart state is unsafe");
|
|
1069
|
+
return !fs.readFileSync(file, "utf8").split("\n").some((line) => /^ouro-butler(?:-rollback|-staging|-legacy-evidence)?(?:\s|$)/u.test(line));
|
|
1070
|
+
}
|
|
1071
|
+
#p(absolute) { return `${this.#prefix}${absolute}`; }
|
|
1072
|
+
#bootScript() {
|
|
1073
|
+
const mode = fs.lstatSync(this.#p("/boot/config/go")).mode & 0o7777;
|
|
1074
|
+
if (![0o600, 0o644, 0o700, 0o755].includes(mode))
|
|
1075
|
+
throw new Error("Sanctuary boot script metadata is unsafe");
|
|
1076
|
+
return { text: this.#private("/boot/config/go", mode), mode };
|
|
1077
|
+
}
|
|
1078
|
+
#record(absolute, expected) {
|
|
1079
|
+
const value = JSON.parse(this.#private(absolute));
|
|
1080
|
+
if (!value || Object.keys(value).sort().join(",") !== Object.keys(expected).sort().join(",")
|
|
1081
|
+
|| Object.entries(expected).some(([key, entry]) => value[key] !== entry))
|
|
1082
|
+
throw new Error("Sanctuary root lifecycle record identity changed");
|
|
1083
|
+
}
|
|
1084
|
+
#private(absolute, mode = 0o600, maxBytes = 1024 * 1024) {
|
|
1085
|
+
const file = this.#p(absolute);
|
|
1086
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
1087
|
+
try {
|
|
1088
|
+
const stat = fs.fstatSync(fd);
|
|
1089
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.uid !== this.#uid || stat.gid !== this.#gid || (stat.mode & 0o7777) !== mode
|
|
1090
|
+
|| fs.realpathSync(file) !== file || stat.size < 1 || stat.size > maxBytes)
|
|
1091
|
+
throw new Error("Sanctuary root lifecycle private file is unsafe");
|
|
1092
|
+
return fs.readFileSync(fd, "utf8");
|
|
1093
|
+
}
|
|
1094
|
+
finally {
|
|
1095
|
+
fs.closeSync(fd);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
#write(absolute, value, mode = 0o600) {
|
|
1099
|
+
const file = this.#p(absolute);
|
|
1100
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
1101
|
+
const parent = fs.lstatSync(path.dirname(file));
|
|
1102
|
+
if (parent.uid !== this.#uid || parent.gid !== this.#gid
|
|
1103
|
+
|| !(absolute === "/boot/config/go" ? [0o700, 0o755] : [0o700]).includes(parent.mode & 0o7777)
|
|
1104
|
+
|| fs.realpathSync(path.dirname(file)) !== path.dirname(file))
|
|
1105
|
+
throw new Error("Sanctuary root lifecycle parent is unsafe");
|
|
1106
|
+
// This temporary belongs exclusively to the fenced root transaction.
|
|
1107
|
+
const temporary = `${file}.tmp`;
|
|
1108
|
+
const fd = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_NOFOLLOW, 0o600);
|
|
1109
|
+
let owned = false;
|
|
1110
|
+
try {
|
|
1111
|
+
try {
|
|
1112
|
+
const stat = fs.fstatSync(fd);
|
|
1113
|
+
if (stat.nlink !== 1 || !stat.isFile() || stat.uid !== this.#uid || stat.gid !== this.#gid || ![0o600, mode].includes(stat.mode & 0o7777))
|
|
1114
|
+
throw new Error("Sanctuary root lifecycle temporary is unsafe");
|
|
1115
|
+
owned = true;
|
|
1116
|
+
fs.ftruncateSync(fd, 0);
|
|
1117
|
+
fs.writeFileSync(fd, value);
|
|
1118
|
+
fs.fchmodSync(fd, mode);
|
|
1119
|
+
fs.fsyncSync(fd);
|
|
1120
|
+
}
|
|
1121
|
+
finally {
|
|
1122
|
+
fs.closeSync(fd);
|
|
1123
|
+
}
|
|
1124
|
+
fs.renameSync(temporary, file);
|
|
1125
|
+
}
|
|
1126
|
+
catch (error) {
|
|
1127
|
+
if (owned)
|
|
1128
|
+
fs.unlinkSync(temporary);
|
|
1129
|
+
throw error;
|
|
1130
|
+
}
|
|
1131
|
+
const directory = fs.openSync(path.dirname(file), fs.constants.O_RDONLY);
|
|
1132
|
+
try {
|
|
1133
|
+
fs.fsyncSync(directory);
|
|
1134
|
+
}
|
|
1135
|
+
finally {
|
|
1136
|
+
fs.closeSync(directory);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
#docker(args, input) {
|
|
1140
|
+
return (0, node_child_process_1.execFileSync)("/usr/bin/docker", args, { encoding: "utf8", input, maxBuffer: 1024 * 1024, timeout: 120_000, stdio: ["pipe", "pipe", "pipe"] });
|
|
1141
|
+
}
|
|
1142
|
+
#containers() {
|
|
1143
|
+
const names = this.#docker(["ps", "-a", "--format", "{{json .}}"]).trim().split("\n").filter(Boolean).map((line) => String(JSON.parse(line).Names));
|
|
1144
|
+
return names.filter((name) => ["ouro-butler", "ouro-butler-rollback"].includes(name)).flatMap((name) => JSON.parse(this.#docker(["inspect", name])));
|
|
1145
|
+
}
|
|
1146
|
+
#assertNoForeignPoller() {
|
|
1147
|
+
const names = this.#docker(["ps", "--format", "{{json .}}"]).trim().split("\n").filter(Boolean).map((line) => String(JSON.parse(line).Names));
|
|
1148
|
+
for (const name of names) {
|
|
1149
|
+
if (["ouro-butler", "ouro-butler-rollback"].includes(name))
|
|
1150
|
+
continue;
|
|
1151
|
+
const containers = JSON.parse(this.#docker(["inspect", name]));
|
|
1152
|
+
if (containers.some((container) => container.Mounts.some((mount) => mount.Source === BUNDLE || mount.Source === RUNTIME)))
|
|
1153
|
+
throw new Error("Sanctuary has an unowned possible direct poller");
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
#vault(operation, input) {
|
|
1157
|
+
// The fenced container runs root with every capability dropped, but the
|
|
1158
|
+
// resident credential store is owned by the resident uid (10001), so a bare
|
|
1159
|
+
// cap-dropped root cannot even read it (EACCES on agent.json → surfaces as
|
|
1160
|
+
// "vault owner unavailable"). CAP_DAC_OVERRIDE is restored — the single
|
|
1161
|
+
// capability needed to reach the resident's files, nothing broader.
|
|
1162
|
+
//
|
|
1163
|
+
// bitwarden cannot unlock (or sync) a store it cannot write to, so the store
|
|
1164
|
+
// dir must be writable inside the fence. The two verbs split by intent:
|
|
1165
|
+
// • snapshot / presence are READS: the read-only store is copied into a
|
|
1166
|
+
// throwaway tmpfs, so the fence never mutates resident state.
|
|
1167
|
+
// • remove / restore are WRITES whose whole purpose is to change the vault
|
|
1168
|
+
// (remove-resident-token, rollback restore-token-cursor). They bind the
|
|
1169
|
+
// real ${RUNTIME}/bitwarden writable (rw over the read-only .ouro-cli),
|
|
1170
|
+
// so the edit — and bitwarden's own sync-on-unlock — persists. Copying to
|
|
1171
|
+
// tmpfs here would silently discard the write and fail the step's readback.
|
|
1172
|
+
// `operation` is one of the four fixed literals validated by the CLI.
|
|
1173
|
+
const cli = "/opt/ouro/dist/heart/daemon/sanctuary-authority-root-lifecycle.js";
|
|
1174
|
+
const persists = operation === "remove" || operation === "restore";
|
|
1175
|
+
const bitwarden = persists
|
|
1176
|
+
? ["--mount", `type=bind,src=${RUNTIME}/bitwarden,dst=/home/ouro/.ouro-cli/bitwarden`]
|
|
1177
|
+
: [
|
|
1178
|
+
"--mount", `type=bind,src=${RUNTIME}/bitwarden,dst=/home/ouro/.bw-src,readonly`,
|
|
1179
|
+
"--tmpfs", "/home/ouro/.ouro-cli/bitwarden:rw,nosuid,nodev,noexec,mode=0700",
|
|
1180
|
+
];
|
|
1181
|
+
const prepare = persists ? "" : "cp -r /home/ouro/.bw-src/. /home/ouro/.ouro-cli/bitwarden/ && ";
|
|
1182
|
+
return JSON.parse(this.#docker([
|
|
1183
|
+
"run", "--rm", "-i", "--pull=never", "--network", "host", "--user", "0:0", "--read-only",
|
|
1184
|
+
"--cap-drop=ALL", "--cap-add=DAC_OVERRIDE", "--security-opt=no-new-privileges", "--entrypoint", "/bin/sh",
|
|
1185
|
+
"--mount", `type=bind,src=${RUNTIME},dst=/home/ouro/.ouro-cli,readonly`,
|
|
1186
|
+
...bitwarden,
|
|
1187
|
+
"--mount", `type=bind,src=${BUNDLE},dst=/home/ouro/AgentBundles/sanctuary.ouro,readonly`,
|
|
1188
|
+
"--tmpfs", "/home/ouro/.config:rw,nosuid,nodev,noexec,mode=0700",
|
|
1189
|
+
"--tmpfs", "/tmp:rw,nosuid,nodev,noexec,mode=0700",
|
|
1190
|
+
"--env", `OURO_AUTHORITY_REMOVE_INTERLOCK=${this.#request.epochId}`,
|
|
1191
|
+
this.#transaction.targetImageId, "-c",
|
|
1192
|
+
`${prepare}exec /usr/local/bin/node ${cli} vault ${operation}`,
|
|
1193
|
+
], input === undefined ? undefined : JSON.stringify(input)));
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
exports.SanctuaryAuthorityRootLifecycle = SanctuaryAuthorityRootLifecycle;
|
|
1197
|
+
_a = SanctuaryAuthorityRootLifecycle;
|
|
1198
|
+
/**
|
|
1199
|
+
* Keep the real failure where only root can read it (D-034). Stderr stays generic
|
|
1200
|
+
* because a parse error can quote private bytes; the authority root is as private
|
|
1201
|
+
* as the token it already holds.
|
|
1202
|
+
*/
|
|
1203
|
+
function recordSanctuaryRootLifecycleFailure(error, file = `${ROOT}/lifecycle-failure.log`) {
|
|
1204
|
+
try {
|
|
1205
|
+
fs.appendFileSync(file, `${new Date().toISOString()} ${error instanceof Error ? error.stack : String(error)}\n`, { mode: 0o600 });
|
|
1206
|
+
return ` Details (root-only): ${file}`;
|
|
1207
|
+
}
|
|
1208
|
+
catch {
|
|
1209
|
+
return "";
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
async function runSanctuaryAuthorityRootCli(argv, write = (text) => process.stdout.write(text)) {
|
|
1213
|
+
if (process.getuid() !== 0 || process.getgid() !== 0)
|
|
1214
|
+
throw new Error("Sanctuary root lifecycle requires root");
|
|
1215
|
+
if (argv.length === 2 && argv[0] === "vault") {
|
|
1216
|
+
// `remove` is the one vault verb that can strand the household. It is safe
|
|
1217
|
+
// only inside the install transaction, which has already proven the same
|
|
1218
|
+
// token into root custody at `verify-token-rotation`. Run bare against a
|
|
1219
|
+
// host with no installed root authority it leaves the Butler with no route
|
|
1220
|
+
// to Telegram at all, which is how Sanctuary lost its front door on
|
|
1221
|
+
// 2026-09-21. This is a safety interlock against invoking it outside its
|
|
1222
|
+
// transaction, not a privilege boundary: root can always set the variable,
|
|
1223
|
+
// but it can no longer do this by reaching for a documented verb.
|
|
1224
|
+
if (argv[1] === "remove" && !process.env.OURO_AUTHORITY_REMOVE_INTERLOCK) {
|
|
1225
|
+
throw new Error("Sanctuary vault removal runs only inside the authority install transaction; use docker-man-template-transaction.mjs authority-install");
|
|
1226
|
+
}
|
|
1227
|
+
const input = argv[1] === "restore" ? JSON.parse(fs.readFileSync(0, "utf8")) : undefined;
|
|
1228
|
+
write(`${JSON.stringify(await (0, sanctuary_authority_vault_migration_1.migrateSanctuaryAuthorityVault)(argv[1], input))}\n`);
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
const repin = argv.length === 3 && argv[0] === "repin-execution";
|
|
1232
|
+
const holdCommit = argv[0] === "upgrade" && argv.length === 4 && argv[3] === "--hold-commit";
|
|
1233
|
+
const upgrade = argv[0] === "upgrade" && (argv.length === 3 || holdCommit || (argv.length === 5 && argv[3] === "--fail-after"));
|
|
1234
|
+
const rollback = argv.length === 1 && argv[0] === "upgrade-rollback";
|
|
1235
|
+
if (!repin && !upgrade && !rollback && (argv.length !== 1 || argv[0] !== "boot"))
|
|
1236
|
+
throw new Error("Usage: sanctuary-authority-root-lifecycle <boot|repin-execution <prlimit-sha256> <setsid-sha256>|upgrade <target-image-id> <image-reference> [--fail-after <step>|--hold-commit]|upgrade-rollback|vault snapshot|vault presence|vault remove|vault restore>");
|
|
1237
|
+
// Boot never waits: the host keeper retries it. An operator upgrade waits out a boot in flight.
|
|
1238
|
+
await (0, session_transaction_1.withSessionTurnLease)(TEMPLATE_JOURNAL, async () => {
|
|
1239
|
+
const activationPath = `${ROOT}/activation.json`;
|
|
1240
|
+
if (!fs.existsSync(activationPath)) {
|
|
1241
|
+
if (repin || upgrade || rollback)
|
|
1242
|
+
throw new Error("Sanctuary authority is not active");
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
const activation = JSON.parse(fs.readFileSync(activationPath, "utf8"));
|
|
1246
|
+
const lifecycle = new SanctuaryAuthorityRootLifecycle(activation);
|
|
1247
|
+
if (repin) {
|
|
1248
|
+
lifecycle.repinExecution(argv[1], argv[2]);
|
|
1249
|
+
write('{"repinned":true,"gatewayRestartRequired":true}\n');
|
|
1250
|
+
}
|
|
1251
|
+
else if (upgrade) {
|
|
1252
|
+
await lifecycle.upgrade({ targetImageId: argv[1], imageReference: argv[2], ...(argv[4] ? { failAfter: argv[4] } : {}), ...(holdCommit ? { holdCommit: true } : {}) });
|
|
1253
|
+
write(`${JSON.stringify({ upgraded: argv[2], ...(holdCommit ? { held: true } : {}) })}\n`);
|
|
1254
|
+
}
|
|
1255
|
+
else if (rollback) {
|
|
1256
|
+
write(`${JSON.stringify({ rolledBack: await lifecycle.rollbackUpgrade() })}\n`);
|
|
1257
|
+
}
|
|
1258
|
+
else
|
|
1259
|
+
await lifecycle.boot();
|
|
1260
|
+
}, { timeoutMs: upgrade || rollback ? 600_000 : 0, confinementRoot: "/boot/config/custom/ouro-butler" });
|
|
1261
|
+
}
|
|
1262
|
+
if (process.argv[1] && fs.existsSync(process.argv[1]) && fs.realpathSync(process.argv[1]) === __filename) {
|
|
1263
|
+
void runSanctuaryAuthorityRootCli(process.argv.slice(2)).catch((error) => {
|
|
1264
|
+
process.stderr.write(`Sanctuary root lifecycle failed; inspect the root transaction and repair its failed boundary.${recordSanctuaryRootLifecycleFailure(error)}\n`);
|
|
1265
|
+
process.exitCode = 1;
|
|
1266
|
+
});
|
|
1267
|
+
}
|