@ouro.bot/cli 0.1.0-alpha.83 → 0.1.0-alpha.831
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 +5308 -16
- package/deploy/unraid/Dockerfile +54 -0
- package/deploy/unraid/README.txt +2983 -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-deployment-target.mjs +710 -0
- package/deploy/unraid/sanctuary-safe-rename.py +24 -0
- package/deploy/unraid/sanctuary-unit16-host-broker.mjs +2084 -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/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 +795 -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 +15 -0
- package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +25 -0
- package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +15 -0
- package/deploy/unraid/sanctuary.ouro/state/policy/steward.json +7 -0
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +135 -0
- package/deploy/unraid/sanctuary.xml +28 -0
- package/dist/a2a/card.js +57 -0
- package/dist/a2a/client.js +180 -0
- package/dist/a2a/config.js +50 -0
- package/dist/a2a/delegation-stores.js +56 -0
- package/dist/a2a/did-resolution.js +93 -0
- package/dist/a2a/identity.js +170 -0
- package/dist/a2a/inbound-share.js +107 -0
- package/dist/a2a/mission-result-wire.js +196 -0
- package/dist/a2a/onboarding.js +115 -0
- package/dist/a2a/pin-store.js +132 -0
- package/dist/a2a/seen-ledger.js +120 -0
- package/dist/a2a/server.js +604 -0
- package/dist/a2a/task-store.js +69 -0
- package/dist/a2a/transport.js +45 -0
- package/dist/a2a/types.js +3 -0
- package/dist/arc/attention-types.js +8 -0
- package/dist/arc/cares.js +360 -0
- package/dist/arc/episodes.js +118 -0
- package/dist/arc/evolution.js +490 -0
- package/dist/arc/flight-recorder.js +1034 -0
- package/dist/arc/intentions.js +134 -0
- package/dist/arc/json-store.js +117 -0
- package/dist/arc/obligations.js +386 -0
- package/dist/arc/packets.js +336 -0
- package/dist/arc/presence.js +185 -0
- package/dist/arc/task-lifecycle.js +57 -0
- package/dist/commerce/store.js +755 -0
- package/dist/commerce/types.js +3 -0
- package/dist/heart/active-work.js +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/await-alert.js +148 -0
- package/dist/heart/awaiting/await-expiry.js +143 -0
- package/dist/heart/awaiting/await-loader.js +91 -0
- package/dist/heart/awaiting/await-parser.js +149 -0
- package/dist/heart/awaiting/await-runtime-state.js +111 -0
- package/dist/heart/awaiting/await-scheduler.js +382 -0
- package/dist/heart/background-operations.js +281 -0
- package/dist/heart/bridges/manager.js +137 -17
- package/dist/heart/bridges/store.js +14 -2
- package/dist/heart/bundle-state.js +168 -0
- package/dist/heart/commitments.js +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 +1157 -0
- package/dist/heart/core.js +2169 -581
- package/dist/heart/cross-chat-delivery.js +9 -22
- package/dist/heart/daemon/agent-config-check.js +451 -0
- package/dist/heart/daemon/agent-discovery.js +185 -3
- package/dist/heart/daemon/agent-service.js +542 -0
- package/dist/heart/daemon/agentic-repair.js +547 -0
- package/dist/heart/daemon/await-private-wake.js +59 -0
- package/dist/heart/daemon/bluebubbles-cli-integration.js +71 -0
- package/dist/heart/daemon/bluebubbles-health-diagnostics.js +122 -0
- package/dist/heart/daemon/bluebubbles-host-protocol.js +433 -0
- package/dist/heart/daemon/bluebubbles-host.js +273 -0
- package/dist/heart/daemon/boot-sync-probe.js +197 -0
- package/dist/heart/daemon/cadence.js +180 -0
- package/dist/heart/daemon/cli-defaults.js +854 -0
- package/dist/heart/daemon/cli-desk.js +322 -0
- package/dist/heart/daemon/cli-exec.js +8822 -0
- package/dist/heart/daemon/cli-help.js +716 -0
- package/dist/heart/daemon/cli-parse.js +2771 -0
- package/dist/heart/daemon/cli-render-doctor.js +57 -0
- package/dist/heart/daemon/cli-render.js +777 -0
- package/dist/heart/daemon/cli-types.js +8 -0
- package/dist/heart/daemon/connect-bay.js +323 -0
- package/dist/heart/daemon/container-credential-bootstrap.js +273 -0
- package/dist/heart/daemon/container-healthcheck.js +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 +229 -0
- package/dist/heart/daemon/daemon-bootstrap-startup.js +115 -0
- package/dist/heart/daemon/daemon-cli.js +29 -2349
- package/dist/heart/daemon/daemon-entry.js +930 -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 +1755 -0
- package/dist/heart/daemon/freshness.js +345 -0
- package/dist/heart/daemon/habit-private-wake.js +61 -0
- package/dist/heart/daemon/health-monitor.js +142 -11
- package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
- package/dist/heart/daemon/hooks/bundle-meta.js +135 -1
- package/dist/heart/daemon/http-health-probe.js +80 -0
- package/dist/heart/daemon/human-command-screens.js +234 -0
- package/dist/heart/daemon/human-readiness.js +114 -0
- package/dist/heart/daemon/inner-status.js +78 -0
- package/dist/heart/daemon/interactive-repair.js +394 -0
- package/dist/heart/daemon/launchd.js +37 -8
- package/dist/heart/daemon/log-tailer.js +79 -10
- package/dist/heart/daemon/logs-prune.js +132 -0
- package/dist/heart/daemon/mcp-canary.js +524 -0
- package/dist/heart/daemon/message-router.js +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 +4 -2
- package/dist/heart/daemon/ouro-entry.js +3 -1
- package/dist/heart/daemon/plugin-cli.js +432 -0
- package/dist/heart/daemon/process-manager.js +733 -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 +256 -0
- package/dist/heart/daemon/sanctuary-authority-installation.js +140 -0
- package/dist/heart/daemon/sanctuary-authority-ledger.js +241 -0
- package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +819 -0
- package/dist/heart/daemon/sanctuary-authority-vault-migration.js +79 -0
- package/dist/heart/daemon/sanctuary-bundle-migration.js +729 -0
- package/dist/heart/daemon/sanctuary-host-authority.js +1008 -0
- package/dist/heart/daemon/sanctuary-host-detached-supervisor.js +494 -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 +445 -0
- package/dist/heart/daemon/sanctuary-telegram-authority-gateway.js +585 -0
- package/dist/heart/daemon/sanctuary-telegram-authority-service.js +630 -0
- package/dist/heart/daemon/sense-manager.js +699 -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 +499 -0
- package/dist/heart/daemon/thoughts.js +138 -54
- package/dist/heart/daemon/up-progress.js +366 -0
- package/dist/heart/daemon/vault-items.js +56 -0
- package/dist/heart/delegation.js +1 -4
- package/dist/heart/external-events/router.js +1430 -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 +196 -59
- package/dist/heart/kept-notes.js +289 -0
- package/dist/heart/kicks.js +1 -1
- package/dist/heart/machine-identity.js +161 -0
- package/dist/heart/mail-import-discovery.js +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 +129 -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 +11 -0
- package/dist/heart/orientation-frame.js +345 -0
- package/dist/heart/platform.js +81 -0
- package/dist/heart/primitives.js +1 -1
- package/dist/heart/private-runtime/decision-renderer.js +158 -0
- package/dist/heart/private-runtime/index.js +18 -0
- package/dist/heart/private-runtime/ledger.js +221 -0
- package/dist/heart/private-runtime/policy.js +252 -0
- package/dist/heart/private-runtime/types.js +2 -0
- package/dist/heart/progress-story.js +1 -1
- package/dist/heart/provider-attempt.js +137 -0
- package/dist/heart/provider-binding-resolver.js +273 -0
- package/dist/heart/provider-credentials.js +449 -0
- package/dist/heart/provider-failover.js +311 -0
- package/dist/heart/provider-models.js +96 -0
- package/dist/heart/provider-ping.js +265 -0
- package/dist/heart/provider-readiness-cache.js +119 -0
- package/dist/heart/provider-visibility.js +188 -0
- package/dist/heart/providers/anthropic-token.js +131 -0
- package/dist/heart/providers/anthropic.js +173 -60
- package/dist/heart/providers/azure.js +23 -11
- package/dist/heart/providers/error-classification.js +127 -0
- package/dist/heart/providers/github-copilot.js +34 -38
- package/dist/heart/providers/minimax-vlm.js +189 -0
- package/dist/heart/providers/minimax.js +26 -8
- package/dist/heart/providers/openai-codex-token.js +349 -0
- package/dist/heart/providers/openai-codex.js +55 -40
- package/dist/heart/providers/openai-compatible.js +177 -0
- package/dist/heart/run-ledger.js +247 -0
- package/dist/heart/runtime-capability-check.js +170 -0
- package/dist/heart/runtime-credentials.js +607 -0
- package/dist/heart/runtime-cwd.js +87 -0
- package/dist/heart/sense-truth.js +22 -4
- package/dist/heart/session-activity.js +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/start-of-turn-packet.js +372 -0
- package/dist/heart/steward-policy.js +618 -0
- package/dist/heart/streaming.js +604 -207
- package/dist/heart/structured-output.js +196 -0
- package/dist/heart/sync-classification.js +176 -0
- package/dist/heart/sync.js +449 -0
- package/dist/heart/target-resolution.js +11 -7
- package/dist/heart/tempo.js +93 -0
- package/dist/heart/temporal-view.js +41 -0
- package/dist/heart/timeouts.js +101 -0
- package/dist/heart/tool-activity-callbacks.js +59 -0
- package/dist/heart/tool-approval.js +382 -0
- package/dist/heart/tool-description.js +155 -0
- package/dist/heart/tool-friction.js +55 -0
- package/dist/heart/tool-loop.js +200 -0
- package/dist/heart/turn-context.js +482 -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 +1 -1
- package/dist/heart/versioning/ouro-path-installer.js +432 -0
- package/dist/heart/versioning/ouro-recovery-launcher.js +76 -0
- package/dist/heart/versioning/ouro-version-manager.js +409 -0
- package/dist/heart/{daemon → versioning}/staged-restart.js +40 -8
- package/dist/heart/{daemon → versioning}/update-checker.js +6 -1
- package/dist/heart/versioning/update-hooks.js +154 -0
- package/dist/heart/versioning/version-intent.js +114 -0
- package/dist/heart/work-card.js +380 -0
- package/dist/mailbox-ui/assets/index-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 +309 -111
- package/dist/mind/desk-section.js +362 -0
- package/dist/mind/diary-integrity.js +60 -0
- package/dist/mind/{memory.js → diary.js} +68 -77
- package/dist/mind/embedding-provider.js +60 -0
- package/dist/mind/file-state.js +179 -0
- package/dist/mind/{associative-recall.js → note-search.js} +47 -58
- package/dist/mind/obligation-steering.js +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 +1050 -189
- 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 +1045 -0
- package/dist/repertoire/bundle-templates.js +71 -0
- package/dist/repertoire/bw-installer.js +180 -0
- package/dist/repertoire/coding/codex-jsonl.js +64 -0
- package/dist/repertoire/coding/context-pack.js +331 -0
- package/dist/repertoire/coding/feedback.js +174 -32
- package/dist/repertoire/coding/index.js +15 -3
- package/dist/repertoire/coding/manager.js +159 -10
- package/dist/repertoire/coding/spawner.js +58 -11
- package/dist/repertoire/coding/tools.js +221 -15
- package/dist/repertoire/coding/workbench-client.js +272 -0
- package/dist/repertoire/coding/workbench-manager.js +346 -0
- package/dist/repertoire/commerce-errors.js +109 -0
- package/dist/repertoire/commerce-self-test.js +156 -0
- package/dist/repertoire/credential-access.js +178 -0
- package/dist/repertoire/desk/classifier.js +362 -0
- package/dist/repertoire/duffel-client.js +185 -0
- package/dist/repertoire/github-client.js +14 -55
- package/dist/repertoire/graph-client.js +11 -52
- package/dist/repertoire/guardrails.js +811 -18
- package/dist/repertoire/mcp-client.js +44 -3
- package/dist/repertoire/mcp-manager.js +427 -139
- package/dist/repertoire/mcp-tools.js +124 -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/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 +170 -0
- package/dist/repertoire/tool-results.js +29 -0
- package/dist/repertoire/tools-a2a.js +699 -0
- package/dist/repertoire/tools-attachments.js +322 -0
- package/dist/repertoire/tools-awaiting.js +575 -0
- package/dist/repertoire/tools-base.js +77 -1138
- package/dist/repertoire/tools-bluebubbles.js +2 -0
- package/dist/repertoire/tools-bridge.js +144 -0
- package/dist/repertoire/tools-bundle.js +993 -0
- package/dist/repertoire/tools-commerce.js +253 -0
- package/dist/repertoire/tools-config.js +186 -0
- package/dist/repertoire/tools-continuity.js +575 -0
- package/dist/repertoire/tools-credential.js +383 -0
- package/dist/repertoire/tools-evolution.js +527 -0
- package/dist/repertoire/tools-files.js +344 -0
- package/dist/repertoire/tools-flight.js +290 -0
- package/dist/repertoire/tools-flow.js +119 -0
- package/dist/repertoire/tools-github.js +3 -8
- package/dist/repertoire/tools-habits.js +103 -0
- package/dist/repertoire/tools-mail.js +1828 -0
- package/dist/repertoire/tools-notes.js +485 -0
- package/dist/repertoire/tools-obligations.js +182 -0
- package/dist/repertoire/tools-orientation.js +31 -0
- package/dist/repertoire/tools-record.js +482 -0
- package/dist/repertoire/tools-rsvp.js +139 -0
- package/dist/repertoire/tools-runtime.js +150 -0
- package/dist/repertoire/tools-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-steward-policy.js +119 -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 +531 -0
- package/dist/repertoire/tools-user-profile.js +146 -0
- package/dist/repertoire/tools-vault.js +40 -0
- package/dist/repertoire/tools-voice.js +145 -0
- package/dist/repertoire/tools.js +494 -148
- 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 +628 -0
- package/dist/rsvp/aisleplanner-client.js +354 -0
- package/dist/rsvp/cli.js +926 -0
- package/dist/rsvp/config.js +327 -0
- package/dist/rsvp/cutover.js +433 -0
- package/dist/rsvp/diagnostics.js +232 -0
- package/dist/rsvp/diff-renderer.js +176 -0
- package/dist/rsvp/habit-policy.js +110 -0
- package/dist/rsvp/habit-stage.js +123 -0
- package/dist/rsvp/incident-bundle.js +97 -0
- package/dist/rsvp/migration.js +204 -0
- package/dist/rsvp/native-habit-runner.js +630 -0
- package/dist/rsvp/outbound-state.js +628 -0
- package/dist/rsvp/query.js +118 -0
- package/dist/rsvp/replay.js +233 -0
- package/dist/rsvp/snapshot.js +232 -0
- package/dist/rsvp/spend-ledger.js +175 -0
- package/dist/scripts/claude-code-hook.js +41 -0
- package/dist/scripts/claude-code-stop-hook.js +47 -0
- package/dist/senses/a2a-entry.js +115 -0
- package/dist/senses/acp-server.js +386 -0
- package/dist/senses/attention-queue.js +186 -0
- package/dist/senses/await-turn-message.js +58 -0
- package/dist/senses/bluebubbles/active-turns.js +216 -0
- package/dist/senses/bluebubbles/attachment-cache.js +53 -0
- package/dist/senses/bluebubbles/attachment-download.js +137 -0
- package/dist/senses/{bluebubbles-client.js → bluebubbles/client.js} +355 -40
- package/dist/senses/bluebubbles/context-packet.js +207 -0
- package/dist/senses/bluebubbles/context-smoke.js +144 -0
- package/dist/senses/bluebubbles/entry.js +90 -0
- package/dist/senses/{bluebubbles-inbound-log.js → bluebubbles/inbound-log.js} +21 -3
- package/dist/senses/bluebubbles/index.js +3841 -0
- package/dist/senses/bluebubbles/latest-turn.js +311 -0
- package/dist/senses/{bluebubbles-media.js → bluebubbles/media.js} +121 -71
- package/dist/senses/{bluebubbles-model.js → bluebubbles/model.js} +159 -28
- package/dist/senses/{bluebubbles-mutation-log.js → bluebubbles/mutation-log.js} +24 -13
- package/dist/senses/bluebubbles/outbound-state.js +308 -0
- package/dist/senses/bluebubbles/processed-log.js +133 -0
- package/dist/senses/bluebubbles/reaction-policy.js +47 -0
- package/dist/senses/bluebubbles/replay.js +284 -0
- package/dist/senses/{bluebubbles-runtime-state.js → bluebubbles/runtime-state.js} +30 -2
- package/dist/senses/bluebubbles/semantic-receipts.js +1986 -0
- package/dist/senses/{bluebubbles-session-cleanup.js → bluebubbles/session-cleanup.js} +1 -1
- package/dist/senses/bluebubbles/webhook-registration.js +228 -0
- package/dist/senses/bluebubbles-meta-guard.js +39 -0
- package/dist/senses/cli/bracketed-paste.js +82 -0
- package/dist/senses/cli/image-paste.js +287 -0
- package/dist/senses/cli/image-ref-navigation.js +75 -0
- package/dist/senses/cli/ink-app.js +156 -0
- package/dist/senses/cli/inline-diff.js +64 -0
- package/dist/senses/cli/input-keys.js +174 -0
- package/dist/senses/cli/kill-ring.js +86 -0
- package/dist/senses/cli/message-list.js +51 -0
- package/dist/senses/cli/ouro-tui.js +607 -0
- package/dist/senses/cli/spinner-imperative.js +135 -0
- package/dist/senses/cli/spinner.js +101 -0
- package/dist/senses/cli/status-line.js +60 -0
- package/dist/senses/cli/streaming-markdown.js +526 -0
- package/dist/senses/cli/tool-display.js +85 -0
- package/dist/senses/cli/tool-render.js +85 -0
- package/dist/senses/cli/tui-store.js +241 -0
- package/dist/senses/cli/virtual-list.js +35 -0
- package/dist/senses/cli-entry.js +60 -8
- package/dist/senses/cli.js +604 -320
- 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 +1115 -204
- package/dist/senses/private-runtime-worker.js +859 -0
- package/dist/senses/private-runtime.js +1517 -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 +100 -0
- package/dist/senses/sanctuary-health.js +408 -0
- package/dist/senses/sanctuary-install-state-contract.js +126 -0
- package/dist/senses/sanctuary-interactive-control.js +410 -0
- package/dist/senses/sanctuary-media-catalog-contract.js +297 -0
- package/dist/senses/sanctuary-media-optimization.js +514 -0
- package/dist/senses/sanctuary-runtime.js +340 -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 +772 -0
- package/dist/senses/surface-tool.js +109 -0
- package/dist/senses/teams-entry.js +60 -8
- package/dist/senses/teams.js +519 -218
- 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 +896 -0
- package/dist/senses/telegram-entry.js +83 -0
- package/dist/senses/telegram.js +2324 -0
- package/dist/senses/trust-gate.js +104 -9
- package/dist/senses/voice/audio-playback.js +237 -0
- package/dist/senses/voice/audio-routing.js +119 -0
- package/dist/senses/voice/elevenlabs.js +202 -0
- package/dist/senses/voice/floor-control.js +431 -0
- package/dist/senses/voice/floor-controller.js +115 -0
- package/dist/senses/voice/golden-path.js +116 -0
- package/dist/senses/voice/index.js +29 -0
- package/dist/senses/voice/meeting.js +113 -0
- package/dist/senses/voice/outbound.js +190 -0
- package/dist/senses/voice/phone.js +33 -0
- package/dist/senses/voice/playback.js +139 -0
- package/dist/senses/voice/realtime-eval.js +496 -0
- package/dist/senses/voice/realtime-trace.js +531 -0
- package/dist/senses/voice/transcript.js +70 -0
- package/dist/senses/voice/turn.js +191 -0
- package/dist/senses/voice/twilio-phone-runtime.js +807 -0
- package/dist/senses/voice/twilio-phone.js +5031 -0
- package/dist/senses/voice/types.js +2 -0
- package/dist/senses/voice/whisper.js +161 -0
- package/dist/senses/voice-entry.js +81 -0
- package/dist/senses/voice-realtime-eval-command.js +99 -0
- package/dist/senses/voice-realtime-eval-entry.js +21 -0
- package/dist/senses/voice-twilio-entry.js +87 -0
- package/dist/trips/core.js +138 -0
- package/dist/trips/store.js +265 -0
- package/dist/util/frontmatter.js +69 -0
- package/npm-shrinkwrap.json +8138 -0
- package/package.json +72 -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 -228
- package/dist/heart/session-recall.js +0 -116
- package/dist/mind/friends/channel.js +0 -84
- package/dist/mind/friends/group-context.js +0 -144
- package/dist/mind/friends/resolver.js +0 -84
- package/dist/mind/friends/store-file.js +0 -190
- package/dist/mind/friends/store.js +0 -4
- package/dist/mind/friends/tokens.js +0 -26
- package/dist/mind/friends/trust-explanation.js +0 -74
- package/dist/mind/friends/types.js +0 -29
- package/dist/repertoire/tasks/board.js +0 -134
- package/dist/repertoire/tasks/index.js +0 -224
- package/dist/repertoire/tasks/lifecycle.js +0 -80
- package/dist/repertoire/tasks/middleware.js +0 -65
- package/dist/repertoire/tasks/parser.js +0 -173
- package/dist/repertoire/tasks/scanner.js +0 -132
- package/dist/repertoire/tasks/transitions.js +0 -144
- package/dist/senses/bluebubbles-entry.js +0 -13
- package/dist/senses/bluebubbles.js +0 -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,1830 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.SANCTUARY_SCENARIO_SOURCES = exports.SANCTUARY_SCENARIO_GATES = exports.SANCTUARY_UNIT_16_EVIDENCE_LABELS = void 0;
|
|
37
|
+
exports.exactSanctuaryContainmentProfileBoundaries = exactSanctuaryContainmentProfileBoundaries;
|
|
38
|
+
exports.resolveSanctuaryAdapterTimeoutMs = resolveSanctuaryAdapterTimeoutMs;
|
|
39
|
+
exports.createSanctuaryAcceptanceHarnessDependencies = createSanctuaryAcceptanceHarnessDependencies;
|
|
40
|
+
exports.sanctuaryScenarioTimeoutBudget = sanctuaryScenarioTimeoutBudget;
|
|
41
|
+
exports.validateSanctuaryUnit16EvidenceAssertions = validateSanctuaryUnit16EvidenceAssertions;
|
|
42
|
+
exports.executeSanctuaryAcceptanceHarness = executeSanctuaryAcceptanceHarness;
|
|
43
|
+
const node_crypto_1 = require("node:crypto");
|
|
44
|
+
const node_child_process_1 = require("node:child_process");
|
|
45
|
+
const node_fs_1 = require("node:fs");
|
|
46
|
+
const node_fs_2 = require("node:fs");
|
|
47
|
+
const path = __importStar(require("node:path"));
|
|
48
|
+
const node_util_1 = require("node:util");
|
|
49
|
+
const friends_1 = require("@ouro.bot/friends");
|
|
50
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
51
|
+
const sanctuary_authority_resident_1 = require("../../senses/sanctuary-authority-resident");
|
|
52
|
+
const relationship_authorization_1 = require("../../repertoire/relationship-authorization");
|
|
53
|
+
const tools_1 = require("../../repertoire/tools");
|
|
54
|
+
const MAX_ADAPTER_OUTPUT = 1_048_576;
|
|
55
|
+
const DEFAULT_ADAPTER_TIMEOUT_MS = 240_000;
|
|
56
|
+
const DEFAULT_TELEGRAM_TIMEOUT_MS = 65_000;
|
|
57
|
+
const PACKAGED_PROVENANCE_ADAPTER = "/opt/ouro/deploy/unraid/sanctuary-acceptance-adapter.sh";
|
|
58
|
+
const OPAQUE_DIGEST = /^[0-9a-f]{64}$/u;
|
|
59
|
+
function exactSanctuaryContainmentProfileBoundaries(value) {
|
|
60
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
61
|
+
return false;
|
|
62
|
+
const boundaries = value;
|
|
63
|
+
const versions = { "sanctuary-owner": 10, "sanctuary-household": 5, "sanctuary-event": 4 };
|
|
64
|
+
if (!(0, node_util_1.isDeepStrictEqual)(Object.keys(boundaries).sort(), Object.keys(versions).sort()))
|
|
65
|
+
return false;
|
|
66
|
+
const packageRoot = path.resolve(__dirname, "../../../deploy/unraid/sanctuary.ouro");
|
|
67
|
+
const registry = (0, relationship_authorization_1.loadRelationshipCapabilityRegistry)(packageRoot);
|
|
68
|
+
const digest = (input) => (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(input)).digest("hex");
|
|
69
|
+
return Object.entries(versions).every(([id, version]) => {
|
|
70
|
+
const raw = boundaries[id];
|
|
71
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
72
|
+
return false;
|
|
73
|
+
const boundary = raw;
|
|
74
|
+
const capabilities = boundary.providerCapabilities;
|
|
75
|
+
if (!Array.isArray(capabilities) || !capabilities.every((entry) => entry === "reasoning-effort" || entry === "phase-annotation" || entry === "approval-continuation")
|
|
76
|
+
|| new Set(capabilities).size !== capabilities.length)
|
|
77
|
+
return false;
|
|
78
|
+
const profile = registry.profiles[id];
|
|
79
|
+
if (profile.version !== version)
|
|
80
|
+
return false;
|
|
81
|
+
const selection = (0, tools_1.selectToolsForChannel)((0, friends_1.getChannelCapabilities)(id === "sanctuary-event" ? "inner" : "telegram"), undefined, undefined, new Set(capabilities), undefined, undefined, {
|
|
82
|
+
agentName: "sanctuary", relationshipAuthorization: { profileId: id, advertisedToolNames: profile.toolNames },
|
|
83
|
+
});
|
|
84
|
+
const schemas = (0, tools_1.toolSelectionSchemas)(selection);
|
|
85
|
+
const excludedToolNames = ["vault_get", "mcp_call", "exec", "credential_get", ...(id === "sanctuary-owner" ? [] : tools_1.SANCTUARY_OWNER_ADDITIONS)];
|
|
86
|
+
const globallyResolvableExcludedToolCount = excludedToolNames.filter((name) => (0, tools_1.resolveToolDefinition)(name)).length;
|
|
87
|
+
return globallyResolvableExcludedToolCount > 0 && (0, node_util_1.isDeepStrictEqual)(boundary, {
|
|
88
|
+
profileId: id, profileVersion: version, profileDigest: digest(profile), profileToolNames: profile.toolNames,
|
|
89
|
+
providerCapabilities: capabilities, schemaDigest: digest(schemas), schemaToolNames: schemas.map((tool) => tool.function.name),
|
|
90
|
+
ordinaryDefinitionCount: selection.ordinary.length, engineSchemaCount: selection.engine.length,
|
|
91
|
+
profileExact: true, schemasExact: true, handlersExact: true, poisonedSchemaIntersectionCount: 0,
|
|
92
|
+
excludedToolNames, excludedSchemaIntersectionCount: 0, fabricatedHandlerInvocationCount: 0,
|
|
93
|
+
excludedToolAttemptCount: excludedToolNames.length, excludedToolRejectedCount: excludedToolNames.length,
|
|
94
|
+
excludedToolInvokedCount: 0, excludedToolSideEffectCount: 0, globallyResolvableExcludedToolCount,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function resolveSanctuaryAdapterTimeoutMs(configured, remaining) {
|
|
99
|
+
const maximum = configured ?? DEFAULT_ADAPTER_TIMEOUT_MS;
|
|
100
|
+
return Math.max(1, Math.min(maximum, remaining ?? maximum));
|
|
101
|
+
}
|
|
102
|
+
function createSanctuaryAcceptanceHarnessDependencies(secretFd = 3, options = {}) {
|
|
103
|
+
const adapterTimeoutMs = options.adapterTimeoutMs ?? DEFAULT_ADAPTER_TIMEOUT_MS;
|
|
104
|
+
const telegramTimeoutMs = options.telegramTimeoutMs ?? DEFAULT_TELEGRAM_TIMEOUT_MS;
|
|
105
|
+
return {
|
|
106
|
+
gateway: () => (0, sanctuary_authority_resident_1.openSanctuaryResidentAuthority)({}, {}),
|
|
107
|
+
readSecret: () => (0, node_fs_1.readFileSync)(secretFd, "utf8"),
|
|
108
|
+
runAdapter: async (executable, payload, remainingMs) => {
|
|
109
|
+
requireAbsoluteExecutable(executable);
|
|
110
|
+
const result = (0, node_child_process_1.spawnSync)(executable, [], {
|
|
111
|
+
input: `${JSON.stringify(payload)}\n`,
|
|
112
|
+
encoding: "utf8",
|
|
113
|
+
maxBuffer: MAX_ADAPTER_OUTPUT,
|
|
114
|
+
timeout: resolveSanctuaryAdapterTimeoutMs(adapterTimeoutMs, remainingMs),
|
|
115
|
+
cwd: "/",
|
|
116
|
+
env: { PATH: process.env.PATH ?? "/usr/bin:/bin" },
|
|
117
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
118
|
+
});
|
|
119
|
+
if (result.error && "code" in result.error && result.error.code === "ETIMEDOUT")
|
|
120
|
+
throw new Error("Sanctuary acceptance adapter timed out");
|
|
121
|
+
if (result.error && "code" in result.error && result.error.code === "ENOBUFS")
|
|
122
|
+
throw new Error("Sanctuary acceptance adapter output exceeded the limit");
|
|
123
|
+
if (result.error || result.status !== 0)
|
|
124
|
+
throw new Error("Sanctuary acceptance adapter failed");
|
|
125
|
+
const stdout = result.stdout;
|
|
126
|
+
try {
|
|
127
|
+
return JSON.parse(stdout);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
throw new Error("Sanctuary acceptance adapter returned invalid JSON");
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
realpath: node_fs_1.realpathSync,
|
|
134
|
+
now: Date.now,
|
|
135
|
+
randomBytes: node_crypto_1.randomBytes,
|
|
136
|
+
sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
137
|
+
telegramTimeoutMs,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function object(value, label) {
|
|
141
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
142
|
+
throw new Error(`${label} must be an object`);
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
function text(value, label) {
|
|
146
|
+
if (typeof value !== "string" || !value.trim())
|
|
147
|
+
throw new Error(`${label} must be nonempty text`);
|
|
148
|
+
return value.trim();
|
|
149
|
+
}
|
|
150
|
+
function integer(value, label, minimum = 0) {
|
|
151
|
+
if (!Number.isSafeInteger(value) || value < minimum)
|
|
152
|
+
throw new Error(`${label} must be a safe integer >= ${minimum}`);
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
function boolean(value, label) {
|
|
156
|
+
if (typeof value !== "boolean")
|
|
157
|
+
throw new Error(`${label} must be boolean`);
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
function stringArray(value, label) {
|
|
161
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string" && item.trim()))
|
|
162
|
+
throw new Error(`${label} must be nonempty strings`);
|
|
163
|
+
return value.map((item) => item.trim());
|
|
164
|
+
}
|
|
165
|
+
function requireAbsoluteExecutable(executable) {
|
|
166
|
+
if (!path.isAbsolute(executable))
|
|
167
|
+
throw new Error("adapter executable must be an absolute path");
|
|
168
|
+
}
|
|
169
|
+
function adapter(value, label) {
|
|
170
|
+
const executable = text(value, label);
|
|
171
|
+
requireAbsoluteExecutable(executable);
|
|
172
|
+
return executable;
|
|
173
|
+
}
|
|
174
|
+
function digest(value) {
|
|
175
|
+
return (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(value)).digest("hex");
|
|
176
|
+
}
|
|
177
|
+
function processUid() {
|
|
178
|
+
const uid = process.getuid?.();
|
|
179
|
+
if (uid === undefined)
|
|
180
|
+
throw new Error("acceptance harness requires an operating-system user identity");
|
|
181
|
+
return uid;
|
|
182
|
+
}
|
|
183
|
+
function privateAllowedRoot(config) {
|
|
184
|
+
const configured = text(config.allowedRoot, "allowedRoot");
|
|
185
|
+
if (!path.isAbsolute(configured))
|
|
186
|
+
throw new Error("allowed root must be absolute");
|
|
187
|
+
const resolved = path.resolve(configured);
|
|
188
|
+
const metadata = (0, node_fs_1.lstatSync)(resolved);
|
|
189
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory())
|
|
190
|
+
throw new Error("allowed root must be a nonsymlink directory");
|
|
191
|
+
if (metadata.uid !== processUid())
|
|
192
|
+
throw new Error("allowed root must be owned by the harness user");
|
|
193
|
+
if ((metadata.mode & 0o077) !== 0)
|
|
194
|
+
throw new Error("allowed root must be private");
|
|
195
|
+
if ((0, node_fs_1.realpathSync)(resolved) !== resolved)
|
|
196
|
+
throw new Error("allowed root must be canonical");
|
|
197
|
+
return resolved;
|
|
198
|
+
}
|
|
199
|
+
function confinedPath(root, value, label) {
|
|
200
|
+
const configured = text(value, label);
|
|
201
|
+
if (!path.isAbsolute(configured))
|
|
202
|
+
throw new Error(`${label} must be absolute`);
|
|
203
|
+
const resolved = path.resolve(configured);
|
|
204
|
+
const relative = path.relative(root, resolved);
|
|
205
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
206
|
+
throw new Error(`${label} must remain within the allowed root`);
|
|
207
|
+
}
|
|
208
|
+
let cursor = root;
|
|
209
|
+
for (const segment of path.dirname(relative).split(path.sep).filter(Boolean)) {
|
|
210
|
+
cursor = path.join(cursor, segment);
|
|
211
|
+
const metadata = (0, node_fs_1.lstatSync)(cursor);
|
|
212
|
+
if (metadata.isSymbolicLink())
|
|
213
|
+
throw new Error(`${label} ancestor must not be a symlink`);
|
|
214
|
+
if (!metadata.isDirectory() || metadata.uid !== processUid() || (metadata.mode & 0o077) !== 0) {
|
|
215
|
+
throw new Error(`${label} ancestor must be an owned private directory`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return resolved;
|
|
219
|
+
}
|
|
220
|
+
function requirePrivateRegularFile(root, value, label) {
|
|
221
|
+
const filePath = confinedPath(root, value, label);
|
|
222
|
+
const metadata = (0, node_fs_1.lstatSync)(filePath);
|
|
223
|
+
if (metadata.isSymbolicLink() || !metadata.isFile())
|
|
224
|
+
throw new Error(`${label} must be a nonsymlink regular file`);
|
|
225
|
+
if (metadata.uid !== processUid() || (metadata.mode & 0o777) !== 0o600)
|
|
226
|
+
throw new Error(`${label} must be an owned private file`);
|
|
227
|
+
return filePath;
|
|
228
|
+
}
|
|
229
|
+
function pathEntryExists(filePath) {
|
|
230
|
+
try {
|
|
231
|
+
(0, node_fs_1.lstatSync)(filePath);
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (error.code === "ENOENT")
|
|
236
|
+
return false;
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function syncDirectory(directory) {
|
|
241
|
+
const handle = (0, node_fs_1.openSync)(directory, "r");
|
|
242
|
+
try {
|
|
243
|
+
(0, node_fs_1.fsyncSync)(handle);
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
(0, node_fs_1.closeSync)(handle);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function writeAtomicPrivateJson(root, filePath, value, replace) {
|
|
250
|
+
const confined = confinedPath(root, filePath, "checkpoint path");
|
|
251
|
+
const temporary = `${confined}.tmp-${process.pid}-${(0, node_crypto_1.randomBytes)(8).toString("hex")}`;
|
|
252
|
+
try {
|
|
253
|
+
(0, node_fs_1.writeFileSync)(temporary, `${JSON.stringify(value)}\n`, { flag: "wx", mode: 0o600 });
|
|
254
|
+
const handle = (0, node_fs_1.openSync)(temporary, "r");
|
|
255
|
+
try {
|
|
256
|
+
(0, node_fs_1.fsyncSync)(handle);
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
(0, node_fs_1.closeSync)(handle);
|
|
260
|
+
}
|
|
261
|
+
if (replace) {
|
|
262
|
+
requirePrivateRegularFile(root, confined, "checkpoint path");
|
|
263
|
+
(0, node_fs_1.renameSync)(temporary, confined);
|
|
264
|
+
(0, node_fs_1.chmodSync)(confined, 0o600);
|
|
265
|
+
syncDirectory(path.dirname(confined));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
(0, node_fs_1.linkSync)(temporary, confined);
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
throw new Error("acceptance checkpoint claim failed; inspect-before-retry is required");
|
|
273
|
+
}
|
|
274
|
+
(0, node_fs_1.unlinkSync)(temporary);
|
|
275
|
+
syncDirectory(path.dirname(confined));
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
if ((0, node_fs_1.existsSync)(temporary))
|
|
279
|
+
(0, node_fs_1.unlinkSync)(temporary);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function writeAtomicPrivateText(root, filePath, value) {
|
|
283
|
+
const confined = confinedPath(root, filePath, "private text path");
|
|
284
|
+
const temporary = `${confined}.tmp-${process.pid}-${(0, node_crypto_1.randomBytes)(8).toString("hex")}`;
|
|
285
|
+
try {
|
|
286
|
+
(0, node_fs_1.writeFileSync)(temporary, value, { flag: "wx", mode: 0o600 });
|
|
287
|
+
const handle = (0, node_fs_1.openSync)(temporary, "r");
|
|
288
|
+
try {
|
|
289
|
+
(0, node_fs_1.fsyncSync)(handle);
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
(0, node_fs_1.closeSync)(handle);
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
(0, node_fs_1.linkSync)(temporary, confined);
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
throw new Error("private text claim failed; inspect-before-retry is required");
|
|
299
|
+
}
|
|
300
|
+
(0, node_fs_1.unlinkSync)(temporary);
|
|
301
|
+
syncDirectory(path.dirname(confined));
|
|
302
|
+
}
|
|
303
|
+
finally {
|
|
304
|
+
if ((0, node_fs_1.existsSync)(temporary))
|
|
305
|
+
(0, node_fs_1.unlinkSync)(temporary);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function initializeCheckpoint(root, filePath, value) {
|
|
309
|
+
writeAtomicPrivateJson(root, filePath, value, false);
|
|
310
|
+
}
|
|
311
|
+
function refuseExistingCheckpoint(root, filePath) {
|
|
312
|
+
const confined = confinedPath(root, filePath, "checkpoint path");
|
|
313
|
+
if (pathEntryExists(confined))
|
|
314
|
+
throw new Error("acceptance checkpoint exists; inspect-before-retry is required");
|
|
315
|
+
}
|
|
316
|
+
function replaceCheckpoint(root, filePath, value) {
|
|
317
|
+
writeAtomicPrivateJson(root, filePath, value, true);
|
|
318
|
+
}
|
|
319
|
+
function readCheckpoint(root, filePath) {
|
|
320
|
+
const confined = requirePrivateRegularFile(root, filePath, "acceptance checkpoint");
|
|
321
|
+
return object(JSON.parse((0, node_fs_1.readFileSync)(confined, "utf8")), "acceptance checkpoint");
|
|
322
|
+
}
|
|
323
|
+
function safeErrorCategory(error) {
|
|
324
|
+
if (!(error instanceof Error))
|
|
325
|
+
return "unknown";
|
|
326
|
+
if (/timed out/iu.test(error.message))
|
|
327
|
+
return "timeout";
|
|
328
|
+
if (/adapter/iu.test(error.message))
|
|
329
|
+
return "adapter";
|
|
330
|
+
if (/checkpoint|inspect-before-retry/iu.test(error.message))
|
|
331
|
+
return "checkpoint";
|
|
332
|
+
return "validation";
|
|
333
|
+
}
|
|
334
|
+
function failedCheckpoint(root, filePath, base, error) {
|
|
335
|
+
replaceCheckpoint(root, filePath, { ...base, phase: "failed", errorCategory: safeErrorCategory(error) });
|
|
336
|
+
}
|
|
337
|
+
function telegramBootstrapRequestError(method, error) {
|
|
338
|
+
const outcome = error instanceof Error && /timed out/iu.test(error.message) ? "timed out" : "failed";
|
|
339
|
+
return new Error(`Telegram ${method} ${outcome}; actor: agent-runnable; retry Telegram bootstrap`);
|
|
340
|
+
}
|
|
341
|
+
exports.SANCTUARY_UNIT_16_EVIDENCE_LABELS = [
|
|
342
|
+
"unit-12c-1-opaque-identity",
|
|
343
|
+
"unit-14b-3-opaque-identity-live",
|
|
344
|
+
"unit-15c-1-no-callback-terminalization",
|
|
345
|
+
"unit-16a-pre-reboot-checkpoint",
|
|
346
|
+
"unit-16a-reboot-request",
|
|
347
|
+
"unit-16a-boot-recovery-milestones",
|
|
348
|
+
"unit-16b-runtime-vault-readiness",
|
|
349
|
+
"unit-16c-provider-readiness",
|
|
350
|
+
"unit-16d-whats-up",
|
|
351
|
+
"unit-16d-1-space",
|
|
352
|
+
"unit-16d-2-unknown-admission",
|
|
353
|
+
"unit-16e-containment-audit",
|
|
354
|
+
"unit-16e-1-stop-denial",
|
|
355
|
+
"unit-16e-2-restart-denial",
|
|
356
|
+
"unit-16f-cron-fingerprint",
|
|
357
|
+
"unit-16g-health-transition",
|
|
358
|
+
"unit-16h-acceptance-delivery-probe",
|
|
359
|
+
"unit-16i-delayed-approval",
|
|
360
|
+
"unit-16j-denial",
|
|
361
|
+
"unit-16k-timeout-stale",
|
|
362
|
+
"unit-16l-duplicate-callback",
|
|
363
|
+
"unit-16m-restart-continuation",
|
|
364
|
+
];
|
|
365
|
+
const SANCTUARY_SCENARIO_ADAPTER_OPERATION = "capture_acceptance_scenario";
|
|
366
|
+
const SANCTUARY_SCENARIO_COMMAND = "evidence-snapshot";
|
|
367
|
+
const SCENARIO_CLEANUP_RESERVE_MS = 5_000;
|
|
368
|
+
const MAX_SCENARIO_INTERVAL_MS = 30_000;
|
|
369
|
+
const SCENARIO_CAPTURE_ADAPTER_TIMEOUT_MS = 210_000;
|
|
370
|
+
const APPROVAL_TTL_MS = 300_000;
|
|
371
|
+
const STALE_CALLBACK_INJECTION_TIMEOUT_MS = 120_000;
|
|
372
|
+
const TIMEOUT_STALE_SCENARIO_MS = SCENARIO_CAPTURE_ADAPTER_TIMEOUT_MS
|
|
373
|
+
+ APPROVAL_TTL_MS
|
|
374
|
+
+ STALE_CALLBACK_INJECTION_TIMEOUT_MS
|
|
375
|
+
+ MAX_SCENARIO_INTERVAL_MS
|
|
376
|
+
+ SCENARIO_CAPTURE_ADAPTER_TIMEOUT_MS
|
|
377
|
+
+ SCENARIO_CLEANUP_RESERVE_MS;
|
|
378
|
+
const REBOOT_SCENARIO_LABELS = new Set([
|
|
379
|
+
"unit-16a-pre-reboot-checkpoint",
|
|
380
|
+
"unit-16a-reboot-request",
|
|
381
|
+
"unit-16a-boot-recovery-milestones",
|
|
382
|
+
]);
|
|
383
|
+
const MAX_MATRIX_TIMEOUT_MS = 7_200_000;
|
|
384
|
+
function sanctuaryScenarioTimeoutBudget(label) {
|
|
385
|
+
if (REBOOT_SCENARIO_LABELS.has(label))
|
|
386
|
+
return 125_000;
|
|
387
|
+
if (label === "unit-15c-1-no-callback-terminalization")
|
|
388
|
+
return 365_000;
|
|
389
|
+
if (label === "unit-16h-acceptance-delivery-probe")
|
|
390
|
+
return 1_025_000;
|
|
391
|
+
if (label === "unit-16f-cron-fingerprint")
|
|
392
|
+
return 1_025_000;
|
|
393
|
+
if (label === "unit-16i-delayed-approval")
|
|
394
|
+
return 185_000;
|
|
395
|
+
if (label === "unit-16k-timeout-stale")
|
|
396
|
+
return TIMEOUT_STALE_SCENARIO_MS;
|
|
397
|
+
return exports.SANCTUARY_SCENARIO_GATES[label] === "none" ? 35_000 : 305_000;
|
|
398
|
+
}
|
|
399
|
+
function exactObjectKeys(value, expected, label) {
|
|
400
|
+
if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify([...expected].sort())) {
|
|
401
|
+
throw new Error(`${label} fields are invalid`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function requiredTrue(value, key, label) {
|
|
405
|
+
if (boolean(value[key], `${label} ${key}`) !== true)
|
|
406
|
+
throw new Error(`${label} ${key} must be true`);
|
|
407
|
+
}
|
|
408
|
+
function requiredFalse(value, key, label) {
|
|
409
|
+
if (boolean(value[key], `${label} ${key}`) !== false)
|
|
410
|
+
throw new Error(`${label} ${key} must be false`);
|
|
411
|
+
}
|
|
412
|
+
function requiredInteger(value, key, expected, label) {
|
|
413
|
+
if (integer(value[key], `${label} ${key}`) !== expected)
|
|
414
|
+
throw new Error(`${label} ${key} must equal ${expected}`);
|
|
415
|
+
}
|
|
416
|
+
function validateSanctuaryUnit16EvidenceAssertions(label, raw) {
|
|
417
|
+
if (!exports.SANCTUARY_UNIT_16_EVIDENCE_LABELS.includes(label))
|
|
418
|
+
throw new Error("Sanctuary evidence label is retired or unsupported");
|
|
419
|
+
const value = object(raw, `${label} assertions`);
|
|
420
|
+
const exact = (keys) => exactObjectKeys(value, keys, `${label} assertions`);
|
|
421
|
+
const allTrue = (keys) => keys.forEach((key) => requiredTrue(value, key, label));
|
|
422
|
+
const allZero = (keys) => keys.forEach((key) => requiredInteger(value, key, 0, label));
|
|
423
|
+
switch (label) {
|
|
424
|
+
case "unit-12c-1-opaque-identity":
|
|
425
|
+
case "unit-14b-3-opaque-identity-live":
|
|
426
|
+
exact(["identityBound", "opaqueSubject", "rawIdentityAbsent"]);
|
|
427
|
+
allTrue(["identityBound", "opaqueSubject", "rawIdentityAbsent"]);
|
|
428
|
+
break;
|
|
429
|
+
case "unit-15c-1-no-callback-terminalization":
|
|
430
|
+
exact(["buttonsRemoved", "elapsedMs", "mutationCount", "noInboundUpdate", "replayMutationCount", "terminalExpired", "ttlMs"]);
|
|
431
|
+
allTrue(["buttonsRemoved", "noInboundUpdate", "terminalExpired"]);
|
|
432
|
+
allZero(["mutationCount", "replayMutationCount"]);
|
|
433
|
+
if (integer(value.ttlMs, `${label} ttlMs`, 1) > integer(value.elapsedMs, `${label} elapsedMs`, 1))
|
|
434
|
+
throw new Error(`${label} elapsedMs must reach ttlMs`);
|
|
435
|
+
break;
|
|
436
|
+
case "unit-16a-pre-reboot-checkpoint":
|
|
437
|
+
exact(["approvalDigest", "auditDigest", "containerDigest", "fingerprintDigest", "offsetDigest", "processBindingDigest", "ready", "unrelatedHostOperations"]);
|
|
438
|
+
for (const key of ["approvalDigest", "auditDigest", "containerDigest", "fingerprintDigest", "offsetDigest"])
|
|
439
|
+
opaqueDigest(value[key], `${label} ${key}`);
|
|
440
|
+
opaqueDigest(value.processBindingDigest, `${label} processBindingDigest`);
|
|
441
|
+
requiredTrue(value, "ready", label);
|
|
442
|
+
requiredInteger(value, "unrelatedHostOperations", 0, label);
|
|
443
|
+
break;
|
|
444
|
+
case "unit-16a-reboot-request":
|
|
445
|
+
exact(["exactlyOnce", "processBindingDigest", "requestCheckpointPersisted", "requestDigest"]);
|
|
446
|
+
allTrue(["exactlyOnce", "requestCheckpointPersisted"]);
|
|
447
|
+
opaqueDigest(value.processBindingDigest, `${label} processBindingDigest`);
|
|
448
|
+
opaqueDigest(value.requestDigest, `${label} requestDigest`);
|
|
449
|
+
break;
|
|
450
|
+
case "unit-16a-boot-recovery-milestones":
|
|
451
|
+
exact(["arrayReady", "bootIdentityChanged", "butlerReady", "dockerReady", "hostReady", "postbootIntegrityPreserved", "processBindingDigest", "sshReady", "tailscaleReady"]);
|
|
452
|
+
allTrue(["arrayReady", "bootIdentityChanged", "butlerReady", "dockerReady", "hostReady", "postbootIntegrityPreserved", "sshReady", "tailscaleReady"]);
|
|
453
|
+
opaqueDigest(value.processBindingDigest, `${label} processBindingDigest`);
|
|
454
|
+
break;
|
|
455
|
+
case "unit-16b-runtime-vault-readiness":
|
|
456
|
+
exact(["autostartExact", "exactImage", "manualAuthRequired", "updaterDisabled", "vaultUnlocked"]);
|
|
457
|
+
allTrue(["autostartExact", "exactImage", "updaterDisabled", "vaultUnlocked"]);
|
|
458
|
+
requiredFalse(value, "manualAuthRequired", label);
|
|
459
|
+
break;
|
|
460
|
+
case "unit-16c-provider-readiness":
|
|
461
|
+
exact(["innerReady", "laneSelectionExact", "outwardReady", "silentFallback", "singleCredentialExact", "vaultCoordinatesExact"]);
|
|
462
|
+
allTrue(["innerReady", "laneSelectionExact", "outwardReady", "singleCredentialExact", "vaultCoordinatesExact"]);
|
|
463
|
+
requiredFalse(value, "silentFallback", label);
|
|
464
|
+
break;
|
|
465
|
+
case "unit-16d-whats-up":
|
|
466
|
+
exact(["accurate", "authorized", "grounded", "liveFactsMatched", "responseCount", "responseWithinLimit", "telegramDelivered"]);
|
|
467
|
+
allTrue(["accurate", "authorized", "grounded", "liveFactsMatched", "responseWithinLimit", "telegramDelivered"]);
|
|
468
|
+
requiredInteger(value, "responseCount", 1, label);
|
|
469
|
+
break;
|
|
470
|
+
case "unit-16d-1-space":
|
|
471
|
+
exact(["accurate", "authorized", "grounded", "liveFactsMatched", "mutationCount", "responseCount", "responseWithinLimit", "telegramDelivered"]);
|
|
472
|
+
allTrue(["accurate", "authorized", "grounded", "liveFactsMatched", "responseWithinLimit", "telegramDelivered"]);
|
|
473
|
+
requiredInteger(value, "responseCount", 1, label);
|
|
474
|
+
requiredInteger(value, "mutationCount", 0, label);
|
|
475
|
+
break;
|
|
476
|
+
case "unit-16d-2-unknown-admission":
|
|
477
|
+
exact(["acknowledgementSent", "agentTurnCount", "distinctAccount", "mutationCount", "ownerCardSent", "providerInvocationCount", "quarantined", "responseCount", "workItemCount"]);
|
|
478
|
+
allTrue(["acknowledgementSent", "distinctAccount", "ownerCardSent", "quarantined"]);
|
|
479
|
+
allZero(["agentTurnCount", "mutationCount", "providerInvocationCount", "responseCount", "workItemCount"]);
|
|
480
|
+
break;
|
|
481
|
+
case "unit-16e-containment-audit":
|
|
482
|
+
exact([
|
|
483
|
+
"schemaVersion", "keyCount", "keyInventoryDigest", "readScopeDigest", "writeScopeDigest", "keyRoleAssignmentCount",
|
|
484
|
+
"profileBoundaries",
|
|
485
|
+
"auditPathDigest", "auditLedgerDigest", "auditRecordCount", "auditLifecyclePairCount",
|
|
486
|
+
"containerUser", "liveProcessUser", "mountCount", "publishedPortCount", "networkMode", "readOnlyRoot", "mountsExact", "securityExact", "updaterDisabled", "writableKeyExposure",
|
|
487
|
+
"rawWriteMaterialFieldCount", "typedWriteExecutorCount", "writeApprovalPolicyDigest", "writeApprovalPolicyExact", "sensitiveMaterialObserved", "mutationCount",
|
|
488
|
+
]);
|
|
489
|
+
if (text(value.schemaVersion, `${label} schemaVersion`) !== "sanctuary-containment-audit-v2")
|
|
490
|
+
throw new Error(`${label} schemaVersion is invalid`);
|
|
491
|
+
for (const key of ["keyInventoryDigest", "readScopeDigest", "writeScopeDigest", "auditPathDigest", "auditLedgerDigest", "writeApprovalPolicyDigest"])
|
|
492
|
+
opaqueDigest(value[key], `${label} ${key}`);
|
|
493
|
+
for (const [key, expected] of Object.entries({
|
|
494
|
+
readScopeDigest: "9914469afdcb574937d1020a03faa82e3c02d767169d3eccae4b81863dafa06e",
|
|
495
|
+
writeScopeDigest: "1de873b2bc3c7769010c32c69fcc8ea55343a5647cfdb0294769e831142945ec",
|
|
496
|
+
auditPathDigest: "1cb8f1a00c544a5d10b0577090dbf070a07a5b6a99de13ccd27c11a257f84b75",
|
|
497
|
+
}))
|
|
498
|
+
if (value[key] !== expected)
|
|
499
|
+
throw new Error(`${label} ${key} does not match the canonical contract`);
|
|
500
|
+
requiredInteger(value, "keyCount", 2, label);
|
|
501
|
+
requiredInteger(value, "keyRoleAssignmentCount", 0, label);
|
|
502
|
+
if (!exactSanctuaryContainmentProfileBoundaries(value.profileBoundaries))
|
|
503
|
+
throw new Error(`${label} profileBoundaries do not match the canonical contract`);
|
|
504
|
+
allZero(["publishedPortCount", "rawWriteMaterialFieldCount", "mutationCount"]);
|
|
505
|
+
requiredFalse(value, "sensitiveMaterialObserved", label);
|
|
506
|
+
requiredFalse(value, "writableKeyExposure", label);
|
|
507
|
+
requiredFalse(value, "readOnlyRoot", label);
|
|
508
|
+
integer(value.auditRecordCount, `${label} auditRecordCount`, 2);
|
|
509
|
+
integer(value.auditLifecyclePairCount, `${label} auditLifecyclePairCount`, 1);
|
|
510
|
+
if (text(value.containerUser, `${label} containerUser`) !== "10001:10001" || text(value.liveProcessUser, `${label} liveProcessUser`) !== "10001:10001" || text(value.networkMode, `${label} networkMode`) !== "host")
|
|
511
|
+
throw new Error(`${label} container identity or network is invalid`);
|
|
512
|
+
requiredInteger(value, "mountCount", 4, label);
|
|
513
|
+
requiredInteger(value, "typedWriteExecutorCount", 1, label);
|
|
514
|
+
allTrue(["writeApprovalPolicyExact", "mountsExact", "securityExact", "updaterDisabled"]);
|
|
515
|
+
break;
|
|
516
|
+
case "unit-16e-1-stop-denial":
|
|
517
|
+
case "unit-16e-2-restart-denial":
|
|
518
|
+
exact(["attemptCount", "cursorBoundaryCount", "denied", "mutationCount", "restartCountUnchanged", "resumed"]);
|
|
519
|
+
allTrue(["denied", "restartCountUnchanged", "resumed"]);
|
|
520
|
+
requiredInteger(value, "attemptCount", 1, label);
|
|
521
|
+
requiredInteger(value, "cursorBoundaryCount", 7, label);
|
|
522
|
+
requiredInteger(value, "mutationCount", 0, label);
|
|
523
|
+
break;
|
|
524
|
+
case "unit-16f-cron-fingerprint":
|
|
525
|
+
exact(["fingerprintUnchanged", "messageCount", "providerInvocationCount", "receiptUnchanged", "scheduleRegistered", "sweepObserved"]);
|
|
526
|
+
allTrue(["fingerprintUnchanged", "receiptUnchanged", "scheduleRegistered", "sweepObserved"]);
|
|
527
|
+
allZero(["messageCount", "providerInvocationCount"]);
|
|
528
|
+
break;
|
|
529
|
+
case "unit-16g-health-transition":
|
|
530
|
+
exact(["alertCount", "productionRestored", "transitionObserved"]);
|
|
531
|
+
allTrue(["productionRestored", "transitionObserved"]);
|
|
532
|
+
requiredInteger(value, "alertCount", 3, label);
|
|
533
|
+
break;
|
|
534
|
+
case "unit-16h-acceptance-delivery-probe":
|
|
535
|
+
exact(["acceptanceOnly", "deliveryPathObserved", "firedWithinMs", "messageCount", "productionRestored", "productionScheduleChanged"]);
|
|
536
|
+
allTrue(["acceptanceOnly", "deliveryPathObserved", "productionRestored"]);
|
|
537
|
+
requiredFalse(value, "productionScheduleChanged", label);
|
|
538
|
+
requiredInteger(value, "messageCount", 1, label);
|
|
539
|
+
if (integer(value.firedWithinMs, `${label} firedWithinMs`, 0) > 960_000)
|
|
540
|
+
throw new Error(`${label} fired outside the 16-minute bound`);
|
|
541
|
+
break;
|
|
542
|
+
case "unit-16i-delayed-approval":
|
|
543
|
+
exact(["elapsedMs", "mutationCount", "promptTerminal", "replayMutationCount", "resumed", "state"]);
|
|
544
|
+
if (integer(value.elapsedMs, `${label} elapsedMs`, 1) < 120_000)
|
|
545
|
+
throw new Error(`${label} did not remain suspended for 120 seconds`);
|
|
546
|
+
requiredInteger(value, "mutationCount", 1, label);
|
|
547
|
+
requiredInteger(value, "replayMutationCount", 0, label);
|
|
548
|
+
allTrue(["promptTerminal", "resumed"]);
|
|
549
|
+
if (value.state !== "succeeded")
|
|
550
|
+
throw new Error(`${label} state must be succeeded`);
|
|
551
|
+
break;
|
|
552
|
+
case "unit-16j-denial":
|
|
553
|
+
exact(["mutationCount", "promptTerminal", "replayMutationCount", "resumed", "state"]);
|
|
554
|
+
allZero(["mutationCount", "replayMutationCount"]);
|
|
555
|
+
allTrue(["promptTerminal", "resumed"]);
|
|
556
|
+
if (value.state !== "denied")
|
|
557
|
+
throw new Error(`${label} state must be denied`);
|
|
558
|
+
break;
|
|
559
|
+
case "unit-16k-timeout-stale":
|
|
560
|
+
exact(["buttonsRemoved", "mutationCount", "promptTerminal", "staleAcknowledged", "staleReplayMutationCount", "state"]);
|
|
561
|
+
allTrue(["buttonsRemoved", "promptTerminal", "staleAcknowledged"]);
|
|
562
|
+
allZero(["mutationCount", "staleReplayMutationCount"]);
|
|
563
|
+
if (value.state !== "expired")
|
|
564
|
+
throw new Error(`${label} state must be expired`);
|
|
565
|
+
break;
|
|
566
|
+
case "unit-16l-duplicate-callback":
|
|
567
|
+
exact(["callbackCount", "claimCount", "mutationCount", "promptTerminal", "replayMutationCount", "settledCount", "staleReplaySettled", "writeCredentialAbsent"]);
|
|
568
|
+
requiredInteger(value, "callbackCount", 2, label);
|
|
569
|
+
requiredInteger(value, "claimCount", 1, label);
|
|
570
|
+
requiredInteger(value, "mutationCount", 1, label);
|
|
571
|
+
requiredInteger(value, "settledCount", 2, label);
|
|
572
|
+
requiredInteger(value, "replayMutationCount", 0, label);
|
|
573
|
+
allTrue(["promptTerminal", "staleReplaySettled", "writeCredentialAbsent"]);
|
|
574
|
+
break;
|
|
575
|
+
case "unit-16m-restart-continuation":
|
|
576
|
+
exact(["attemptedIndeterminateRetryCount", "butlerRestartObserved", "checkpointEpochPreserved", "continuationEpochAdvanced", "mutationCount", "preAttemptResumed", "restartObserved", "state"]);
|
|
577
|
+
allTrue(["butlerRestartObserved", "checkpointEpochPreserved", "continuationEpochAdvanced", "preAttemptResumed", "restartObserved"]);
|
|
578
|
+
requiredInteger(value, "attemptedIndeterminateRetryCount", 0, label);
|
|
579
|
+
requiredInteger(value, "mutationCount", 1, label);
|
|
580
|
+
if (value.state !== "succeeded")
|
|
581
|
+
throw new Error(`${label} state must be succeeded`);
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
return value;
|
|
585
|
+
}
|
|
586
|
+
const RAW_LONG_DECIMAL = /^-?\d{5,16}$/u;
|
|
587
|
+
const EPOCH_MILLISECONDS_MIN = 1_577_836_800_000;
|
|
588
|
+
const EPOCH_MILLISECONDS_MAX = 4_102_444_800_000;
|
|
589
|
+
const TYPED_TIMESTAMP_KEYS = new Set(["capturedAt", "completedAt", "requestedAt", "startedAt"]);
|
|
590
|
+
const TYPED_SCENARIO_NUMERIC_KEYS = new Set([
|
|
591
|
+
"activePollers", "alertCount", "attemptedIndeterminateRetryCount", "callbackCount", "claimCount",
|
|
592
|
+
"elapsedMs", "firedWithinMs", "messageCount", "mountCount", "mutationCount", "nonRootUid",
|
|
593
|
+
"providerInvocationCount", "publishedPortCount", "replayMutationCount", "responseCount",
|
|
594
|
+
"settledCount", "staleReplayMutationCount", "ttlMs", "unrelatedHostOperations", "workItemCount",
|
|
595
|
+
]);
|
|
596
|
+
function assertRedactedEvidence(value, label, key = "") {
|
|
597
|
+
if (Array.isArray(value)) {
|
|
598
|
+
for (const item of value)
|
|
599
|
+
assertRedactedEvidence(item, label, key);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (value && typeof value === "object") {
|
|
603
|
+
if (key === "evidenceCounters") {
|
|
604
|
+
const counters = object(value, `${label} evidenceCounters`);
|
|
605
|
+
for (const [counterName, counterValue] of Object.entries(counters)) {
|
|
606
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/u.test(counterName) || !Number.isSafeInteger(counterValue) || counterValue < 0) {
|
|
607
|
+
throw new Error(`${label} evidenceCounters contain an invalid or raw Telegram identity-like counter`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
for (const [key, item] of Object.entries(value)) {
|
|
613
|
+
if (key === "writeCredentialAbsent" && item === true)
|
|
614
|
+
continue;
|
|
615
|
+
if (key === "singleCredentialExact" && typeof item === "boolean")
|
|
616
|
+
continue;
|
|
617
|
+
if (/(?:telegram)?(?:user|chat|update|message)[_-]?id|token|secret|password|credential|api[_-]?key/iu.test(key)) {
|
|
618
|
+
throw new Error(`${label} contains a sensitive or raw Telegram identity field`);
|
|
619
|
+
}
|
|
620
|
+
assertRedactedEvidence(item, label, key);
|
|
621
|
+
}
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if (typeof value === "string" && /\b\d{5,}:[A-Za-z0-9_-]{20,}\b/u.test(value)) {
|
|
625
|
+
throw new Error(`${label} contains sensitive material`);
|
|
626
|
+
}
|
|
627
|
+
if (((typeof value === "string" && RAW_LONG_DECIMAL.test(value))
|
|
628
|
+
|| (typeof value === "number" && Number.isSafeInteger(value) && Math.abs(value) >= 10_000))
|
|
629
|
+
&& !(typeof value === "number" && TYPED_TIMESTAMP_KEYS.has(key)
|
|
630
|
+
&& value >= EPOCH_MILLISECONDS_MIN && value <= EPOCH_MILLISECONDS_MAX)
|
|
631
|
+
&& !(typeof value === "number" && TYPED_SCENARIO_NUMERIC_KEYS.has(key) && value >= 0)) {
|
|
632
|
+
throw new Error(`${label} contains a raw Telegram identity-like decimal value`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
function normalizedEvidenceHash(value) {
|
|
636
|
+
return (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(value)).digest("hex");
|
|
637
|
+
}
|
|
638
|
+
function exactEvidenceLabels(entries) {
|
|
639
|
+
return JSON.stringify(entries.map((entry) => entry.label).sort())
|
|
640
|
+
=== JSON.stringify([...exports.SANCTUARY_UNIT_16_EVIDENCE_LABELS].sort());
|
|
641
|
+
}
|
|
642
|
+
exports.SANCTUARY_SCENARIO_GATES = {
|
|
643
|
+
"unit-12c-1-opaque-identity": "none",
|
|
644
|
+
"unit-14b-3-opaque-identity-live": "authorized-telegram-message",
|
|
645
|
+
"unit-15c-1-no-callback-terminalization": "authorized-telegram-restart-no-callback",
|
|
646
|
+
"unit-16a-pre-reboot-checkpoint": "none",
|
|
647
|
+
"unit-16a-reboot-request": "none",
|
|
648
|
+
"unit-16a-boot-recovery-milestones": "none",
|
|
649
|
+
"unit-16b-runtime-vault-readiness": "none",
|
|
650
|
+
"unit-16c-provider-readiness": "none",
|
|
651
|
+
"unit-16d-whats-up": "authorized-telegram-message",
|
|
652
|
+
"unit-16d-1-space": "authorized-telegram-message",
|
|
653
|
+
"unit-16d-2-unknown-admission": "unknown-telegram-admission",
|
|
654
|
+
"unit-16e-containment-audit": "none",
|
|
655
|
+
"unit-16e-1-stop-denial": "none",
|
|
656
|
+
"unit-16e-2-restart-denial": "none",
|
|
657
|
+
"unit-16f-cron-fingerprint": "none",
|
|
658
|
+
"unit-16g-health-transition": "none",
|
|
659
|
+
"unit-16h-acceptance-delivery-probe": "none",
|
|
660
|
+
"unit-16i-delayed-approval": "telegram-delayed-approve",
|
|
661
|
+
"unit-16j-denial": "telegram-deny",
|
|
662
|
+
"unit-16k-timeout-stale": "telegram-stale-callback",
|
|
663
|
+
"unit-16l-duplicate-callback": "telegram-concurrent-callback",
|
|
664
|
+
"unit-16m-restart-continuation": "telegram-restart-approve",
|
|
665
|
+
};
|
|
666
|
+
exports.SANCTUARY_SCENARIO_SOURCES = {
|
|
667
|
+
"unit-12c-1-opaque-identity": ["identity-key", "identity-surface-audit", "approval-journal"],
|
|
668
|
+
"unit-14b-3-opaque-identity-live": ["identity-key", "identity-surface-audit", "telegram-audit", "approval-journal", "telegram-turn-receipts"],
|
|
669
|
+
"unit-15c-1-no-callback-terminalization": ["identity-key", "telegram-audit", "telegram-offset", "approval-journal", "approval-checkpoints", "container-inspect"],
|
|
670
|
+
"unit-16a-pre-reboot-checkpoint": ["telegram-audit", "telegram-offset", "approval-journal", "container-inspect", "cron-runtime", "reboot-checkpoint"],
|
|
671
|
+
"unit-16a-reboot-request": ["reboot-checkpoint"],
|
|
672
|
+
"unit-16a-boot-recovery-milestones": ["reboot-checkpoint", "container-inspect"],
|
|
673
|
+
"unit-16b-runtime-vault-readiness": ["container-inspect"],
|
|
674
|
+
"unit-16c-provider-readiness": ["provider-live-check"],
|
|
675
|
+
"unit-16d-whats-up": ["telegram-audit", "telegram-offset", "telegram-turn-receipts", "live-grounding-read"],
|
|
676
|
+
"unit-16d-1-space": ["telegram-audit", "telegram-offset", "telegram-turn-receipts", "restart-attempt-ledger", "container-inspect", "live-grounding-read"],
|
|
677
|
+
"unit-16d-2-unknown-admission": ["identity-key", "telegram-audit", "telegram-offset", "telegram-turn-receipts", "telegram-admission-evidence", "approval-journal", "restart-attempt-ledger", "container-inspect", "containment-audit"],
|
|
678
|
+
"unit-16e-containment-audit": ["telegram-audit", "container-inspect", "containment-audit"],
|
|
679
|
+
"unit-16e-1-stop-denial": ["read-only-denial-receipt", "container-inspect"],
|
|
680
|
+
"unit-16e-2-restart-denial": ["read-only-denial-receipt", "container-inspect"],
|
|
681
|
+
"unit-16f-cron-fingerprint": ["health-probe-receipt", "scheduler-liveness-receipt", "cron-runtime", "telegram-audit", "container-inspect"],
|
|
682
|
+
"unit-16g-health-transition": ["health-probe-receipt", "telegram-audit", "container-inspect"],
|
|
683
|
+
"unit-16h-acceptance-delivery-probe": ["health-probe-receipt", "cron-runtime", "telegram-audit", "container-inspect"],
|
|
684
|
+
"unit-16i-delayed-approval": ["identity-key", "telegram-audit", "approval-journal", "approval-checkpoints", "restart-attempt-ledger", "container-inspect"],
|
|
685
|
+
"unit-16j-denial": ["identity-key", "telegram-audit", "approval-journal", "approval-checkpoints", "restart-attempt-ledger", "container-inspect"],
|
|
686
|
+
"unit-16k-timeout-stale": ["identity-key", "telegram-audit", "approval-journal", "approval-checkpoints", "restart-attempt-ledger", "container-inspect", "interactive-driver-receipt"],
|
|
687
|
+
"unit-16l-duplicate-callback": ["identity-key", "telegram-audit", "approval-journal", "approval-checkpoints", "restart-attempt-ledger", "container-inspect", "interactive-driver-receipt"],
|
|
688
|
+
"unit-16m-restart-continuation": ["identity-key", "telegram-audit", "approval-journal", "approval-checkpoints", "restart-attempt-ledger", "container-inspect", "interactive-driver-receipt"],
|
|
689
|
+
};
|
|
690
|
+
function packagedHarnessSha256(value, deps) {
|
|
691
|
+
const configured = text(value, "harnessPath");
|
|
692
|
+
if (!path.isAbsolute(configured))
|
|
693
|
+
throw new Error("harnessPath must be absolute");
|
|
694
|
+
const resolved = path.resolve(configured);
|
|
695
|
+
const handle = (0, node_fs_1.openSync)(resolved, node_fs_2.constants.O_RDONLY | node_fs_2.constants.O_NOFOLLOW);
|
|
696
|
+
try {
|
|
697
|
+
const metadata = (0, node_fs_1.fstatSync)(handle);
|
|
698
|
+
if (!metadata.isFile())
|
|
699
|
+
throw new Error("packaged harness must be a regular file");
|
|
700
|
+
if ((metadata.mode & 0o022) !== 0)
|
|
701
|
+
throw new Error("packaged harness must not be group- or world-writable");
|
|
702
|
+
if (deps.realpath(resolved) !== resolved)
|
|
703
|
+
throw new Error("packaged harness path must be canonical");
|
|
704
|
+
return (0, node_crypto_1.createHash)("sha256").update((0, node_fs_1.readFileSync)(handle)).digest("hex");
|
|
705
|
+
}
|
|
706
|
+
finally {
|
|
707
|
+
(0, node_fs_1.closeSync)(handle);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
function evidenceProvenance(value, label) {
|
|
711
|
+
const provenance = object(value, `${label} provenance`);
|
|
712
|
+
const expectedKeys = ["containerDigest", "cursorDigest", "harnessSha256", "imageDigest"];
|
|
713
|
+
if (JSON.stringify(Object.keys(provenance).sort()) !== JSON.stringify(expectedKeys)) {
|
|
714
|
+
throw new Error(`${label} provenance must contain the exact continuity coordinates`);
|
|
715
|
+
}
|
|
716
|
+
return {
|
|
717
|
+
imageDigest: opaqueDigest(provenance.imageDigest, `${label} imageDigest`),
|
|
718
|
+
containerDigest: opaqueDigest(provenance.containerDigest, `${label} containerDigest`),
|
|
719
|
+
cursorDigest: opaqueDigest(provenance.cursorDigest, `${label} cursorDigest`),
|
|
720
|
+
harnessSha256: opaqueDigest(provenance.harnessSha256, `${label} harnessSha256`),
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
function completeEvidenceContract(value, label) {
|
|
724
|
+
exactObjectKeys(value, ["assertions", "operation", "phase", "producer", "provenance", "schemaVersion"], `${label} evidence`);
|
|
725
|
+
if (value.schemaVersion !== 1 || value.operation !== label || value.phase !== "complete") {
|
|
726
|
+
throw new Error(`${label} evidence contract must use schemaVersion 1, its exact operation, and phase complete`);
|
|
727
|
+
}
|
|
728
|
+
const assertions = validateSanctuaryUnit16EvidenceAssertions(label, value.assertions);
|
|
729
|
+
const producer = object(value.producer, `${label} producer`);
|
|
730
|
+
exactObjectKeys(producer, ["adapterOperation", "captureDigest", "checkpointDigest", "command", "sourceDigest"], `${label} producer`);
|
|
731
|
+
if (producer.command !== SANCTUARY_SCENARIO_COMMAND || producer.adapterOperation !== SANCTUARY_SCENARIO_ADAPTER_OPERATION) {
|
|
732
|
+
throw new Error(`${label} evidence was not produced by the fixed packaged scenario command`);
|
|
733
|
+
}
|
|
734
|
+
opaqueDigest(producer.checkpointDigest, `${label} checkpointDigest`);
|
|
735
|
+
opaqueDigest(producer.sourceDigest, `${label} sourceDigest`);
|
|
736
|
+
if (opaqueDigest(producer.captureDigest, `${label} captureDigest`) !== normalizedEvidenceHash(assertions)) {
|
|
737
|
+
throw new Error(`${label} capture digest does not bind its typed assertions`);
|
|
738
|
+
}
|
|
739
|
+
return evidenceProvenance(value.provenance, label);
|
|
740
|
+
}
|
|
741
|
+
function sameStableProvenance(left, right) {
|
|
742
|
+
return left.imageDigest === right.imageDigest
|
|
743
|
+
&& left.containerDigest === right.containerDigest
|
|
744
|
+
&& left.harnessSha256 === right.harnessSha256;
|
|
745
|
+
}
|
|
746
|
+
async function liveEvidenceProvenance(config, deps, deadline) {
|
|
747
|
+
const executable = adapter(config.provenanceAdapter, "provenanceAdapter");
|
|
748
|
+
if (executable !== PACKAGED_PROVENANCE_ADAPTER)
|
|
749
|
+
throw new Error("provenanceAdapter must be the packaged Sanctuary acceptance adapter");
|
|
750
|
+
const remainingMs = deadline === undefined ? undefined : deadline - deps.now();
|
|
751
|
+
if (remainingMs !== undefined && remainingMs <= 0)
|
|
752
|
+
throw new Error("live evidence provenance exceeded its remaining deadline");
|
|
753
|
+
const capture = object(await deps.runAdapter(executable, {
|
|
754
|
+
operation: "capture_evidence_provenance",
|
|
755
|
+
schema: "sanctuary-unit-16-provenance-v1",
|
|
756
|
+
}, remainingMs), "live provenance");
|
|
757
|
+
const expectedKeys = ["containerDigest", "cursorDigest", "imageDigest"];
|
|
758
|
+
if (JSON.stringify(Object.keys(capture).sort()) !== JSON.stringify(expectedKeys)) {
|
|
759
|
+
throw new Error("live provenance must contain the exact image, container, and cursor coordinates");
|
|
760
|
+
}
|
|
761
|
+
return {
|
|
762
|
+
imageDigest: opaqueDigest(capture.imageDigest, "live provenance imageDigest"),
|
|
763
|
+
containerDigest: opaqueDigest(capture.containerDigest, "live provenance containerDigest"),
|
|
764
|
+
cursorDigest: opaqueDigest(capture.cursorDigest, "live provenance cursorDigest"),
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
function matchesLiveProvenance(provenance, live) {
|
|
768
|
+
return provenance.imageDigest === live.imageDigest
|
|
769
|
+
&& provenance.containerDigest === live.containerDigest
|
|
770
|
+
&& provenance.cursorDigest === live.cursorDigest;
|
|
771
|
+
}
|
|
772
|
+
async function evidenceBundleIndex(config, deps) {
|
|
773
|
+
const root = privateAllowedRoot(config);
|
|
774
|
+
const evidencePath = confinedPath(root, config.evidencePath, "evidencePath");
|
|
775
|
+
refuseExistingCheckpoint(root, evidencePath);
|
|
776
|
+
if (!Array.isArray(config.entries))
|
|
777
|
+
throw new Error("evidence entries must be an array");
|
|
778
|
+
const requested = config.entries.map((raw) => {
|
|
779
|
+
const entry = object(raw, "evidence entry");
|
|
780
|
+
return { label: text(entry.label, "evidence entry label"), path: entry.path };
|
|
781
|
+
});
|
|
782
|
+
if (!exactEvidenceLabels(requested) || new Set(requested.map((entry) => entry.label)).size !== requested.length) {
|
|
783
|
+
throw new Error("evidence entries must equal the complete Unit 16 evidence matrix");
|
|
784
|
+
}
|
|
785
|
+
const harnessSha256 = packagedHarnessSha256(config.harnessPath, deps);
|
|
786
|
+
const byLabel = new Map(requested.map((entry) => [entry.label, entry.path]));
|
|
787
|
+
let continuity;
|
|
788
|
+
let latest;
|
|
789
|
+
const entries = exports.SANCTUARY_UNIT_16_EVIDENCE_LABELS.map((label) => {
|
|
790
|
+
const source = requirePrivateRegularFile(root, byLabel.get(label), `${label} evidence`);
|
|
791
|
+
const value = object(JSON.parse((0, node_fs_1.readFileSync)(source, "utf8")), `${label} evidence`);
|
|
792
|
+
assertRedactedEvidence(value, label);
|
|
793
|
+
const provenance = completeEvidenceContract(value, label);
|
|
794
|
+
if (provenance.harnessSha256 !== harnessSha256)
|
|
795
|
+
throw new Error(`${label} harness provenance does not match the packaged harness bytes`);
|
|
796
|
+
continuity ??= provenance;
|
|
797
|
+
if (!sameStableProvenance(continuity, provenance))
|
|
798
|
+
throw new Error(`${label} provenance breaks image, container, or harness continuity`);
|
|
799
|
+
latest = provenance;
|
|
800
|
+
return { label, sha256: normalizedEvidenceHash(value), evidence: value };
|
|
801
|
+
});
|
|
802
|
+
const live = await liveEvidenceProvenance(config, deps);
|
|
803
|
+
const finalContinuity = latest;
|
|
804
|
+
if (!matchesLiveProvenance(finalContinuity, live)) {
|
|
805
|
+
throw new Error("evidence coordinates do not match trusted live provenance");
|
|
806
|
+
}
|
|
807
|
+
const core = {
|
|
808
|
+
schemaVersion: 1,
|
|
809
|
+
operation: "sanctuary-unit-16-evidence-bundle",
|
|
810
|
+
phase: "complete",
|
|
811
|
+
imageDigest: finalContinuity.imageDigest,
|
|
812
|
+
containerDigest: finalContinuity.containerDigest,
|
|
813
|
+
cursorDigest: finalContinuity.cursorDigest,
|
|
814
|
+
harnessSha256,
|
|
815
|
+
entries,
|
|
816
|
+
};
|
|
817
|
+
initializeCheckpoint(root, evidencePath, { ...core, bundleDigest: normalizedEvidenceHash(core), completedAt: deps.now() });
|
|
818
|
+
}
|
|
819
|
+
async function verifyEvidenceBundle(config, deps) {
|
|
820
|
+
const root = privateAllowedRoot(config);
|
|
821
|
+
const bundle = readCheckpoint(root, config.evidencePath);
|
|
822
|
+
const harnessSha256 = packagedHarnessSha256(config.harnessPath, deps);
|
|
823
|
+
if (bundle.schemaVersion !== 1 || bundle.operation !== "sanctuary-unit-16-evidence-bundle" || bundle.phase !== "complete") {
|
|
824
|
+
throw new Error("evidence bundle header is invalid");
|
|
825
|
+
}
|
|
826
|
+
if (!Array.isArray(bundle.entries))
|
|
827
|
+
throw new Error("evidence bundle entries must be an array");
|
|
828
|
+
let continuity;
|
|
829
|
+
let latest;
|
|
830
|
+
const entries = bundle.entries.map((raw) => {
|
|
831
|
+
const entry = object(raw, "evidence bundle entry");
|
|
832
|
+
const label = text(entry.label, "evidence bundle label");
|
|
833
|
+
const evidence = object(entry.evidence, `${label} evidence`);
|
|
834
|
+
assertRedactedEvidence(evidence, label);
|
|
835
|
+
const provenance = completeEvidenceContract(evidence, label);
|
|
836
|
+
if (provenance.harnessSha256 !== harnessSha256)
|
|
837
|
+
throw new Error(`${label} harness provenance does not match the packaged harness bytes`);
|
|
838
|
+
continuity ??= provenance;
|
|
839
|
+
if (!sameStableProvenance(continuity, provenance))
|
|
840
|
+
throw new Error(`${label} provenance breaks image, container, or harness continuity`);
|
|
841
|
+
latest = provenance;
|
|
842
|
+
const sha256 = opaqueDigest(entry.sha256, `${label} entry hash`);
|
|
843
|
+
if (sha256 !== normalizedEvidenceHash(evidence))
|
|
844
|
+
throw new Error("evidence bundle entry hash mismatch");
|
|
845
|
+
return { label, sha256, evidence };
|
|
846
|
+
});
|
|
847
|
+
if (!exactEvidenceLabels(entries) || new Set(entries.map((entry) => entry.label)).size !== entries.length) {
|
|
848
|
+
throw new Error("evidence bundle does not contain the complete Unit 16 evidence matrix");
|
|
849
|
+
}
|
|
850
|
+
const live = await liveEvidenceProvenance(config, deps);
|
|
851
|
+
const finalContinuity = latest;
|
|
852
|
+
if (!matchesLiveProvenance(finalContinuity, live)) {
|
|
853
|
+
throw new Error("evidence bundle coordinates do not match trusted live provenance");
|
|
854
|
+
}
|
|
855
|
+
const core = {
|
|
856
|
+
schemaVersion: 1,
|
|
857
|
+
operation: "sanctuary-unit-16-evidence-bundle",
|
|
858
|
+
phase: "complete",
|
|
859
|
+
imageDigest: opaqueDigest(bundle.imageDigest, "bundle imageDigest"),
|
|
860
|
+
containerDigest: opaqueDigest(bundle.containerDigest, "bundle containerDigest"),
|
|
861
|
+
cursorDigest: opaqueDigest(bundle.cursorDigest, "bundle cursorDigest"),
|
|
862
|
+
harnessSha256: opaqueDigest(bundle.harnessSha256, "bundle harnessSha256"),
|
|
863
|
+
entries,
|
|
864
|
+
};
|
|
865
|
+
if (core.imageDigest !== finalContinuity.imageDigest || core.containerDigest !== finalContinuity.containerDigest
|
|
866
|
+
|| core.cursorDigest !== finalContinuity.cursorDigest || core.harnessSha256 !== finalContinuity.harnessSha256) {
|
|
867
|
+
throw new Error("evidence bundle continuity coordinates do not match its entries");
|
|
868
|
+
}
|
|
869
|
+
if (opaqueDigest(bundle.bundleDigest, "bundle digest") !== normalizedEvidenceHash(core))
|
|
870
|
+
throw new Error("evidence bundle digest mismatch");
|
|
871
|
+
}
|
|
872
|
+
async function telegramBootstrap(config, deps) {
|
|
873
|
+
const root = privateAllowedRoot(config);
|
|
874
|
+
const evidencePath = confinedPath(root, config.evidencePath, "evidencePath");
|
|
875
|
+
refuseExistingCheckpoint(root, evidencePath);
|
|
876
|
+
const offsetPath = confinedPath(root, config.offsetPath, "offsetPath");
|
|
877
|
+
const expectedBotId = text(config.expectedBotId, "expectedBotId");
|
|
878
|
+
const expectedUsername = text(config.expectedUsername, "expectedUsername");
|
|
879
|
+
const currentOffset = integer(config.currentOffset, "currentOffset");
|
|
880
|
+
const noncePath = confinedPath(root, config.noncePath, "noncePath");
|
|
881
|
+
refuseExistingCheckpoint(root, noncePath);
|
|
882
|
+
const pollerAdapter = adapter(config.pollerAdapter, "pollerAdapter");
|
|
883
|
+
if (pollerAdapter !== PACKAGED_PROVENANCE_ADAPTER)
|
|
884
|
+
throw new Error("Telegram bootstrap requires the fixed packaged acceptance adapter");
|
|
885
|
+
const deadlineMs = integer(config.deadlineMs, "deadlineMs", 300_000);
|
|
886
|
+
if (deadlineMs > 900_000)
|
|
887
|
+
throw new Error("Telegram bootstrap deadline exceeds 15 minutes");
|
|
888
|
+
const pollTimeoutSeconds = integer(config.pollTimeoutSeconds, "pollTimeoutSeconds", 1);
|
|
889
|
+
if (pollTimeoutSeconds > 50)
|
|
890
|
+
throw new Error("Telegram poll timeout exceeds 50 seconds");
|
|
891
|
+
const gateway = deps.gateway?.();
|
|
892
|
+
if (!gateway)
|
|
893
|
+
throw new Error("Telegram root gateway is unavailable");
|
|
894
|
+
async function bounded(operation, timeoutMs = deps.telegramTimeoutMs ?? DEFAULT_TELEGRAM_TIMEOUT_MS) {
|
|
895
|
+
let timer;
|
|
896
|
+
try {
|
|
897
|
+
return await Promise.race([
|
|
898
|
+
operation(),
|
|
899
|
+
new Promise((_resolve, reject) => {
|
|
900
|
+
timer = setTimeout(() => reject(new Error("Telegram gateway request timed out")), timeoutMs);
|
|
901
|
+
}),
|
|
902
|
+
]);
|
|
903
|
+
}
|
|
904
|
+
finally {
|
|
905
|
+
clearTimeout(timer);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
try {
|
|
909
|
+
if (!await bounded(async () => gateway.authorityTransport.hostApproval?.refresh()))
|
|
910
|
+
throw new Error("Telegram root gateway health is unavailable");
|
|
911
|
+
const initialCursor = await bounded(() => gateway.cursorSnapshot());
|
|
912
|
+
if (initialCursor.cursor !== currentOffset)
|
|
913
|
+
throw new Error("Telegram gateway cursor changed before bootstrap");
|
|
914
|
+
let getMe;
|
|
915
|
+
try {
|
|
916
|
+
getMe = await bounded(() => gateway.authorityTransport.api.request("getMe", {}));
|
|
917
|
+
}
|
|
918
|
+
catch (error) {
|
|
919
|
+
throw telegramBootstrapRequestError("getMe", error);
|
|
920
|
+
}
|
|
921
|
+
const bot = object(getMe, "Telegram getMe result");
|
|
922
|
+
if (String(bot.id) !== expectedBotId || gateway.credentials.botId !== expectedBotId || bot.username !== expectedUsername) {
|
|
923
|
+
throw new Error("Telegram bot identity mismatch; repair root gateway identity");
|
|
924
|
+
}
|
|
925
|
+
const nonce = deps.randomBytes(16).toString("hex");
|
|
926
|
+
const base = {
|
|
927
|
+
schemaVersion: 1, operation: "telegram-bootstrap", phase: "preflight",
|
|
928
|
+
botIdentityDigest: digest({ id: expectedBotId, username: expectedUsername }), startedAt: deps.now(),
|
|
929
|
+
};
|
|
930
|
+
initializeCheckpoint(root, evidencePath, base);
|
|
931
|
+
try {
|
|
932
|
+
const quiesced = object(await deps.runAdapter(pollerAdapter, { operation: "quiesce_telegram_poller", expectedState: "stopped" }), "Telegram poller precondition");
|
|
933
|
+
exactObjectKeys(quiesced, ["activePollers", "quiesced"], "Telegram poller precondition");
|
|
934
|
+
if (quiesced.quiesced !== true || quiesced.activePollers !== 1)
|
|
935
|
+
throw new Error("Telegram root poller or stopped resident is not proven");
|
|
936
|
+
writeAtomicPrivateText(root, noncePath, nonce);
|
|
937
|
+
const deadline = deps.now() + deadlineMs;
|
|
938
|
+
let match;
|
|
939
|
+
while (deps.now() < deadline && !match) {
|
|
940
|
+
let updates;
|
|
941
|
+
try {
|
|
942
|
+
updates = await bounded(() => gateway.authorityTransport.api.request("getUpdates", { offset: currentOffset, timeout: pollTimeoutSeconds, allowed_updates: ["message", "callback_query"] }), Math.min(deps.telegramTimeoutMs ?? DEFAULT_TELEGRAM_TIMEOUT_MS, deadline - deps.now()));
|
|
943
|
+
}
|
|
944
|
+
catch (error) {
|
|
945
|
+
throw telegramBootstrapRequestError("getUpdates", error);
|
|
946
|
+
}
|
|
947
|
+
if (!Array.isArray(updates) || updates.length > 1)
|
|
948
|
+
throw new Error("Telegram gateway delivery is ambiguous");
|
|
949
|
+
if (updates.length === 0)
|
|
950
|
+
continue;
|
|
951
|
+
const candidate = updates[0];
|
|
952
|
+
const message = candidate.message;
|
|
953
|
+
const observation = gateway.authorityTransport.metadataForUpdate(candidate);
|
|
954
|
+
if (!message || message.text !== nonce || message.chat.type !== "private" || !message.from
|
|
955
|
+
|| Object.keys(message).some((key) => key.startsWith("forward_")) || !Number.isSafeInteger(message.date) || message.date < Math.floor(Number(base.startedAt) / 1000)
|
|
956
|
+
|| !observation?.ownerEligible || observation.userId !== gateway.credentials.authorizedUserId || observation.chatId !== gateway.credentials.authorizedChatId) {
|
|
957
|
+
throw new Error("Telegram gateway has a pending non-bootstrap owner update; resume resident dispatch without discarding it");
|
|
958
|
+
}
|
|
959
|
+
match = candidate;
|
|
960
|
+
}
|
|
961
|
+
if (!match)
|
|
962
|
+
throw new Error("Telegram nonce confirmation timed out");
|
|
963
|
+
const confirmed = { ...base, phase: "nonce_confirmed", updateDigest: digest(match), coordinateDigest: digest({ userId: gateway.credentials.authorizedUserId, chatId: gateway.credentials.authorizedChatId }) };
|
|
964
|
+
replaceCheckpoint(root, evidencePath, confirmed);
|
|
965
|
+
await bounded(() => gateway.authorityTransport.settleTransport(match, "completed"));
|
|
966
|
+
const settled = await bounded(() => gateway.cursorSnapshot());
|
|
967
|
+
if (settled.cursor <= match.update_id)
|
|
968
|
+
throw new Error("Telegram gateway settlement readback did not advance");
|
|
969
|
+
atomicPrivateJson(root, offsetPath, { nextUpdateId: settled.cursor, progressDigest: settled.progressDigest });
|
|
970
|
+
replaceCheckpoint(root, evidencePath, { ...confirmed, phase: "complete", offsetDigest: settled.progressDigest.slice("sha256:".length), completedAt: deps.now() });
|
|
971
|
+
}
|
|
972
|
+
catch (error) {
|
|
973
|
+
failedCheckpoint(root, evidencePath, base, error);
|
|
974
|
+
throw error;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
finally {
|
|
978
|
+
gateway.authorityTransport.api.stop();
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
function atomicPrivateJson(root, filePath, value) {
|
|
982
|
+
writeAtomicPrivateJson(root, filePath, value, pathEntryExists(filePath));
|
|
983
|
+
}
|
|
984
|
+
function opaqueDigest(value, label) {
|
|
985
|
+
const result = text(value, label);
|
|
986
|
+
if (!OPAQUE_DIGEST.test(result))
|
|
987
|
+
throw new Error(`${label} must be an opaque sha256 digest`);
|
|
988
|
+
return result;
|
|
989
|
+
}
|
|
990
|
+
function fixedEvidenceValues(schema, payload) {
|
|
991
|
+
const value = object(payload, `${schema} evidence`);
|
|
992
|
+
switch (schema) {
|
|
993
|
+
case "telegram-cursor-v1":
|
|
994
|
+
return {
|
|
995
|
+
offsetDigest: opaqueDigest(value.offsetDigest, "telegram cursor offsetDigest"),
|
|
996
|
+
auditCursorDigest: opaqueDigest(value.auditCursorDigest, "telegram cursor auditCursorDigest"),
|
|
997
|
+
};
|
|
998
|
+
case "postboot-health-v1":
|
|
999
|
+
return {
|
|
1000
|
+
healthy: boolean(value.healthy, "postboot healthy"),
|
|
1001
|
+
containerImageDigest: opaqueDigest(value.containerImageDigest, "postboot containerImageDigest"),
|
|
1002
|
+
telegramOffsetDigest: opaqueDigest(value.telegramOffsetDigest, "postboot telegramOffsetDigest"),
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
async function cursorSnapshot(config, deps) {
|
|
1007
|
+
const root = privateAllowedRoot(config);
|
|
1008
|
+
const evidencePath = confinedPath(root, config.evidencePath, "evidencePath");
|
|
1009
|
+
refuseExistingCheckpoint(root, evidencePath);
|
|
1010
|
+
if (!Array.isArray(config.adapters) || config.adapters.length === 0)
|
|
1011
|
+
throw new Error("snapshot adapters must be nonempty");
|
|
1012
|
+
const values = {};
|
|
1013
|
+
const schemas = new Set();
|
|
1014
|
+
for (const raw of config.adapters) {
|
|
1015
|
+
const spec = object(raw, "snapshot adapter");
|
|
1016
|
+
const schema = text(spec.schema, "snapshot adapter schema");
|
|
1017
|
+
if (schema !== "telegram-cursor-v1")
|
|
1018
|
+
throw new Error("unsupported cursor snapshot evidence schema");
|
|
1019
|
+
if (schemas.has(schema))
|
|
1020
|
+
throw new Error("snapshot adapter schemas must be unique");
|
|
1021
|
+
schemas.add(schema);
|
|
1022
|
+
const executable = adapter(spec.executable, "snapshot adapter executable");
|
|
1023
|
+
const allowGenesis = spec.allowGenesis === undefined ? false : boolean(spec.allowGenesis, "snapshot adapter allowGenesis");
|
|
1024
|
+
if (allowGenesis && evidencePath !== path.join(root, "cursor-before.json"))
|
|
1025
|
+
throw new Error("snapshot adapter genesis authority is only valid for cursor-before evidence");
|
|
1026
|
+
const payload = await deps.runAdapter(executable, { operation: "snapshot", schema, allowGenesis });
|
|
1027
|
+
const selected = fixedEvidenceValues(schema, payload);
|
|
1028
|
+
for (const [name, value] of Object.entries(selected))
|
|
1029
|
+
values[`${schema}.${name}`] = value;
|
|
1030
|
+
}
|
|
1031
|
+
initializeCheckpoint(root, evidencePath, { schemaVersion: 1, operation: "cursor-snapshot", phase: "complete", schema: "telegram-cursor-v1", capturedAt: deps.now(), values });
|
|
1032
|
+
}
|
|
1033
|
+
function cursorSnapshotValues(checkpoint, label) {
|
|
1034
|
+
if (checkpoint.schemaVersion !== 1 || checkpoint.operation !== "cursor-snapshot" || checkpoint.phase !== "complete"
|
|
1035
|
+
|| checkpoint.schema !== "telegram-cursor-v1") {
|
|
1036
|
+
throw new Error(`${label} must be an exact complete cursor snapshot`);
|
|
1037
|
+
}
|
|
1038
|
+
const values = object(checkpoint.values, `${label} values`);
|
|
1039
|
+
const expectedKeys = ["telegram-cursor-v1.auditCursorDigest", "telegram-cursor-v1.offsetDigest"];
|
|
1040
|
+
if (JSON.stringify(Object.keys(values).sort()) !== JSON.stringify(expectedKeys)) {
|
|
1041
|
+
throw new Error(`${label} must contain the exact cursor snapshot values`);
|
|
1042
|
+
}
|
|
1043
|
+
return Object.fromEntries(expectedKeys.map((key) => [key, opaqueDigest(values[key], `${label} ${key}`)]));
|
|
1044
|
+
}
|
|
1045
|
+
async function cursorDelta(config, deps) {
|
|
1046
|
+
const root = privateAllowedRoot(config);
|
|
1047
|
+
const evidencePath = confinedPath(root, config.evidencePath, "evidencePath");
|
|
1048
|
+
refuseExistingCheckpoint(root, evidencePath);
|
|
1049
|
+
const before = cursorSnapshotValues(readCheckpoint(root, config.beforePath), "before cursor snapshot");
|
|
1050
|
+
const after = cursorSnapshotValues(readCheckpoint(root, config.afterPath), "after cursor snapshot");
|
|
1051
|
+
const changes = {};
|
|
1052
|
+
for (const key of Object.keys(before).sort()) {
|
|
1053
|
+
if (before[key] !== after[key])
|
|
1054
|
+
changes[key] = { before: before[key], after: after[key] };
|
|
1055
|
+
}
|
|
1056
|
+
initializeCheckpoint(root, evidencePath, { schemaVersion: 1, operation: "cursor-delta", phase: "complete", capturedAt: deps.now(), changes });
|
|
1057
|
+
}
|
|
1058
|
+
async function callbackInject(config, deps) {
|
|
1059
|
+
const root = privateAllowedRoot(config);
|
|
1060
|
+
const evidencePath = confinedPath(root, config.evidencePath, "evidencePath");
|
|
1061
|
+
refuseExistingCheckpoint(root, evidencePath);
|
|
1062
|
+
const executable = adapter(config.adapter, "callback adapter");
|
|
1063
|
+
const concurrency = integer(config.concurrency, "concurrency", 2);
|
|
1064
|
+
if (concurrency > 16)
|
|
1065
|
+
throw new Error("callback concurrency exceeds 16");
|
|
1066
|
+
let update;
|
|
1067
|
+
try {
|
|
1068
|
+
update = object(JSON.parse(deps.readSecret()), "saved callback update");
|
|
1069
|
+
}
|
|
1070
|
+
catch {
|
|
1071
|
+
throw new Error("saved callback update must be valid JSON");
|
|
1072
|
+
}
|
|
1073
|
+
object(update.callback_query, "saved callback update callback_query");
|
|
1074
|
+
const base = { schemaVersion: 1, operation: "callback-inject", phase: "preflight", updateDigest: digest(update), concurrency, replay: true };
|
|
1075
|
+
initializeCheckpoint(root, evidencePath, base);
|
|
1076
|
+
try {
|
|
1077
|
+
const preflight = async () => {
|
|
1078
|
+
const result = object(await deps.runAdapter(executable, { operation: "callback_playback_preflight", update }), "callback playback preflight");
|
|
1079
|
+
if (JSON.stringify(Object.keys(result).sort()) !== JSON.stringify(["coordinateDigest", "journalDigest", "playbackCount"])) {
|
|
1080
|
+
throw new Error("callback playback preflight shape is invalid");
|
|
1081
|
+
}
|
|
1082
|
+
if (!Number.isSafeInteger(result.playbackCount) || Number(result.playbackCount) < 0) {
|
|
1083
|
+
throw new Error("callback playback preflight count is invalid");
|
|
1084
|
+
}
|
|
1085
|
+
if (typeof result.coordinateDigest !== "string" || !/^[0-9a-f]{64}$/u.test(result.coordinateDigest)) {
|
|
1086
|
+
throw new Error("callback playback preflight coordinate digest is invalid");
|
|
1087
|
+
}
|
|
1088
|
+
if (typeof result.journalDigest !== "string" || !/^[0-9a-f]{64}$/u.test(result.journalDigest)) {
|
|
1089
|
+
throw new Error("callback playback preflight journal digest is invalid");
|
|
1090
|
+
}
|
|
1091
|
+
return { playbackCount: Number(result.playbackCount), coordinateDigest: result.coordinateDigest, journalDigest: result.journalDigest };
|
|
1092
|
+
};
|
|
1093
|
+
const firstPreflight = await preflight();
|
|
1094
|
+
if (firstPreflight.playbackCount !== 0)
|
|
1095
|
+
throw new Error("callback injection requires zero prior playback");
|
|
1096
|
+
const secondPreflight = await preflight();
|
|
1097
|
+
if (secondPreflight.playbackCount !== 0)
|
|
1098
|
+
throw new Error("callback injection requires zero prior playback");
|
|
1099
|
+
if (secondPreflight.coordinateDigest !== firstPreflight.coordinateDigest)
|
|
1100
|
+
throw new Error("callback playback coordinate changed between preflights");
|
|
1101
|
+
if (secondPreflight.journalDigest !== firstPreflight.journalDigest)
|
|
1102
|
+
throw new Error("callback playback journal changed between preflights");
|
|
1103
|
+
const batch = object(await deps.runAdapter(executable, { operation: "inject_callbacks_concurrently", update, concurrency }), "callback batch result");
|
|
1104
|
+
if (!Array.isArray(batch.results) || batch.results.length !== concurrency)
|
|
1105
|
+
throw new Error("callback batch result count mismatch");
|
|
1106
|
+
const responses = batch.results;
|
|
1107
|
+
let claims = 0;
|
|
1108
|
+
let mutations = 0;
|
|
1109
|
+
for (const response of responses) {
|
|
1110
|
+
const result = object(response, "callback adapter result");
|
|
1111
|
+
if (result.settled !== true)
|
|
1112
|
+
throw new Error("callback adapter did not settle");
|
|
1113
|
+
if (result.claimed === true)
|
|
1114
|
+
claims += 1;
|
|
1115
|
+
else if (result.claimed !== false)
|
|
1116
|
+
throw new Error("callback adapter claim must be boolean");
|
|
1117
|
+
if (result.mutated === true)
|
|
1118
|
+
mutations += 1;
|
|
1119
|
+
else if (result.mutated !== false)
|
|
1120
|
+
throw new Error("callback adapter mutation must be boolean");
|
|
1121
|
+
}
|
|
1122
|
+
if (claims !== 1)
|
|
1123
|
+
throw new Error("callback claim total must be exactly one");
|
|
1124
|
+
if (mutations !== 1)
|
|
1125
|
+
throw new Error("callback mutation total must be exactly one");
|
|
1126
|
+
const replayResult = object(await deps.runAdapter(executable, { operation: "inject_callback_replay", update }), "callback replay result");
|
|
1127
|
+
if (replayResult.settled !== true || typeof replayResult.claimed !== "boolean" || typeof replayResult.mutated !== "boolean") {
|
|
1128
|
+
throw new Error("callback replay did not settle canonically");
|
|
1129
|
+
}
|
|
1130
|
+
if (replayResult.claimed || replayResult.mutated)
|
|
1131
|
+
throw new Error("callback replay was claimed or mutated state");
|
|
1132
|
+
replaceCheckpoint(root, evidencePath, { ...base, phase: "complete", coordinateDigest: firstPreflight.coordinateDigest, zeroPlayback: true, claims, mutations, replayClaimed: false, replayMutated: false, completedAt: deps.now() });
|
|
1133
|
+
}
|
|
1134
|
+
catch (error) {
|
|
1135
|
+
failedCheckpoint(root, evidencePath, base, error);
|
|
1136
|
+
throw error;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
const SANCTUARY_ACCEPTANCE_ADAPTER = "/opt/ouro/deploy/unraid/sanctuary-acceptance-adapter.sh";
|
|
1140
|
+
function inventory(value) {
|
|
1141
|
+
const root = object(value, "Unraid inventory");
|
|
1142
|
+
if (!Array.isArray(root.keys))
|
|
1143
|
+
throw new Error("Unraid inventory keys must be an array");
|
|
1144
|
+
const keys = root.keys.map((raw) => {
|
|
1145
|
+
const key = object(raw, "Unraid inventory key");
|
|
1146
|
+
return {
|
|
1147
|
+
id: text(key.id, "Unraid key id"),
|
|
1148
|
+
name: text(key.name, "Unraid key name"),
|
|
1149
|
+
permissions: stringArray(key.permissions, "Unraid key permissions").sort(),
|
|
1150
|
+
roles: Array.isArray(key.roles) ? stringArray(key.roles, "Unraid key roles").sort() : (() => { throw new Error("Unraid key roles must be an array"); })(),
|
|
1151
|
+
};
|
|
1152
|
+
});
|
|
1153
|
+
if (new Set(keys.map((key) => key.id)).size !== keys.length)
|
|
1154
|
+
throw new Error("Unraid inventory key IDs must be unique");
|
|
1155
|
+
if (new Set(keys.map((key) => key.name)).size !== keys.length)
|
|
1156
|
+
throw new Error("Unraid inventory key names must be unique");
|
|
1157
|
+
return keys;
|
|
1158
|
+
}
|
|
1159
|
+
function sameStrings(left, right) {
|
|
1160
|
+
return JSON.stringify([...left].sort()) === JSON.stringify([...right].sort());
|
|
1161
|
+
}
|
|
1162
|
+
function sameInventoryKey(left, right) {
|
|
1163
|
+
return left.id === right.id && left.name === right.name && sameStrings(left.permissions, right.permissions) && sameStrings(left.roles, right.roles);
|
|
1164
|
+
}
|
|
1165
|
+
function sameInventory(left, right) {
|
|
1166
|
+
const sorted = (keys) => [...keys].sort((a, b) => a.id.localeCompare(b.id));
|
|
1167
|
+
const expected = sorted(left);
|
|
1168
|
+
const actual = sorted(right);
|
|
1169
|
+
return expected.length === actual.length && expected.every((key, index) => sameInventoryKey(key, actual[index]));
|
|
1170
|
+
}
|
|
1171
|
+
async function rotateOccupiedCanonicalUnraidKeys(input) {
|
|
1172
|
+
const { root, evidencePath, targetServerId, adapters, desired, oldKeys, initial, deps } = input;
|
|
1173
|
+
const prior = pathEntryExists(evidencePath) ? readCheckpoint(root, evidencePath) : null;
|
|
1174
|
+
if (prior && (prior.schemaVersion !== 1 || prior.operation !== "unraid-key-rotate" || prior.targetServerId !== targetServerId)) {
|
|
1175
|
+
throw new Error("Unraid rotation checkpoint identity mismatch");
|
|
1176
|
+
}
|
|
1177
|
+
const suffix = prior ? text(prior.transactionSuffix, "Unraid rotation transaction suffix") : deps.randomBytes(8).subarray(0, 8).toString("hex");
|
|
1178
|
+
if (!/^[0-9a-f]{16}$/u.test(suffix))
|
|
1179
|
+
throw new Error("Unraid rotation transaction suffix is invalid");
|
|
1180
|
+
const createdKeyIds = prior ? stringArray(prior.createdKeyIds, "createdKeyIds") : [];
|
|
1181
|
+
const revokedKeyIds = prior ? stringArray(prior.revokedKeyIds, "revokedKeyIds") : [];
|
|
1182
|
+
const transactionKeys = prior ? (() => {
|
|
1183
|
+
if (!Array.isArray(prior.transactionKeys))
|
|
1184
|
+
throw new Error("Unraid rotation transaction keys are unavailable");
|
|
1185
|
+
return prior.transactionKeys.map((raw) => {
|
|
1186
|
+
const value = object(raw, "Unraid rotation transaction key");
|
|
1187
|
+
return {
|
|
1188
|
+
id: text(value.id, "transaction key id"), name: text(value.name, "transaction key name"),
|
|
1189
|
+
vaultField: text(value.vaultField, "transaction key vault field"),
|
|
1190
|
+
permissions: stringArray(value.permissions, "transaction key permissions").sort(),
|
|
1191
|
+
temporary: boolean(value.temporary, "transaction key temporary"),
|
|
1192
|
+
attested: boolean(value.attested, "transaction key attested"),
|
|
1193
|
+
};
|
|
1194
|
+
});
|
|
1195
|
+
})() : [];
|
|
1196
|
+
if (new Set(createdKeyIds).size !== createdKeyIds.length || new Set(revokedKeyIds).size !== revokedKeyIds.length) {
|
|
1197
|
+
throw new Error("Unraid rotation checkpoint IDs are ambiguous");
|
|
1198
|
+
}
|
|
1199
|
+
if (new Set(transactionKeys.map((entry) => entry.id)).size !== transactionKeys.length
|
|
1200
|
+
|| new Set(transactionKeys.map((entry) => entry.name)).size !== transactionKeys.length
|
|
1201
|
+
|| !sameStrings(createdKeyIds, transactionKeys.map((entry) => entry.id))) {
|
|
1202
|
+
throw new Error("Unraid rotation transaction key metadata is ambiguous");
|
|
1203
|
+
}
|
|
1204
|
+
const slots = desired.flatMap((key) => [
|
|
1205
|
+
{ ...key, name: `${key.name} Rotation ${suffix}`, temporary: true },
|
|
1206
|
+
{ ...key, temporary: false },
|
|
1207
|
+
]);
|
|
1208
|
+
if (transactionKeys.some((entry) => {
|
|
1209
|
+
const slot = slots.find((candidate) => candidate.name === entry.name);
|
|
1210
|
+
return !slot || entry.temporary !== slot.temporary || entry.vaultField !== slot.vaultField
|
|
1211
|
+
|| !sameStrings(entry.permissions, slot.permissions);
|
|
1212
|
+
}))
|
|
1213
|
+
throw new Error("Unraid rotation checkpoint transaction slots are invalid");
|
|
1214
|
+
const revocableKeyIds = new Set([
|
|
1215
|
+
...oldKeys.map((entry) => entry.id),
|
|
1216
|
+
...transactionKeys.filter((entry) => entry.temporary).map((entry) => entry.id),
|
|
1217
|
+
]);
|
|
1218
|
+
if (revokedKeyIds.some((id) => !revocableKeyIds.has(id))) {
|
|
1219
|
+
throw new Error("Unraid rotation checkpoint contains an unowned revoked key ID");
|
|
1220
|
+
}
|
|
1221
|
+
const temporary = transactionKeys.filter((entry) => entry.temporary);
|
|
1222
|
+
const canonical = transactionKeys.filter((entry) => !entry.temporary);
|
|
1223
|
+
const allOldRevoked = oldKeys.every((entry) => revokedKeyIds.includes(entry.id));
|
|
1224
|
+
const allTemporaryAttested = temporary.length === desired.length && temporary.every((entry) => entry.attested);
|
|
1225
|
+
const allCanonicalAttested = canonical.length === desired.length && canonical.every((entry) => entry.attested);
|
|
1226
|
+
const revokedTemporary = temporary.filter((entry) => revokedKeyIds.includes(entry.id));
|
|
1227
|
+
if (revokedTemporary.some((entry) => !entry.attested)
|
|
1228
|
+
|| (oldKeys.some((entry) => revokedKeyIds.includes(entry.id)) && !allTemporaryAttested)
|
|
1229
|
+
|| (revokedTemporary.length > 0 && !allCanonicalAttested)
|
|
1230
|
+
|| (canonical.length > 0 && !allOldRevoked)) {
|
|
1231
|
+
throw new Error("Unraid rotation checkpoint revocation order is invalid");
|
|
1232
|
+
}
|
|
1233
|
+
const phase = prior ? text(prior.phase, "Unraid rotation phase") : "preflight";
|
|
1234
|
+
const knownPhases = new Set(["preflight", "temporary_key_created", "temporary_key_attested", "key_revoke_attempted", "key_revoked", "canonical_key_created", "canonical_key_attested", "complete"]);
|
|
1235
|
+
const resumePhase = phase === "failed" ? text(prior?.resumePhase, "Unraid rotation resume phase") : phase;
|
|
1236
|
+
if (!knownPhases.has(resumePhase) || (phase !== "failed" && prior?.resumePhase !== undefined)) {
|
|
1237
|
+
throw new Error("Unraid rotation checkpoint phase is invalid");
|
|
1238
|
+
}
|
|
1239
|
+
const stateMatchesPhase = resumePhase === "preflight" ? transactionKeys.length === 0 && revokedKeyIds.length === 0
|
|
1240
|
+
: resumePhase === "temporary_key_created" ? temporary.length > 0 && canonical.length === 0 && revokedKeyIds.length === 0 && temporary.filter((entry) => !entry.attested).length === 1
|
|
1241
|
+
: resumePhase === "temporary_key_attested" ? temporary.length > 0 && canonical.length === 0 && revokedKeyIds.length === 0 && temporary.every((entry) => entry.attested)
|
|
1242
|
+
: resumePhase === "key_revoke_attempted" ? allTemporaryAttested && (canonical.length === 0 || allCanonicalAttested)
|
|
1243
|
+
: resumePhase === "key_revoked" ? revokedKeyIds.length > 0 && allTemporaryAttested && (canonical.length === 0 || allCanonicalAttested)
|
|
1244
|
+
: resumePhase === "canonical_key_created" ? allTemporaryAttested && canonical.length > 0 && sameStrings(revokedKeyIds, oldKeys.map((entry) => entry.id)) && canonical.filter((entry) => !entry.attested).length === 1
|
|
1245
|
+
: resumePhase === "canonical_key_attested" ? allTemporaryAttested && canonical.length > 0 && sameStrings(revokedKeyIds, oldKeys.map((entry) => entry.id)) && canonical.every((entry) => entry.attested)
|
|
1246
|
+
: transactionKeys.length === slots.length && allTemporaryAttested && allCanonicalAttested
|
|
1247
|
+
&& sameStrings(revokedKeyIds, [...oldKeys.map((entry) => entry.id), ...temporary.map((entry) => entry.id)]);
|
|
1248
|
+
if (!stateMatchesPhase)
|
|
1249
|
+
throw new Error("Unraid rotation checkpoint state does not match its phase");
|
|
1250
|
+
const base = {
|
|
1251
|
+
schemaVersion: 1, operation: "unraid-key-rotate", targetServerId,
|
|
1252
|
+
initialInventoryDigest: prior ? text(prior.initialInventoryDigest, "initialInventoryDigest") : digest(initial),
|
|
1253
|
+
transactionSuffix: suffix,
|
|
1254
|
+
};
|
|
1255
|
+
if (!prior)
|
|
1256
|
+
initializeCheckpoint(root, evidencePath, { ...base, phase: "preflight", createdKeyIds, revokedKeyIds, transactionKeys });
|
|
1257
|
+
if (phase === "complete") {
|
|
1258
|
+
const final = inventory(await deps.runAdapter(adapters.inventory, { operation: "inventory_keys", targetServerId }));
|
|
1259
|
+
const canonical = transactionKeys.filter((entry) => !entry.temporary)
|
|
1260
|
+
.map(({ id, name, permissions }) => ({ id, name, permissions, roles: [] }));
|
|
1261
|
+
if (canonical.length !== desired.length || !sameInventory(final, canonical)) {
|
|
1262
|
+
throw new Error("completed Unraid rotation inventory drifted");
|
|
1263
|
+
}
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
const created = [];
|
|
1267
|
+
let activePhase = resumePhase;
|
|
1268
|
+
const checkpoint = (phase) => {
|
|
1269
|
+
activePhase = phase;
|
|
1270
|
+
replaceCheckpoint(root, evidencePath, { ...base, phase, createdKeyIds, revokedKeyIds, transactionKeys });
|
|
1271
|
+
};
|
|
1272
|
+
const createAndAttest = async (key, name, temporary) => {
|
|
1273
|
+
const live = inventory(await deps.runAdapter(adapters.inventory, { operation: "inventory_keys", targetServerId }));
|
|
1274
|
+
const matches = live.filter((entry) => entry.name === name);
|
|
1275
|
+
const transaction = transactionKeys.find((entry) => entry.name === name);
|
|
1276
|
+
if (transaction && revokedKeyIds.includes(transaction.id)) {
|
|
1277
|
+
if (matches.length !== 0)
|
|
1278
|
+
throw new Error("revoked Unraid transaction key inventory mismatch");
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
if (transaction && temporary && matches.length === 0)
|
|
1282
|
+
return;
|
|
1283
|
+
let id;
|
|
1284
|
+
let handle;
|
|
1285
|
+
if (matches.length === 1) {
|
|
1286
|
+
const existing = matches[0];
|
|
1287
|
+
if (!sameStrings(existing.permissions, key.permissions) || existing.roles.length !== 0
|
|
1288
|
+
|| oldKeys.some((old) => old.id === existing.id)
|
|
1289
|
+
|| (transaction && transaction.id !== existing.id)) {
|
|
1290
|
+
throw new Error("resumable Unraid key scope or identity mismatch");
|
|
1291
|
+
}
|
|
1292
|
+
id = existing.id;
|
|
1293
|
+
handle = `unraid-key:${id}:${key.vaultField}`;
|
|
1294
|
+
if (!transaction) {
|
|
1295
|
+
const recovered = object(await deps.runAdapter(adapters.create, {
|
|
1296
|
+
operation: "create_key", targetServerId, name, permissions: key.permissions,
|
|
1297
|
+
}), "recovered Unraid key");
|
|
1298
|
+
if (recovered.id !== id || recovered.name !== name
|
|
1299
|
+
|| !sameStrings(stringArray(recovered.permissions, "recovered permissions"), key.permissions)
|
|
1300
|
+
|| !Array.isArray(recovered.roles) || recovered.roles.length !== 0) {
|
|
1301
|
+
throw new Error("recovered Unraid key scope or identity mismatch");
|
|
1302
|
+
}
|
|
1303
|
+
handle = text(recovered.key, "recovered key handle");
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
else {
|
|
1307
|
+
const response = object(await deps.runAdapter(adapters.create, {
|
|
1308
|
+
operation: "create_key", targetServerId, name, permissions: key.permissions,
|
|
1309
|
+
}), "created Unraid key");
|
|
1310
|
+
id = text(response.id, "created key id");
|
|
1311
|
+
handle = text(response.key, "created key handle");
|
|
1312
|
+
if (response.name !== name || !sameStrings(stringArray(response.permissions, "created permissions"), key.permissions)
|
|
1313
|
+
|| !Array.isArray(response.roles) || response.roles.length !== 0
|
|
1314
|
+
|| oldKeys.some((old) => old.id === id) || created.some((entry) => entry.id === id)) {
|
|
1315
|
+
throw new Error("created Unraid key scope or identity mismatch");
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
created.push({ id, name, permissions: key.permissions, roles: [], handle, desired: key, temporary });
|
|
1319
|
+
if (!createdKeyIds.includes(id))
|
|
1320
|
+
createdKeyIds.push(id);
|
|
1321
|
+
const metadata = transaction ?? { id, name, vaultField: key.vaultField, permissions: key.permissions, temporary, attested: false };
|
|
1322
|
+
if (!transaction)
|
|
1323
|
+
transactionKeys.push(metadata);
|
|
1324
|
+
checkpoint(temporary ? "temporary_key_created" : "canonical_key_created");
|
|
1325
|
+
if (!metadata.attested) {
|
|
1326
|
+
const stored = object(await deps.runAdapter(adapters.store, {
|
|
1327
|
+
operation: "store_key", targetServerId, vaultField: key.vaultField, keyId: id, key: handle,
|
|
1328
|
+
}), "key store result");
|
|
1329
|
+
if (stored.stored !== true || stored.keyId !== id)
|
|
1330
|
+
throw new Error("vault did not attest exact key storage");
|
|
1331
|
+
}
|
|
1332
|
+
const probe = object(await deps.runAdapter(adapters.probe, {
|
|
1333
|
+
operation: "probe_new_key", targetServerId, id, key: handle,
|
|
1334
|
+
}), "new key probe");
|
|
1335
|
+
if (probe.valid !== true)
|
|
1336
|
+
throw new Error("new Unraid key readiness failed");
|
|
1337
|
+
metadata.attested = true;
|
|
1338
|
+
checkpoint(temporary ? "temporary_key_attested" : "canonical_key_attested");
|
|
1339
|
+
};
|
|
1340
|
+
const revokeAndReject = async (id, handle) => {
|
|
1341
|
+
if (revokedKeyIds.includes(id))
|
|
1342
|
+
return;
|
|
1343
|
+
const live = inventory(await deps.runAdapter(adapters.inventory, { operation: "inventory_keys", targetServerId }));
|
|
1344
|
+
if (live.some((entry) => entry.id === id)) {
|
|
1345
|
+
const revoked = object(await deps.runAdapter(adapters.revoke, { operation: "revoke_key", targetServerId, id }), "key revoke result");
|
|
1346
|
+
if (revoked.revoked !== true || revoked.id !== id)
|
|
1347
|
+
throw new Error("Unraid revoke did not attest exact key id");
|
|
1348
|
+
checkpoint("key_revoke_attempted");
|
|
1349
|
+
}
|
|
1350
|
+
const probe = object(await deps.runAdapter(adapters.probe, { operation: "probe_revoked_key", targetServerId, id, key: handle }), "revoked key probe");
|
|
1351
|
+
if (probe.valid !== false || (probe.status !== 401 && probe.status !== 403))
|
|
1352
|
+
throw new Error("revoked Unraid key still authenticates");
|
|
1353
|
+
revokedKeyIds.push(id);
|
|
1354
|
+
checkpoint("key_revoked");
|
|
1355
|
+
};
|
|
1356
|
+
try {
|
|
1357
|
+
for (const key of desired)
|
|
1358
|
+
await createAndAttest(key, `${key.name} Rotation ${suffix}`, true);
|
|
1359
|
+
const withTemporary = inventory(await deps.runAdapter(adapters.inventory, { operation: "inventory_keys", targetServerId }));
|
|
1360
|
+
const liveOld = withTemporary.filter((entry) => oldKeys.some((old) => old.id === entry.id));
|
|
1361
|
+
const inventoryAmbiguous = withTemporary.some((entry) => {
|
|
1362
|
+
if (oldKeys.some((old) => old.id === entry.id)) {
|
|
1363
|
+
const expected = desired.find((candidate) => candidate.name === entry.name);
|
|
1364
|
+
return !expected || !sameStrings(expected.permissions, entry.permissions) || entry.roles.length !== 0;
|
|
1365
|
+
}
|
|
1366
|
+
const metadata = transactionKeys.find((candidate) => candidate.id === entry.id);
|
|
1367
|
+
return !metadata || metadata.name !== entry.name || !sameStrings(metadata.permissions, entry.permissions) || entry.roles.length !== 0;
|
|
1368
|
+
});
|
|
1369
|
+
const missingBridge = liveOld.length > 0 && transactionKeys.filter((entry) => entry.temporary && !revokedKeyIds.includes(entry.id))
|
|
1370
|
+
.some((entry) => !withTemporary.some((candidate) => candidate.id === entry.id));
|
|
1371
|
+
if (inventoryAmbiguous || missingBridge) {
|
|
1372
|
+
throw new Error("temporary Unraid key inventory changed ambiguously");
|
|
1373
|
+
}
|
|
1374
|
+
for (const old of oldKeys) {
|
|
1375
|
+
const live = inventory(await deps.runAdapter(adapters.inventory, { operation: "inventory_keys", targetServerId }));
|
|
1376
|
+
if (live.some((entry) => entry.id === old.id)) {
|
|
1377
|
+
const secret = object(await deps.runAdapter(old.secretAdapter, { operation: "read_old_key", targetServerId, id: old.id }), "old key handle result");
|
|
1378
|
+
await revokeAndReject(old.id, text(secret.key, "old key handle"));
|
|
1379
|
+
}
|
|
1380
|
+
else {
|
|
1381
|
+
await revokeAndReject(old.id, `unraid-key:${old.id}:legacy`);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
let afterOldRevokes = inventory(await deps.runAdapter(adapters.inventory, { operation: "inventory_keys", targetServerId }));
|
|
1385
|
+
const existingCanonical = transactionKeys.filter((entry) => !entry.temporary);
|
|
1386
|
+
if (existingCanonical.length === desired.length) {
|
|
1387
|
+
for (const key of transactionKeys.filter((entry) => entry.temporary && !revokedKeyIds.includes(entry.id))) {
|
|
1388
|
+
if (!afterOldRevokes.some((entry) => entry.id === key.id)) {
|
|
1389
|
+
await revokeAndReject(key.id, `unraid-key:${key.id}:${key.vaultField}`);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
afterOldRevokes = inventory(await deps.runAdapter(adapters.inventory, { operation: "inventory_keys", targetServerId }));
|
|
1393
|
+
}
|
|
1394
|
+
const exactPhase = transactionKeys.filter((entry) => !revokedKeyIds.includes(entry.id))
|
|
1395
|
+
.map(({ id, name, permissions }) => ({ id, name, permissions, roles: [] }));
|
|
1396
|
+
if (existingCanonical.length > desired.length || !sameInventory(afterOldRevokes, exactPhase)) {
|
|
1397
|
+
throw new Error("old-key revoke inventory is not the exact temporary pair");
|
|
1398
|
+
}
|
|
1399
|
+
for (const key of desired)
|
|
1400
|
+
await createAndAttest(key, key.name, false);
|
|
1401
|
+
const withCanonical = inventory(await deps.runAdapter(adapters.inventory, { operation: "inventory_keys", targetServerId }));
|
|
1402
|
+
if (withCanonical.some((entry) => {
|
|
1403
|
+
const metadata = transactionKeys.find((candidate) => candidate.id === entry.id);
|
|
1404
|
+
return !metadata || metadata.name !== entry.name || !sameStrings(metadata.permissions, entry.permissions) || entry.roles.length !== 0;
|
|
1405
|
+
}) || transactionKeys.filter((entry) => !entry.temporary).some((entry) => !withCanonical.some((candidate) => candidate.id === entry.id))) {
|
|
1406
|
+
throw new Error("canonical-key mint inventory changed ambiguously");
|
|
1407
|
+
}
|
|
1408
|
+
for (const key of transactionKeys.filter((entry) => entry.temporary)) {
|
|
1409
|
+
await revokeAndReject(key.id, `unraid-key:${key.id}:${key.vaultField}`);
|
|
1410
|
+
}
|
|
1411
|
+
const final = inventory(await deps.runAdapter(adapters.inventory, { operation: "inventory_keys", targetServerId }));
|
|
1412
|
+
const canonical = transactionKeys.filter((entry) => !entry.temporary)
|
|
1413
|
+
.map(({ id, name, permissions }) => ({ id, name, permissions, roles: [] }));
|
|
1414
|
+
if (!sameInventory(final, canonical))
|
|
1415
|
+
throw new Error("final canonical Unraid key inventory mismatch");
|
|
1416
|
+
replaceCheckpoint(root, evidencePath, {
|
|
1417
|
+
...base,
|
|
1418
|
+
phase: "complete",
|
|
1419
|
+
createdKeyIds,
|
|
1420
|
+
revokedKeyIds,
|
|
1421
|
+
transactionKeys,
|
|
1422
|
+
finalInventoryDigest: digest(final),
|
|
1423
|
+
completedAt: deps.now(),
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
catch (error) {
|
|
1427
|
+
replaceCheckpoint(root, evidencePath, { ...base, phase: "failed", resumePhase: activePhase, createdKeyIds, revokedKeyIds, transactionKeys, errorCategory: safeErrorCategory(error) });
|
|
1428
|
+
throw error;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
async function unraidKeyRotate(config, deps) {
|
|
1432
|
+
const root = privateAllowedRoot(config);
|
|
1433
|
+
const evidencePath = confinedPath(root, config.evidencePath, "evidencePath");
|
|
1434
|
+
const targetServerId = text(config.targetServerId, "targetServerId");
|
|
1435
|
+
const inventoryAdapter = adapter(config.inventoryAdapter, "inventoryAdapter");
|
|
1436
|
+
const createAdapter = adapter(config.createAdapter, "createAdapter");
|
|
1437
|
+
const storeAdapter = adapter(config.storeAdapter, "storeAdapter");
|
|
1438
|
+
const revokeAdapter = adapter(config.revokeAdapter, "revokeAdapter");
|
|
1439
|
+
const probeAdapter = adapter(config.probeAdapter, "probeAdapter");
|
|
1440
|
+
if (!Array.isArray(config.keys) || config.keys.length === 0)
|
|
1441
|
+
throw new Error("Unraid desired keys must be nonempty");
|
|
1442
|
+
const desired = config.keys.map((raw) => {
|
|
1443
|
+
const key = object(raw, "desired Unraid key");
|
|
1444
|
+
const permissions = stringArray(key.permissions, "desired key permissions");
|
|
1445
|
+
if (new Set(permissions).size !== permissions.length)
|
|
1446
|
+
throw new Error("desired key permissions must be unique");
|
|
1447
|
+
return { name: text(key.name, "desired key name"), vaultField: text(key.vaultField, "desired vault field"), permissions: permissions.sort() };
|
|
1448
|
+
});
|
|
1449
|
+
if (new Set(desired.map((key) => key.name)).size !== desired.length)
|
|
1450
|
+
throw new Error("desired key names must be unique");
|
|
1451
|
+
if (new Set(desired.map((key) => key.vaultField)).size !== desired.length)
|
|
1452
|
+
throw new Error("desired key vault fields must be unique");
|
|
1453
|
+
if (!Array.isArray(config.oldKeys))
|
|
1454
|
+
throw new Error("oldKeys must be an array");
|
|
1455
|
+
const oldKeys = config.oldKeys.map((raw) => {
|
|
1456
|
+
const old = object(raw, "old Unraid key");
|
|
1457
|
+
return { id: text(old.id, "old key id"), secretAdapter: adapter(old.secretAdapter, "old key secretAdapter") };
|
|
1458
|
+
});
|
|
1459
|
+
if (new Set(oldKeys.map((key) => key.id)).size !== oldKeys.length)
|
|
1460
|
+
throw new Error("old key IDs must be unique");
|
|
1461
|
+
const initial = inventory(await deps.runAdapter(inventoryAdapter, { operation: "inventory_keys", targetServerId }));
|
|
1462
|
+
const occupiedCanonicalContract = targetServerId === "sanctuary-unraid"
|
|
1463
|
+
&& desired.length === 2 && oldKeys.length === 2
|
|
1464
|
+
&& JSON.stringify(desired.map((key) => key.name).sort()) === JSON.stringify(["Butler RO", "Butler RW"])
|
|
1465
|
+
&& (pathEntryExists(evidencePath) || (initial.length === 2 && desired.every((key) => {
|
|
1466
|
+
const existing = initial.find((entry) => entry.name === key.name);
|
|
1467
|
+
return existing !== undefined && oldKeys.some((old) => old.id === existing.id)
|
|
1468
|
+
&& existing.roles.length === 0 && sameStrings(existing.permissions, key.permissions);
|
|
1469
|
+
})))
|
|
1470
|
+
&& [inventoryAdapter, createAdapter, storeAdapter, revokeAdapter, probeAdapter, ...oldKeys.map((old) => old.secretAdapter)]
|
|
1471
|
+
.every((value) => value === SANCTUARY_ACCEPTANCE_ADAPTER);
|
|
1472
|
+
if (occupiedCanonicalContract) {
|
|
1473
|
+
await rotateOccupiedCanonicalUnraidKeys({
|
|
1474
|
+
root, evidencePath, targetServerId,
|
|
1475
|
+
adapters: { inventory: inventoryAdapter, create: createAdapter, store: storeAdapter, revoke: revokeAdapter, probe: probeAdapter },
|
|
1476
|
+
desired, oldKeys, initial, deps,
|
|
1477
|
+
});
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
refuseExistingCheckpoint(root, evidencePath);
|
|
1481
|
+
for (const key of desired)
|
|
1482
|
+
if (initial.some((entry) => entry.name === key.name))
|
|
1483
|
+
throw new Error(`Unraid key ${key.name} already exists`);
|
|
1484
|
+
for (const old of oldKeys)
|
|
1485
|
+
if (!initial.some((entry) => entry.id === old.id))
|
|
1486
|
+
throw new Error(`old Unraid key ${old.id} is absent`);
|
|
1487
|
+
const base = { schemaVersion: 1, operation: "unraid-key-rotate", phase: "preflight", targetServerId, initialInventoryDigest: digest(initial), createdKeyIds: [], revokedKeyIds: [] };
|
|
1488
|
+
initializeCheckpoint(root, evidencePath, base);
|
|
1489
|
+
const createdKeyIds = [];
|
|
1490
|
+
const revokedKeyIds = [];
|
|
1491
|
+
try {
|
|
1492
|
+
for (const key of desired) {
|
|
1493
|
+
const created = object(await deps.runAdapter(createAdapter, { operation: "create_key", targetServerId, name: key.name, permissions: key.permissions }), "created Unraid key");
|
|
1494
|
+
const id = text(created.id, "created key id");
|
|
1495
|
+
const rawKey = text(created.key, "created raw key");
|
|
1496
|
+
if (created.name !== key.name || !sameStrings(stringArray(created.permissions, "created permissions"), key.permissions)
|
|
1497
|
+
|| !Array.isArray(created.roles) || created.roles.length !== 0)
|
|
1498
|
+
throw new Error("created Unraid key scope mismatch");
|
|
1499
|
+
if (initial.some((entry) => entry.id === id) || oldKeys.some((entry) => entry.id === id) || createdKeyIds.includes(id)) {
|
|
1500
|
+
throw new Error("created Unraid key ID is preexisting or reused");
|
|
1501
|
+
}
|
|
1502
|
+
createdKeyIds.push(id);
|
|
1503
|
+
replaceCheckpoint(root, evidencePath, { ...base, phase: "key_created", createdKeyIds, revokedKeyIds });
|
|
1504
|
+
const stored = object(await deps.runAdapter(storeAdapter, { operation: "store_key", targetServerId, vaultField: key.vaultField, keyId: id, key: rawKey }), "key store result");
|
|
1505
|
+
if (stored.stored !== true || stored.keyId !== id)
|
|
1506
|
+
throw new Error("vault did not attest exact key storage");
|
|
1507
|
+
const probe = object(await deps.runAdapter(probeAdapter, { operation: "probe_new_key", targetServerId, id, key: rawKey }), "new key probe");
|
|
1508
|
+
if (probe.valid !== true)
|
|
1509
|
+
throw new Error("new Unraid key readiness failed");
|
|
1510
|
+
replaceCheckpoint(root, evidencePath, { ...base, phase: "key_attested", createdKeyIds, revokedKeyIds });
|
|
1511
|
+
}
|
|
1512
|
+
for (const old of oldKeys) {
|
|
1513
|
+
const reconciled = inventory(await deps.runAdapter(inventoryAdapter, { operation: "inventory_keys", targetServerId }));
|
|
1514
|
+
const expected = [
|
|
1515
|
+
...initial.filter((entry) => !revokedKeyIds.includes(entry.id)),
|
|
1516
|
+
...desired.map((key, index) => ({ id: createdKeyIds[index], name: key.name, permissions: key.permissions, roles: [] })),
|
|
1517
|
+
];
|
|
1518
|
+
if (!sameInventory(reconciled, expected))
|
|
1519
|
+
throw new Error("Unraid inventory changed ambiguously before exact revoke");
|
|
1520
|
+
const secretResult = object(await deps.runAdapter(old.secretAdapter, { operation: "read_old_key", targetServerId, id: old.id }), "old key secret result");
|
|
1521
|
+
const rawOldKey = text(secretResult.key, "old raw key");
|
|
1522
|
+
const revoked = object(await deps.runAdapter(revokeAdapter, { operation: "revoke_key", targetServerId, id: old.id }), "key revoke result");
|
|
1523
|
+
if (revoked.revoked !== true || revoked.id !== old.id)
|
|
1524
|
+
throw new Error("Unraid revoke did not attest exact key id");
|
|
1525
|
+
const probe = object(await deps.runAdapter(probeAdapter, { operation: "probe_revoked_key", targetServerId, id: old.id, key: rawOldKey }), "revoked key probe");
|
|
1526
|
+
if (probe.valid !== false || (probe.status !== 401 && probe.status !== 403))
|
|
1527
|
+
throw new Error("revoked Unraid key still authenticates");
|
|
1528
|
+
revokedKeyIds.push(old.id);
|
|
1529
|
+
replaceCheckpoint(root, evidencePath, { ...base, phase: "old_key_revoked", createdKeyIds, revokedKeyIds });
|
|
1530
|
+
}
|
|
1531
|
+
const final = inventory(await deps.runAdapter(inventoryAdapter, { operation: "inventory_keys", targetServerId }));
|
|
1532
|
+
const expectedFinal = [
|
|
1533
|
+
...initial.filter((entry) => !oldKeys.some((old) => old.id === entry.id)),
|
|
1534
|
+
...desired.map((key, index) => ({ id: createdKeyIds[index], name: key.name, permissions: key.permissions, roles: [] })),
|
|
1535
|
+
];
|
|
1536
|
+
if (!sameInventory(final, expectedFinal))
|
|
1537
|
+
throw new Error("final Unraid key inventory mismatch");
|
|
1538
|
+
replaceCheckpoint(root, evidencePath, { ...base, phase: "complete", createdKeyIds, revokedKeyIds, finalInventoryDigest: digest(final), completedAt: deps.now() });
|
|
1539
|
+
}
|
|
1540
|
+
catch (error) {
|
|
1541
|
+
failedCheckpoint(root, evidencePath, { ...base, createdKeyIds, revokedKeyIds }, error);
|
|
1542
|
+
throw error;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
async function captureScenarioEvidence(input) {
|
|
1546
|
+
const { config, root, executable, harnessSha256, label, deadline, intervalMs, deps } = input;
|
|
1547
|
+
const evidencePath = path.join(root, `${label}.json`);
|
|
1548
|
+
refuseExistingCheckpoint(root, evidencePath);
|
|
1549
|
+
const gate = exports.SANCTUARY_SCENARIO_GATES[label];
|
|
1550
|
+
const sources = exports.SANCTUARY_SCENARIO_SOURCES[label];
|
|
1551
|
+
const operationDeadline = deadline - SCENARIO_CLEANUP_RESERVE_MS;
|
|
1552
|
+
const runBefore = async (payload, callLabel, callDeadline = operationDeadline) => {
|
|
1553
|
+
const remainingMs = callDeadline - deps.now();
|
|
1554
|
+
if (remainingMs <= 0)
|
|
1555
|
+
throw new Error(`${label} ${callLabel} timed out at its remaining deadline`);
|
|
1556
|
+
return deps.runAdapter(executable, payload, remainingMs);
|
|
1557
|
+
};
|
|
1558
|
+
let capture;
|
|
1559
|
+
let operationError;
|
|
1560
|
+
try {
|
|
1561
|
+
let response = object(await runBefore({
|
|
1562
|
+
operation: SANCTUARY_SCENARIO_ADAPTER_OPERATION,
|
|
1563
|
+
phase: "begin",
|
|
1564
|
+
label,
|
|
1565
|
+
externalGate: gate,
|
|
1566
|
+
sources,
|
|
1567
|
+
}, "begin"), `${label} begin result`);
|
|
1568
|
+
exactObjectKeys(response, response.state === "complete" ? ["assertions", "checkpointDigest", "sourceDigests", "state"] : ["checkpointDigest", "state"], `${label} begin result`);
|
|
1569
|
+
const checkpointDigest = opaqueDigest(response.checkpointDigest, `${label} checkpointDigest`);
|
|
1570
|
+
while (response.state === "waiting") {
|
|
1571
|
+
const remainingBeforeSleep = operationDeadline - deps.now();
|
|
1572
|
+
if (remainingBeforeSleep <= 0)
|
|
1573
|
+
throw new Error(`${label} live scenario timed out while awaiting ${gate}`);
|
|
1574
|
+
await deps.sleep(Math.min(intervalMs, remainingBeforeSleep));
|
|
1575
|
+
response = object(await runBefore({
|
|
1576
|
+
operation: SANCTUARY_SCENARIO_ADAPTER_OPERATION,
|
|
1577
|
+
phase: "poll",
|
|
1578
|
+
label,
|
|
1579
|
+
externalGate: gate,
|
|
1580
|
+
sources,
|
|
1581
|
+
checkpointDigest,
|
|
1582
|
+
}, "poll"), `${label} poll result`);
|
|
1583
|
+
exactObjectKeys(response, response.state === "complete" ? ["assertions", "checkpointDigest", "sourceDigests", "state"] : ["checkpointDigest", "state"], `${label} poll result`);
|
|
1584
|
+
if (opaqueDigest(response.checkpointDigest, `${label} checkpointDigest`) !== checkpointDigest)
|
|
1585
|
+
throw new Error(`${label} checkpoint identity drifted`);
|
|
1586
|
+
}
|
|
1587
|
+
if (response.state !== "complete")
|
|
1588
|
+
throw new Error(`${label} scenario returned an invalid state`);
|
|
1589
|
+
const sourceDigests = object(response.sourceDigests, `${label} sourceDigests`);
|
|
1590
|
+
exactObjectKeys(sourceDigests, sources, `${label} sourceDigests`);
|
|
1591
|
+
for (const source of sources)
|
|
1592
|
+
opaqueDigest(sourceDigests[source], `${label} ${source} source digest`);
|
|
1593
|
+
capture = {
|
|
1594
|
+
checkpointDigest,
|
|
1595
|
+
assertions: validateSanctuaryUnit16EvidenceAssertions(label, response.assertions),
|
|
1596
|
+
sourceDigest: normalizedEvidenceHash(sourceDigests),
|
|
1597
|
+
provenance: await liveEvidenceProvenance(config, deps, operationDeadline),
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
catch (error) {
|
|
1601
|
+
operationError = error;
|
|
1602
|
+
}
|
|
1603
|
+
let cleanupError;
|
|
1604
|
+
try {
|
|
1605
|
+
const cleanupRemainingMs = Math.max(1, deadline - deps.now());
|
|
1606
|
+
const finalized = object(await deps.runAdapter(executable, { operation: "finalize_acceptance_scenarios" }, cleanupRemainingMs), "scenario finalization result");
|
|
1607
|
+
exactObjectKeys(finalized, ["finalized"], "scenario finalization result");
|
|
1608
|
+
if (finalized.finalized !== true)
|
|
1609
|
+
throw new Error("scenario finalization failed");
|
|
1610
|
+
}
|
|
1611
|
+
catch (error) {
|
|
1612
|
+
cleanupError = error;
|
|
1613
|
+
}
|
|
1614
|
+
if (operationError && cleanupError) {
|
|
1615
|
+
const operationMessage = operationError instanceof Error ? operationError.message : "unknown operation error";
|
|
1616
|
+
const cleanupMessage = cleanupError instanceof Error ? cleanupError.message : "unknown cleanup error";
|
|
1617
|
+
throw new AggregateError([operationError, cleanupError], `${label} scenario operation failed: ${operationMessage}; cleanup failed: ${cleanupMessage}`);
|
|
1618
|
+
}
|
|
1619
|
+
if (operationError)
|
|
1620
|
+
throw operationError;
|
|
1621
|
+
if (cleanupError)
|
|
1622
|
+
throw cleanupError;
|
|
1623
|
+
if (!capture)
|
|
1624
|
+
throw new Error(`${label} scenario capture was not produced`);
|
|
1625
|
+
const value = {
|
|
1626
|
+
schemaVersion: 1,
|
|
1627
|
+
operation: label,
|
|
1628
|
+
phase: "complete",
|
|
1629
|
+
provenance: { ...capture.provenance, harnessSha256 },
|
|
1630
|
+
producer: {
|
|
1631
|
+
command: SANCTUARY_SCENARIO_COMMAND,
|
|
1632
|
+
adapterOperation: SANCTUARY_SCENARIO_ADAPTER_OPERATION,
|
|
1633
|
+
checkpointDigest: capture.checkpointDigest,
|
|
1634
|
+
sourceDigest: capture.sourceDigest,
|
|
1635
|
+
captureDigest: normalizedEvidenceHash(capture.assertions),
|
|
1636
|
+
},
|
|
1637
|
+
assertions: capture.assertions,
|
|
1638
|
+
};
|
|
1639
|
+
completeEvidenceContract(value, label);
|
|
1640
|
+
initializeCheckpoint(root, evidencePath, value);
|
|
1641
|
+
}
|
|
1642
|
+
async function scenarioMatrixSnapshot(config, deps) {
|
|
1643
|
+
const root = privateAllowedRoot(config);
|
|
1644
|
+
const executable = adapter(config.adapter, "scenario adapter");
|
|
1645
|
+
if (executable !== PACKAGED_PROVENANCE_ADAPTER || config.provenanceAdapter !== PACKAGED_PROVENANCE_ADAPTER) {
|
|
1646
|
+
throw new Error("scenario matrix requires the fixed packaged acceptance adapter");
|
|
1647
|
+
}
|
|
1648
|
+
const timeoutMs = integer(config.timeoutMs, "scenario total timeoutMs", 1);
|
|
1649
|
+
const intervalMs = integer(config.intervalMs, "scenario intervalMs", 1);
|
|
1650
|
+
if (timeoutMs > MAX_MATRIX_TIMEOUT_MS || intervalMs > MAX_SCENARIO_INTERVAL_MS)
|
|
1651
|
+
throw new Error("scenario timing bound is invalid");
|
|
1652
|
+
const harnessSha256 = packagedHarnessSha256(config.harnessPath, deps);
|
|
1653
|
+
const totalDeadline = deps.now() + timeoutMs;
|
|
1654
|
+
for (const label of REBOOT_SCENARIO_LABELS) {
|
|
1655
|
+
const value = object(JSON.parse((0, node_fs_1.readFileSync)(requirePrivateRegularFile(root, path.join(root, `${label}.json`), `${label} phase evidence`), "utf8")), `${label} phase evidence`);
|
|
1656
|
+
const provenance = completeEvidenceContract(value, label);
|
|
1657
|
+
if (provenance.harnessSha256 !== harnessSha256)
|
|
1658
|
+
throw new Error(`${label} phase evidence does not match the packaged harness`);
|
|
1659
|
+
}
|
|
1660
|
+
for (const label of exports.SANCTUARY_UNIT_16_EVIDENCE_LABELS.filter((candidate) => !REBOOT_SCENARIO_LABELS.has(candidate))) {
|
|
1661
|
+
const scenarioDeadline = Math.min(totalDeadline, deps.now() + sanctuaryScenarioTimeoutBudget(label));
|
|
1662
|
+
await captureScenarioEvidence({ config, root, executable, harnessSha256, label, deadline: scenarioDeadline, intervalMs, deps });
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
async function evidenceSnapshot(config, deps) {
|
|
1666
|
+
if (config.schema === "sanctuary-unit-16-matrix-v1") {
|
|
1667
|
+
await scenarioMatrixSnapshot(config, deps);
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
const root = privateAllowedRoot(config);
|
|
1671
|
+
const evidencePath = confinedPath(root, config.evidencePath, "evidencePath");
|
|
1672
|
+
refuseExistingCheckpoint(root, evidencePath);
|
|
1673
|
+
const schema = text(config.schema, "snapshot schema");
|
|
1674
|
+
if (schema !== "postboot-health-v1")
|
|
1675
|
+
throw new Error("unsupported standalone evidence schema");
|
|
1676
|
+
const executable = adapter(config.adapter, "snapshot adapter");
|
|
1677
|
+
const payload = await deps.runAdapter(executable, { operation: "evidence_snapshot", schema });
|
|
1678
|
+
const values = fixedEvidenceValues(schema, payload);
|
|
1679
|
+
initializeCheckpoint(root, evidencePath, {
|
|
1680
|
+
schemaVersion: 1,
|
|
1681
|
+
operation: "evidence-snapshot",
|
|
1682
|
+
phase: "complete",
|
|
1683
|
+
schema,
|
|
1684
|
+
capturedAt: deps.now(),
|
|
1685
|
+
values,
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
async function captureRebootScenario(config, root, label, deps) {
|
|
1689
|
+
const executable = adapter(config.scenarioAdapter, "reboot scenario adapter");
|
|
1690
|
+
if (executable !== PACKAGED_PROVENANCE_ADAPTER || config.provenanceAdapter !== PACKAGED_PROVENANCE_ADAPTER) {
|
|
1691
|
+
throw new Error("reboot scenario capture requires the fixed packaged acceptance adapter");
|
|
1692
|
+
}
|
|
1693
|
+
const harnessSha256 = packagedHarnessSha256(config.harnessPath, deps);
|
|
1694
|
+
const intervalMs = integer(config.scenarioIntervalMs, "reboot scenario intervalMs", 1);
|
|
1695
|
+
const timeoutMs = integer(config.scenarioTimeoutMs, "reboot scenario timeoutMs", 1);
|
|
1696
|
+
if (timeoutMs > sanctuaryScenarioTimeoutBudget(label) || intervalMs > MAX_SCENARIO_INTERVAL_MS)
|
|
1697
|
+
throw new Error("reboot scenario timing bound is invalid");
|
|
1698
|
+
await captureScenarioEvidence({
|
|
1699
|
+
config, root, executable, harnessSha256, label,
|
|
1700
|
+
deadline: deps.now() + timeoutMs,
|
|
1701
|
+
intervalMs,
|
|
1702
|
+
deps,
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
async function rebootRequest(config, deps) {
|
|
1706
|
+
const root = privateAllowedRoot(config);
|
|
1707
|
+
const evidencePath = confinedPath(root, config.evidencePath, "evidencePath");
|
|
1708
|
+
refuseExistingCheckpoint(root, evidencePath);
|
|
1709
|
+
const targetId = text(config.targetId, "targetId");
|
|
1710
|
+
const executable = adapter(config.adapter, "reboot adapter");
|
|
1711
|
+
const idempotencyKey = deps.randomBytes(16).toString("hex");
|
|
1712
|
+
const preflight = object(await deps.runAdapter(executable, { operation: "reboot_preflight_snapshot", targetId }), "reboot preflight adapter result");
|
|
1713
|
+
const preflightDigest = opaqueDigest(preflight.digest, "reboot preflight digest");
|
|
1714
|
+
const processBindingDigest = opaqueDigest(preflight.processBindingDigest, "reboot process binding digest");
|
|
1715
|
+
if (preflight.arrayReady !== true || typeof preflight.parityActive !== "boolean" || typeof preflight.moverActive !== "boolean" || typeof preflight.mutationActive !== "boolean") {
|
|
1716
|
+
throw new Error("reboot preflight is invalid");
|
|
1717
|
+
}
|
|
1718
|
+
const unrelatedHostOperations = [preflight.parityActive, preflight.moverActive, preflight.mutationActive].filter((value) => value === true).length;
|
|
1719
|
+
if (preflight.safe !== true || unrelatedHostOperations !== 0)
|
|
1720
|
+
throw new Error("reboot preflight is unsafe");
|
|
1721
|
+
const prebootIntegrity = object(await deps.runAdapter(executable, { operation: "postboot_integrity_snapshot" }), "preboot integrity snapshot");
|
|
1722
|
+
const base = { schemaVersion: 1, operation: "reboot", phase: "preflight", targetId, idempotencyDigest: digest(idempotencyKey), preflightDigest, processBindingDigest, prebootIntegrity, unrelatedHostOperations, requestedAt: deps.now() };
|
|
1723
|
+
initializeCheckpoint(root, evidencePath, base);
|
|
1724
|
+
try {
|
|
1725
|
+
if (config.scenarioAdapter !== undefined) {
|
|
1726
|
+
await captureRebootScenario(config, root, "unit-16a-pre-reboot-checkpoint", deps);
|
|
1727
|
+
}
|
|
1728
|
+
const response = object(await deps.runAdapter(executable, { operation: "request_reboot", targetId, idempotencyKey, preflightDigest, processBindingDigest }), "reboot adapter result");
|
|
1729
|
+
if (response.accepted !== true || response.targetId !== targetId)
|
|
1730
|
+
throw new Error("reboot adapter did not accept the exact target");
|
|
1731
|
+
const requestId = text(response.requestId, "reboot requestId");
|
|
1732
|
+
const reservationId = opaqueDigest(response.reservationId, "reboot reservationId");
|
|
1733
|
+
const prebootDigest = digest(text(response.prebootId, "reboot prebootId"));
|
|
1734
|
+
replaceCheckpoint(root, evidencePath, { ...base, phase: "requested", requestId, reservationId, prebootDigest });
|
|
1735
|
+
}
|
|
1736
|
+
catch (error) {
|
|
1737
|
+
failedCheckpoint(root, evidencePath, base, error);
|
|
1738
|
+
throw error;
|
|
1739
|
+
}
|
|
1740
|
+
if (config.scenarioAdapter !== undefined) {
|
|
1741
|
+
await captureRebootScenario(config, root, "unit-16a-reboot-request", deps);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
async function rebootResume(config, deps) {
|
|
1745
|
+
const root = privateAllowedRoot(config);
|
|
1746
|
+
const evidencePath = confinedPath(root, config.evidencePath, "evidencePath");
|
|
1747
|
+
const checkpoint = readCheckpoint(root, evidencePath);
|
|
1748
|
+
if (checkpoint.operation !== "reboot" || (checkpoint.phase !== "requested" && checkpoint.phase !== "complete"))
|
|
1749
|
+
throw new Error("reboot checkpoint is not resumable");
|
|
1750
|
+
const targetId = text(checkpoint.targetId, "checkpoint targetId");
|
|
1751
|
+
const requestId = text(checkpoint.requestId, "checkpoint requestId");
|
|
1752
|
+
const prebootDigest = opaqueDigest(checkpoint.prebootDigest, "checkpoint prebootDigest");
|
|
1753
|
+
if (checkpoint.phase === "requested") {
|
|
1754
|
+
const executable = adapter(config.adapter, "reboot poll adapter");
|
|
1755
|
+
const timeoutMs = integer(config.timeoutMs, "timeoutMs", 1);
|
|
1756
|
+
const intervalMs = integer(config.intervalMs, "intervalMs", 1);
|
|
1757
|
+
const deadline = deps.now() + timeoutMs;
|
|
1758
|
+
let complete = false;
|
|
1759
|
+
while (deps.now() < deadline) {
|
|
1760
|
+
const response = object(await deps.runAdapter(executable, { operation: "poll_reboot", targetId, requestId }), "reboot poll result");
|
|
1761
|
+
if (response.targetId !== targetId || response.requestId !== requestId)
|
|
1762
|
+
throw new Error("reboot target drift");
|
|
1763
|
+
if (response.state === "ready") {
|
|
1764
|
+
const bootId = text(response.bootId, "reboot bootId");
|
|
1765
|
+
const postbootDigest = digest(bootId);
|
|
1766
|
+
if (postbootDigest === prebootDigest)
|
|
1767
|
+
throw new Error("reboot boot identity did not change");
|
|
1768
|
+
replaceCheckpoint(root, evidencePath, { ...checkpoint, phase: "complete", postbootDigest, completedAt: deps.now() });
|
|
1769
|
+
complete = true;
|
|
1770
|
+
break;
|
|
1771
|
+
}
|
|
1772
|
+
if (response.state !== "booting" && response.state !== "offline")
|
|
1773
|
+
throw new Error("reboot poll returned an invalid state");
|
|
1774
|
+
await deps.sleep(intervalMs);
|
|
1775
|
+
}
|
|
1776
|
+
if (!complete)
|
|
1777
|
+
throw new Error("reboot resume timed out");
|
|
1778
|
+
}
|
|
1779
|
+
else {
|
|
1780
|
+
opaqueDigest(checkpoint.postbootDigest, "checkpoint postbootDigest");
|
|
1781
|
+
}
|
|
1782
|
+
if (config.scenarioAdapter !== undefined && !pathEntryExists(path.join(root, "unit-16a-boot-recovery-milestones.json"))) {
|
|
1783
|
+
await captureRebootScenario(config, root, "unit-16a-boot-recovery-milestones", deps);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
async function executeSanctuaryAcceptanceHarness(command, rawConfig, deps = createSanctuaryAcceptanceHarnessDependencies()) {
|
|
1787
|
+
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.sanctuary_acceptance_start", message: "Sanctuary acceptance operation started", meta: { command } });
|
|
1788
|
+
try {
|
|
1789
|
+
const config = object(rawConfig, "acceptance config");
|
|
1790
|
+
switch (command) {
|
|
1791
|
+
case "telegram-bootstrap": {
|
|
1792
|
+
await telegramBootstrap(config, deps);
|
|
1793
|
+
break;
|
|
1794
|
+
}
|
|
1795
|
+
case "cursor-snapshot":
|
|
1796
|
+
await cursorSnapshot(config, deps);
|
|
1797
|
+
break;
|
|
1798
|
+
case "cursor-delta":
|
|
1799
|
+
await cursorDelta(config, deps);
|
|
1800
|
+
break;
|
|
1801
|
+
case "callback-inject":
|
|
1802
|
+
await callbackInject(config, deps);
|
|
1803
|
+
break;
|
|
1804
|
+
case "unraid-key-rotate":
|
|
1805
|
+
await unraidKeyRotate(config, deps);
|
|
1806
|
+
break;
|
|
1807
|
+
case "evidence-snapshot":
|
|
1808
|
+
await evidenceSnapshot(config, deps);
|
|
1809
|
+
break;
|
|
1810
|
+
case "reboot-request":
|
|
1811
|
+
await rebootRequest(config, deps);
|
|
1812
|
+
break;
|
|
1813
|
+
case "reboot-resume":
|
|
1814
|
+
await rebootResume(config, deps);
|
|
1815
|
+
break;
|
|
1816
|
+
case "evidence-bundle-index":
|
|
1817
|
+
await evidenceBundleIndex(config, deps);
|
|
1818
|
+
break;
|
|
1819
|
+
case "evidence-bundle-verify":
|
|
1820
|
+
await verifyEvidenceBundle(config, deps);
|
|
1821
|
+
break;
|
|
1822
|
+
default: throw new Error("unknown Sanctuary acceptance command");
|
|
1823
|
+
}
|
|
1824
|
+
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.sanctuary_acceptance_end", message: "Sanctuary acceptance operation completed", meta: { command } });
|
|
1825
|
+
}
|
|
1826
|
+
catch (error) {
|
|
1827
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", component: "daemon", event: "daemon.sanctuary_acceptance_error", message: "Sanctuary acceptance operation failed", meta: { command, errorCategory: safeErrorCategory(error) } });
|
|
1828
|
+
throw error;
|
|
1829
|
+
}
|
|
1830
|
+
}
|