@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,710 @@
|
|
|
1
|
+
#!/usr/local/bin/node
|
|
2
|
+
|
|
3
|
+
import { spawn, spawnSync } from "node:child_process"
|
|
4
|
+
import { closeSync, constants, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readlinkSync, readdirSync, statSync, writeFileSync } from "node:fs"
|
|
5
|
+
import { fileURLToPath } from "node:url"
|
|
6
|
+
|
|
7
|
+
const DOCKER = "/usr/bin/docker"
|
|
8
|
+
const KEY_ROOT = "/boot/config/plugins/dynamix.my.servers/keys"
|
|
9
|
+
const GRAPHQL_ENDPOINT = "http://127.0.0.1/graphql"
|
|
10
|
+
const IMAGE_ID = /^sha256:[0-9a-f]{64}$/u
|
|
11
|
+
const CONTAINER_ID = /^[0-9a-f]{64}$/u
|
|
12
|
+
const CANONICAL_NAMES = ["ouro-butler", "ouro-butler-staging", "ouro-butler-rollback"]
|
|
13
|
+
const PROFILES = Object.freeze({
|
|
14
|
+
staging: Object.freeze({ name: "staging", containerName: "ouro-butler-staging", requiredStopped: [], optionalStopped: [], forbidden: ["ouro-butler", "ouro-butler-rollback"] }),
|
|
15
|
+
final: Object.freeze({ name: "final", containerName: "ouro-butler", requiredStopped: [], optionalStopped: ["ouro-butler-rollback"], forbidden: ["ouro-butler-staging"] }),
|
|
16
|
+
})
|
|
17
|
+
const DOCUMENTED_UNIX_CONTROLS = [
|
|
18
|
+
"/tmp/ouroboros-daemon.sock",
|
|
19
|
+
"/tmp/ouroboros-daemon.sock.frontend",
|
|
20
|
+
"/home/ouro/AgentBundles/sanctuary.ouro/state/acceptance/telegram-control.sock",
|
|
21
|
+
]
|
|
22
|
+
const DOCUMENTED_LOOPBACK_TCP_CONTROLS = new Set([6876])
|
|
23
|
+
const TERMINAL_CONTAINMENT_MAX_SAMPLES = 8
|
|
24
|
+
const TARGET_THAW_MAX_ATTEMPTS = 3
|
|
25
|
+
const WATCHDOG_POLL_MS = 100
|
|
26
|
+
const WATCHDOG_PARENT_DEATH_ENFORCEMENT_MS = 25_000
|
|
27
|
+
const RO_PERMISSIONS = ["ARRAY", "DASHBOARD", "DISK", "DOCKER", "INFO", "LOGS", "NOTIFICATIONS", "SHARE", "VARS"]
|
|
28
|
+
.map((resource) => `${resource}:READ_ANY`).sort()
|
|
29
|
+
|
|
30
|
+
function object(value, label) {
|
|
31
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`)
|
|
32
|
+
return value
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function targetProfile(name) {
|
|
36
|
+
const profile = PROFILES[name]
|
|
37
|
+
if (!profile) throw new Error("deployment target profile is invalid")
|
|
38
|
+
return profile
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function exactSet(left, right) {
|
|
42
|
+
return left.size === right.size && [...left].every((entry) => right.has(entry))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readCanonicalReadDescriptor() {
|
|
46
|
+
const root = statSync(KEY_ROOT)
|
|
47
|
+
if (!root.isDirectory() || root.uid !== 0 || (root.mode & 0o077) !== 0) throw new Error("Unraid key directory metadata is invalid")
|
|
48
|
+
const matches = []
|
|
49
|
+
for (const entry of readdirSync(KEY_ROOT, { withFileTypes: true })) {
|
|
50
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) throw new Error("unexpected Unraid key directory entry")
|
|
51
|
+
const fd = openSync(`${KEY_ROOT}/${entry.name}`, constants.O_RDONLY | constants.O_NOFOLLOW)
|
|
52
|
+
let record
|
|
53
|
+
try {
|
|
54
|
+
const metadata = fstatSync(fd)
|
|
55
|
+
if (!metadata.isFile() || metadata.uid !== 0 || (metadata.mode & 0o777) !== 0o600) throw new Error("Unraid key file metadata is invalid")
|
|
56
|
+
record = object(JSON.parse(readFileSync(fd, "utf8")), "Unraid key record")
|
|
57
|
+
} finally { closeSync(fd) }
|
|
58
|
+
const permissions = Array.isArray(record.permissions)
|
|
59
|
+
? record.permissions.flatMap((raw) => {
|
|
60
|
+
const permission = object(raw, "Unraid key permission")
|
|
61
|
+
return Array.isArray(permission.actions) ? permission.actions.map((action) => `${permission.resource}:${action}`) : []
|
|
62
|
+
}).sort()
|
|
63
|
+
: []
|
|
64
|
+
if (record.name === "Butler RO" && Array.isArray(record.roles) && record.roles.length === 0 && JSON.stringify(permissions) === JSON.stringify(RO_PERMISSIONS) && typeof record.key === "string" && record.key) matches.push(record.key)
|
|
65
|
+
}
|
|
66
|
+
if (matches.length !== 1) throw new Error("canonical read-only Unraid key is absent or ambiguous")
|
|
67
|
+
return matches[0]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function queryGraphqlAutostart(fetchImpl = fetch, readDescriptor = readCanonicalReadDescriptor) {
|
|
71
|
+
const response = await fetchImpl(GRAPHQL_ENDPOINT, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: { "content-type": "application/json", "x-api-key": readDescriptor() },
|
|
74
|
+
body: JSON.stringify({ query: "query AcceptanceContainerTopology { vars { id } docker { containers(skipCache: true) { id names autoStart } } }", variables: {} }),
|
|
75
|
+
signal: AbortSignal.timeout(10_000),
|
|
76
|
+
})
|
|
77
|
+
if (!response.ok) throw new Error("Unraid container topology query failed")
|
|
78
|
+
const envelope = object(await response.json(), "Unraid container topology response")
|
|
79
|
+
if (envelope.errors || !envelope.data) throw new Error("Unraid container topology query was rejected")
|
|
80
|
+
const data = object(envelope.data, "Unraid topology data")
|
|
81
|
+
const serverIdentity = /^([0-9a-f]{64}):vars$/u.exec(object(data.vars, "Unraid topology vars").id)
|
|
82
|
+
if (!serverIdentity) throw new Error("Unraid server identity is invalid")
|
|
83
|
+
const containers = object(data.docker, "Unraid topology docker").containers
|
|
84
|
+
if (!Array.isArray(containers)) throw new Error("Unraid container topology is invalid")
|
|
85
|
+
const result = new Map()
|
|
86
|
+
for (const raw of containers) {
|
|
87
|
+
const container = object(raw, "Unraid topology container")
|
|
88
|
+
if (!Array.isArray(container.names) || container.names.some((name) => typeof name !== "string")) throw new Error("Unraid container topology identity is invalid")
|
|
89
|
+
const canonical = container.names.map((name) => name.replace(/^\//u, "")).filter((name) => CANONICAL_NAMES.includes(name))
|
|
90
|
+
const identity = typeof container.id === "string" ? /^([0-9a-f]{64}):([0-9a-f]{64})$/u.exec(container.id) : null
|
|
91
|
+
if (canonical.length > 1 || (canonical.length === 1 && (!identity || identity[1] !== serverIdentity[1]))) throw new Error("Unraid container topology identity is ambiguous or belongs to another server")
|
|
92
|
+
if (canonical.length === 1 && typeof container.autoStart !== "boolean") throw new Error("Unraid container autostart state is invalid")
|
|
93
|
+
if (canonical.length === 1) {
|
|
94
|
+
if (result.has(canonical[0])) throw new Error("Unraid autostart topology is ambiguous")
|
|
95
|
+
result.set(canonical[0], { containerId: identity[2], autoStart: container.autoStart })
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return result
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeRecord(raw, label) {
|
|
102
|
+
const value = object(raw, label)
|
|
103
|
+
if (!CONTAINER_ID.test(value.id) || !Array.isArray(value.names) || value.names.length !== 1 || typeof value.names[0] !== "string") throw new Error(`${label} identity is invalid`)
|
|
104
|
+
const name = value.names[0].replace(/^\//u, "")
|
|
105
|
+
if (!CANONICAL_NAMES.includes(name)) throw new Error(`${label} name is not canonical`)
|
|
106
|
+
if (!IMAGE_ID.test(value.imageId) || typeof value.running !== "boolean" || typeof value.autoStart !== "boolean" || value.restartPolicy !== "unless-stopped" || value.networkMode !== "host") throw new Error(`${label} effective configuration is invalid`)
|
|
107
|
+
if (!Number.isSafeInteger(value.pid) || value.pid < 0 || value.running !== (value.pid > 0)) throw new Error(`${label} PID state is invalid`)
|
|
108
|
+
return { id: value.id, name, imageId: value.imageId, running: value.running, autoStart: value.autoStart, restartPolicy: value.restartPolicy, networkMode: value.networkMode, pid: value.pid }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function records(value, label) {
|
|
112
|
+
if (!Array.isArray(value)) throw new Error(`${label} must be an array`)
|
|
113
|
+
const normalized = value.map((entry, index) => normalizeRecord(entry, `${label}[${index}]`))
|
|
114
|
+
if (new Set(normalized.map(({ name }) => name)).size !== normalized.length || new Set(normalized.map(({ id }) => id)).size !== normalized.length) throw new Error(`${label} is duplicate or ambiguous`)
|
|
115
|
+
return normalized.sort((left, right) => left.name.localeCompare(right.name))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function signature(value) {
|
|
119
|
+
return JSON.stringify(value.map(({ id, name, imageId, running, autoStart, restartPolicy, networkMode, pid }) => ({ id, name, imageId, running, autoStart, restartPolicy, networkMode, pid })))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function attestDeploymentTarget(input) {
|
|
123
|
+
const value = object(input, "deployment target input")
|
|
124
|
+
const profile = targetProfile(value.profile)
|
|
125
|
+
if (!IMAGE_ID.test(value.expectedImageId)) throw new Error("expected image ID is invalid")
|
|
126
|
+
const before = records(value.topologyBefore, "topology before")
|
|
127
|
+
const inspected = records(value.inspected, "canonical inspections")
|
|
128
|
+
const after = records(value.topologyAfter, "topology after")
|
|
129
|
+
if (signature(before) !== signature(after) || signature(before) !== signature(inspected)) throw new Error("canonical deployment topology changed or was incompletely inspected")
|
|
130
|
+
const byName = new Map(inspected.map((entry) => [entry.name, entry]))
|
|
131
|
+
const target = byName.get(profile.containerName)
|
|
132
|
+
if (!target || target.imageId !== value.expectedImageId || !target.running || !target.autoStart) throw new Error("deployment target identity, image, state, or autostart is invalid")
|
|
133
|
+
for (const name of profile.requiredStopped) {
|
|
134
|
+
const stopped = byName.get(name)
|
|
135
|
+
if (!stopped || stopped.running || stopped.autoStart) throw new Error("required rollback target state is invalid")
|
|
136
|
+
}
|
|
137
|
+
for (const name of profile.optionalStopped) {
|
|
138
|
+
const stopped = byName.get(name)
|
|
139
|
+
if (stopped && (stopped.running || stopped.autoStart)) throw new Error("optional rollback target state is invalid")
|
|
140
|
+
}
|
|
141
|
+
for (const name of profile.forbidden) if (byName.has(name)) throw new Error("mixed deployment target state is invalid")
|
|
142
|
+
const activeRunningCardinality = inspected.filter(({ running }) => running).length
|
|
143
|
+
if (activeRunningCardinality !== 1) throw new Error("active Butler running cardinality must equal one")
|
|
144
|
+
return { schemaVersion: "sanctuary-deployment-target-v1", profile: profile.name, targetContainerName: profile.containerName, targetContainerId: target.id, targetImageId: target.imageId, targetPid: target.pid, activeRunningCardinality }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function attestOwnedListeners(input) {
|
|
148
|
+
const value = object(input, "listener ownership input")
|
|
149
|
+
if (!Number.isSafeInteger(value.rootPid) || value.rootPid <= 0 || !/^net:\[[0-9]+\]$/u.test(value.netnsBefore) || value.netnsBefore !== value.netnsAfter) throw new Error("target network namespace is invalid or changed")
|
|
150
|
+
const processIdsBefore = value.processIdsBefore
|
|
151
|
+
const processIdsAfter = value.processIdsAfter
|
|
152
|
+
const validProcesses = (pids) => Array.isArray(pids) && pids.length > 0 && pids.includes(value.rootPid) && pids.every((pid) => Number.isSafeInteger(pid) && pid > 0) && new Set(pids).size === pids.length
|
|
153
|
+
if (!validProcesses(processIdsBefore) || !validProcesses(processIdsAfter) || JSON.stringify([...processIdsBefore].sort((a, b) => a - b)) !== JSON.stringify([...processIdsAfter].sort((a, b) => a - b))) throw new Error("target cgroup process membership changed or is invalid")
|
|
154
|
+
const socketSet = (raw, label) => {
|
|
155
|
+
if (!Array.isArray(raw) || raw.some((inode) => !/^[0-9]+$/u.test(inode))) throw new Error(`${label} is invalid`)
|
|
156
|
+
return new Set(raw)
|
|
157
|
+
}
|
|
158
|
+
const inventory = (suffix) => {
|
|
159
|
+
const owned = socketSet(value[`socketInodes${suffix}`], `socket ownership ${suffix.toLowerCase()}`)
|
|
160
|
+
const select = (kind, fallback) => value[kind] ?? value[`${fallback}${suffix}`]
|
|
161
|
+
const tcpRaw = select("tcpListeners", "tcpListeners")
|
|
162
|
+
const udpRaw = select("udpListeners", "udpListeners")
|
|
163
|
+
const unixRaw = select("unixSockets", "unixSockets")
|
|
164
|
+
if (!Array.isArray(tcpRaw) || !Array.isArray(udpRaw) || !Array.isArray(unixRaw)) throw new Error("listener inventory is invalid")
|
|
165
|
+
const tcp = tcpRaw.filter((raw) => owned.has(object(raw, "TCP listener").inode))
|
|
166
|
+
const udp = udpRaw.filter((raw) => owned.has(object(raw, "UDP listener").inode))
|
|
167
|
+
const unix = unixRaw.filter((raw) => owned.has(object(raw, "Unix socket").inode))
|
|
168
|
+
const sortRecords = (records) => [...records].sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)))
|
|
169
|
+
const signature = JSON.stringify({ tcp: sortRecords(tcp), udp: sortRecords(udp), unix: sortRecords(unix) })
|
|
170
|
+
return { owned, tcp, udp, unix, signature }
|
|
171
|
+
}
|
|
172
|
+
const before = inventory("Before")
|
|
173
|
+
const after = inventory("After")
|
|
174
|
+
const terminalFields = ["socketInodesTerminal", "tcpListenersTerminal", "udpListenersTerminal", "unixSocketsTerminal"]
|
|
175
|
+
const providedTerminalFields = terminalFields.filter((field) => value[field] !== undefined)
|
|
176
|
+
if (providedTerminalFields.length !== 0 && providedTerminalFields.length !== terminalFields.length) throw new Error("terminal listener inventory is incomplete")
|
|
177
|
+
const terminal = providedTerminalFields.length === terminalFields.length ? inventory("Terminal") : after
|
|
178
|
+
if (!exactSet(before.owned, after.owned) || !exactSet(after.owned, terminal.owned)) throw new Error("target socket ownership changed")
|
|
179
|
+
if (before.signature !== after.signature || after.signature !== terminal.signature) throw new Error("target listener inventory changed")
|
|
180
|
+
const { owned, tcp, udp, unix } = terminal
|
|
181
|
+
for (const listener of tcp) {
|
|
182
|
+
if (listener.localAddress !== "127.0.0.1" || !DOCUMENTED_LOOPBACK_TCP_CONTROLS.has(listener.port)) throw new Error("target runtime owns an inbound TCP listener")
|
|
183
|
+
}
|
|
184
|
+
if (udp.length !== 0) throw new Error("target runtime owns an inbound UDP listener")
|
|
185
|
+
let unixControlSocketCount = 0
|
|
186
|
+
for (const socket of unix) {
|
|
187
|
+
if (socket.path === "" && socket.flags !== "00010000") continue
|
|
188
|
+
if (!DOCUMENTED_UNIX_CONTROLS.includes(socket.path) || socket.flags !== "00010000" || socket.type !== "0001" || socket.state !== "01") throw new Error("target runtime owns an undocumented Unix endpoint")
|
|
189
|
+
unixControlSocketCount += 1
|
|
190
|
+
}
|
|
191
|
+
return { schemaVersion: "sanctuary-listener-containment-v1", targetRootPid: value.rootPid, networkNamespace: value.netnsBefore, processCount: processIdsAfter.length, ownedSocketCount: owned.size, inboundTcpListenerCount: 0, inboundUdpListenerCount: 0, loopbackTcpControlCount: tcp.length, unixControlSocketCount }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function runDocker(args, timeoutMs = 20_000) {
|
|
195
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 20_000) throw new Error("Docker command timeout is invalid")
|
|
196
|
+
const result = spawnSync(DOCKER, args, { cwd: "/", encoding: "utf8", timeout: timeoutMs, maxBuffer: 1024 * 1024, env: { PATH: "/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin" }, stdio: ["ignore", "pipe", "ignore"] })
|
|
197
|
+
if (result.error || result.status !== 0) throw new Error("bounded deployment target inspection failed")
|
|
198
|
+
return result.stdout
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function pausedTargetState(target, run, timeoutMs = 20_000) {
|
|
202
|
+
const template = '{"containerId":{{json .Id}},"running":{{json .State.Running}},"paused":{{json .State.Paused}},"restarting":{{json .State.Restarting}},"dead":{{json .State.Dead}},"pid":{{json .State.Pid}}}'
|
|
203
|
+
const state = object(JSON.parse(run(["inspect", "--format", template, target.targetContainerId], timeoutMs)), "target pause state")
|
|
204
|
+
if (!CONTAINER_ID.test(state.containerId) || typeof state.running !== "boolean" || typeof state.paused !== "boolean"
|
|
205
|
+
|| typeof state.restarting !== "boolean" || typeof state.dead !== "boolean" || !Number.isSafeInteger(state.pid) || state.pid < 0) {
|
|
206
|
+
throw new Error("target pause state is invalid")
|
|
207
|
+
}
|
|
208
|
+
if (state.containerId !== target.targetContainerId || state.pid !== target.targetPid) throw new Error("target pause identity changed")
|
|
209
|
+
return state
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function restorePausedTarget(target, run) {
|
|
213
|
+
let lastError
|
|
214
|
+
for (let attempt = 0; attempt < TARGET_THAW_MAX_ATTEMPTS; attempt += 1) {
|
|
215
|
+
try { run(["unpause", target.targetContainerId]) } catch (error) { lastError = error }
|
|
216
|
+
try {
|
|
217
|
+
const resumed = pausedTargetState(target, run)
|
|
218
|
+
if (resumed.running && !resumed.paused && !resumed.restarting && !resumed.dead) return
|
|
219
|
+
lastError = new Error("target resumed state is invalid")
|
|
220
|
+
} catch (error) {
|
|
221
|
+
lastError = error
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
throw new Error("target did not resume after bounded thaw attempts", { cause: lastError })
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function syncWait(milliseconds) {
|
|
228
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function watchdogReceipt(root, expected, dependencies = {}) {
|
|
232
|
+
const fileExists = dependencies.existsSync ?? existsSync
|
|
233
|
+
const readFile = dependencies.readFileSync ?? readFileSync
|
|
234
|
+
const wait = dependencies.syncWait ?? syncWait
|
|
235
|
+
const file = `${root}/watchdog-terminal.json`
|
|
236
|
+
for (let attempt = 0; attempt < 100 && !fileExists(file); attempt += 1) wait(50)
|
|
237
|
+
const receipt = object(JSON.parse(readFile(file, "utf8")), "thaw watchdog terminal receipt")
|
|
238
|
+
if (receipt.status !== expected.status || receipt.containerId !== expected.containerId || receipt.pid !== expected.pid
|
|
239
|
+
|| receipt.parentBootId !== expected.parentBootId || receipt.parentStarttime !== expected.parentStarttime
|
|
240
|
+
|| expected.watchdogPid !== undefined && (receipt.watchdogPid !== expected.watchdogPid || receipt.watchdogBootId !== expected.watchdogBootId
|
|
241
|
+
|| receipt.watchdogStarttime !== expected.watchdogStarttime)) throw new Error("thaw watchdog terminal receipt is invalid")
|
|
242
|
+
return receipt
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function parseProcStatIdentity(content, expectedPid) {
|
|
246
|
+
if (typeof content !== "string" || content.includes("\0") || !Number.isSafeInteger(expectedPid) || expectedPid <= 0) throw new Error("parent process identity is invalid")
|
|
247
|
+
const prefix = `${expectedPid} (`
|
|
248
|
+
const close = content.lastIndexOf(") ")
|
|
249
|
+
if (!content.startsWith(prefix) || close < prefix.length || close + 4 > content.length) throw new Error("parent process identity is invalid")
|
|
250
|
+
const state = content[close + 2]
|
|
251
|
+
if (!/^[A-Za-z]$/u.test(state) || content[close + 3] !== " ") throw new Error("parent process identity is invalid")
|
|
252
|
+
const fields = content.slice(close + 4).trim().split(/\s+/u)
|
|
253
|
+
const starttime = fields[18]
|
|
254
|
+
if (!starttime || !/^[0-9]+$/u.test(starttime) || starttime === "0") throw new Error("parent process identity is invalid")
|
|
255
|
+
return { state, starttime }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function terminalProcState(state) {
|
|
259
|
+
return state === "Z" || state === "X" || state === "x"
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function monotonicMilliseconds() {
|
|
263
|
+
return Number(process.hrtime.bigint() / 1_000_000n)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function readParentIdentity(pid) {
|
|
267
|
+
const bootId = readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim()
|
|
268
|
+
if (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/u.test(bootId)) throw new Error("parent boot identity is invalid")
|
|
269
|
+
return { bootId, ...parseProcStatIdentity(readFileSync(`/proc/${pid}/stat`, "utf8"), pid) }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function armThawWatchdog(target, dependencies = {}) {
|
|
273
|
+
const now = dependencies.now ?? Date.now
|
|
274
|
+
const readIdentity = dependencies.readParentIdentity ?? readParentIdentity
|
|
275
|
+
const makeDirectory = dependencies.mkdirSync ?? mkdirSync
|
|
276
|
+
const spawnChild = dependencies.spawn ?? spawn
|
|
277
|
+
const fileExists = dependencies.existsSync ?? existsSync
|
|
278
|
+
const wait = dependencies.syncWait ?? syncWait
|
|
279
|
+
const writeFile = dependencies.writeFileSync ?? writeFileSync
|
|
280
|
+
const readFile = dependencies.readFileSync ?? readFileSync
|
|
281
|
+
const parentIdentity = readIdentity(process.pid)
|
|
282
|
+
if (terminalProcState(parentIdentity.state)) throw new Error("thaw watchdog parent is not alive")
|
|
283
|
+
const root = `/run/ouro-thaw-watchdog.${process.pid}.${now()}`
|
|
284
|
+
makeDirectory(root, { mode: 0o700 })
|
|
285
|
+
const child = spawnChild(process.execPath, [fileURLToPath(import.meta.url), "--thaw-watchdog", target.targetContainerId, String(target.targetPid), String(process.pid), parentIdentity.bootId, parentIdentity.starttime, root], {
|
|
286
|
+
cwd: "/", detached: true, stdio: "ignore",
|
|
287
|
+
})
|
|
288
|
+
child.unref()
|
|
289
|
+
for (let attempt = 0; attempt < 100 && !fileExists(`${root}/ready`); attempt += 1) wait(50)
|
|
290
|
+
if (!fileExists(`${root}/ready`)) throw new Error("thaw watchdog did not arm")
|
|
291
|
+
if (!Number.isSafeInteger(child.pid) || child.pid <= 0) {
|
|
292
|
+
if (!dependencies.spawn) throw new Error("thaw watchdog process identity is invalid")
|
|
293
|
+
return { assertLive() {}, disarm() { writeFile(`${root}/disarm`, "\n", { flag: "wx", mode: 0o600 }); return { status: "test-disarmed" } } }
|
|
294
|
+
}
|
|
295
|
+
const ready = object(JSON.parse(readFile(`${root}/ready`, "utf8")), "thaw watchdog ready receipt")
|
|
296
|
+
const watchdogIdentity = { pid: child.pid, bootId: ready.watchdogBootId, starttime: ready.watchdogStarttime }
|
|
297
|
+
if (ready.watchdogPid !== watchdogIdentity.pid || ready.readyAt === undefined || !Number.isSafeInteger(ready.readyAt)
|
|
298
|
+
|| watchdogIdentity.bootId !== parentIdentity.bootId || !/^[0-9]+$/u.test(watchdogIdentity.starttime) || watchdogIdentity.starttime === "0") {
|
|
299
|
+
throw new Error("thaw watchdog ready identity is invalid")
|
|
300
|
+
}
|
|
301
|
+
const assertLive = () => {
|
|
302
|
+
let observed
|
|
303
|
+
try { observed = readIdentity(watchdogIdentity.pid) } catch (error) { throw new Error("thaw watchdog child exited", { cause: error }) }
|
|
304
|
+
if (observed.bootId !== watchdogIdentity.bootId || observed.starttime !== watchdogIdentity.starttime || terminalProcState(observed.state)) throw new Error("thaw watchdog child identity is not alive")
|
|
305
|
+
}
|
|
306
|
+
assertLive()
|
|
307
|
+
return {
|
|
308
|
+
assertLive,
|
|
309
|
+
disarm() {
|
|
310
|
+
assertLive()
|
|
311
|
+
writeFile(`${root}/disarm`, "\n", { flag: "wx", mode: 0o600 })
|
|
312
|
+
return watchdogReceipt(root, { status: "disarmed", containerId: target.targetContainerId, pid: target.targetPid, parentBootId: parentIdentity.bootId, parentStarttime: parentIdentity.starttime, watchdogPid: watchdogIdentity.pid, watchdogBootId: watchdogIdentity.bootId, watchdogStarttime: watchdogIdentity.starttime }, dependencies)
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function runKillableCommand(executable, args, timeoutMs, dependencies = {}) {
|
|
318
|
+
if (typeof executable !== "string" || !executable.startsWith("/") || !Array.isArray(args) || args.some((arg) => typeof arg !== "string")
|
|
319
|
+
|| !Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 20_000) return Promise.reject(new Error("killable command deadline is invalid"))
|
|
320
|
+
const spawnChild = dependencies.spawn ?? spawn
|
|
321
|
+
const killGroup = dependencies.killProcessGroup ?? ((pid, signal) => {
|
|
322
|
+
try { process.kill(-pid, signal) } catch {
|
|
323
|
+
try { process.kill(pid, signal) } catch {}
|
|
324
|
+
}
|
|
325
|
+
})
|
|
326
|
+
return new Promise((resolve, reject) => {
|
|
327
|
+
let settled = false
|
|
328
|
+
let stdout = ""
|
|
329
|
+
const child = spawnChild(executable, args, {
|
|
330
|
+
cwd: "/", detached: true, stdio: ["ignore", "pipe", "ignore"],
|
|
331
|
+
env: { PATH: "/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin" },
|
|
332
|
+
})
|
|
333
|
+
const finish = (error, value) => {
|
|
334
|
+
if (settled) return
|
|
335
|
+
settled = true
|
|
336
|
+
clearTimeout(termTimer)
|
|
337
|
+
clearTimeout(timer)
|
|
338
|
+
if (error) reject(error)
|
|
339
|
+
else resolve(value)
|
|
340
|
+
}
|
|
341
|
+
child.stdout?.on("data", (chunk) => {
|
|
342
|
+
stdout += String(chunk)
|
|
343
|
+
if (Buffer.byteLength(stdout, "utf8") > 1024 * 1024) {
|
|
344
|
+
killGroup(child.pid, "SIGKILL")
|
|
345
|
+
finish(new Error("killable command output exceeded its bound"))
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
child.once("error", () => finish(new Error("killable command failed to launch")))
|
|
349
|
+
child.once("close", (status, signal) => {
|
|
350
|
+
if (status === 0 && signal === null) finish(undefined, stdout)
|
|
351
|
+
else finish(new Error("killable command failed"))
|
|
352
|
+
})
|
|
353
|
+
const graceMs = Math.min(100, Math.max(1, Math.floor(timeoutMs / 4)))
|
|
354
|
+
const termTimer = setTimeout(() => { killGroup(child.pid, "SIGTERM") }, timeoutMs - graceMs)
|
|
355
|
+
const timer = setTimeout(() => {
|
|
356
|
+
killGroup(child.pid, "SIGKILL")
|
|
357
|
+
finish(new Error("killable command hard deadline timed out"))
|
|
358
|
+
}, timeoutMs)
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function runDockerWatchdog(args, timeoutMs) {
|
|
363
|
+
return await runKillableCommand(DOCKER, args, timeoutMs)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function pausedTargetStateAsync(target, run, timeoutMs) {
|
|
367
|
+
const template = '{"containerId":{{json .Id}},"running":{{json .State.Running}},"paused":{{json .State.Paused}},"restarting":{{json .State.Restarting}},"dead":{{json .State.Dead}},"pid":{{json .State.Pid}}}'
|
|
368
|
+
const state = object(JSON.parse(await run(["inspect", "--format", template, target.targetContainerId], timeoutMs)), "target pause state")
|
|
369
|
+
if (!CONTAINER_ID.test(state.containerId) || typeof state.running !== "boolean" || typeof state.paused !== "boolean"
|
|
370
|
+
|| typeof state.restarting !== "boolean" || typeof state.dead !== "boolean" || !Number.isSafeInteger(state.pid) || state.pid < 0) throw new Error("target pause state is invalid")
|
|
371
|
+
if (state.containerId !== target.targetContainerId || state.pid !== target.targetPid) throw new Error("target pause identity changed")
|
|
372
|
+
return state
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function runThawWatchdog(targetContainerId, targetPid, parentPid, parentBootId, parentStarttime, root, dependencies = {}) {
|
|
376
|
+
const target = { targetContainerId, targetPid }
|
|
377
|
+
const wallNow = dependencies.now ?? Date.now
|
|
378
|
+
const monotonicNow = dependencies.monotonicNow ?? monotonicMilliseconds
|
|
379
|
+
const sleep = dependencies.sleep ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)))
|
|
380
|
+
const fileExists = dependencies.existsSync ?? existsSync
|
|
381
|
+
const writeFile = dependencies.writeFileSync ?? writeFileSync
|
|
382
|
+
const readIdentity = dependencies.readParentIdentity ?? readParentIdentity
|
|
383
|
+
const run = dependencies.runDocker ?? runDockerWatchdog
|
|
384
|
+
const enforcementMs = dependencies.enforcementMs ?? WATCHDOG_PARENT_DEATH_ENFORCEMENT_MS
|
|
385
|
+
const recoveryPollMs = dependencies.recoveryPollMs ?? 250
|
|
386
|
+
const rootIdentity = /^\/run\/ouro-thaw-watchdog\.([0-9]+)\.([0-9]+)$/u.exec(root)
|
|
387
|
+
if (!CONTAINER_ID.test(targetContainerId) || !Number.isSafeInteger(targetPid) || targetPid <= 0
|
|
388
|
+
|| !Number.isSafeInteger(parentPid) || parentPid <= 0
|
|
389
|
+
|| !/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/u.test(parentBootId)
|
|
390
|
+
|| !/^[0-9]+$/u.test(parentStarttime) || parentStarttime === "0"
|
|
391
|
+
|| !rootIdentity || Number(rootIdentity[1]) !== parentPid
|
|
392
|
+
|| !Number.isSafeInteger(enforcementMs) || enforcementMs <= 0 || !Number.isSafeInteger(recoveryPollMs) || recoveryPollMs <= 0) throw new Error("thaw watchdog lease is invalid")
|
|
393
|
+
const initial = await pausedTargetStateAsync(target, run, 20_000)
|
|
394
|
+
if (!initial.running || initial.paused || initial.restarting || initial.dead) throw new Error("thaw watchdog initial state is invalid")
|
|
395
|
+
const selfIdentity = dependencies.readSelfIdentity?.() ?? (dependencies.readParentIdentity
|
|
396
|
+
? { bootId: parentBootId, state: "S", starttime: "1" }
|
|
397
|
+
: readParentIdentity(process.pid))
|
|
398
|
+
if (terminalProcState(selfIdentity.state) || selfIdentity.bootId !== parentBootId || !/^[0-9]+$/u.test(selfIdentity.starttime) || selfIdentity.starttime === "0") throw new Error("thaw watchdog process identity is invalid")
|
|
399
|
+
const readyAt = wallNow()
|
|
400
|
+
writeFile(`${root}/ready`, `${JSON.stringify({ watchdogPid: process.pid, watchdogBootId: selfIdentity.bootId, watchdogStarttime: selfIdentity.starttime, readyAt })}\n`, { flag: "wx", mode: 0o600 })
|
|
401
|
+
const terminalReceipt = (status) => writeFile(`${root}/watchdog-terminal.json`, `${JSON.stringify({ status, containerId: targetContainerId, pid: targetPid, parentBootId, parentStarttime, watchdogPid: process.pid, watchdogBootId: selfIdentity.bootId, watchdogStarttime: selfIdentity.starttime })}\n`, { flag: "wx", mode: 0o600 })
|
|
402
|
+
let transientIdentityFailures = 0
|
|
403
|
+
for (;;) {
|
|
404
|
+
if (fileExists(`${root}/disarm`)) {
|
|
405
|
+
const restored = await pausedTargetStateAsync(target, run, 20_000)
|
|
406
|
+
if (!restored.running || restored.paused || restored.restarting || restored.dead) throw new Error("thaw watchdog disarm state is invalid")
|
|
407
|
+
terminalReceipt("disarmed")
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
let parentAlive = false
|
|
411
|
+
try {
|
|
412
|
+
const observed = readIdentity(parentPid)
|
|
413
|
+
parentAlive = observed.bootId === parentBootId && observed.starttime === parentStarttime && !terminalProcState(observed.state)
|
|
414
|
+
transientIdentityFailures = 0
|
|
415
|
+
} catch (error) {
|
|
416
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : undefined
|
|
417
|
+
const confirmedMissing = code === "ENOENT" || code === "ESRCH" || error instanceof Error && /^(?:ENOENT|ESRCH)$/u.test(error.message)
|
|
418
|
+
if (confirmedMissing) break
|
|
419
|
+
transientIdentityFailures += 1
|
|
420
|
+
if (transientIdentityFailures >= 3) throw new Error("thaw watchdog parent identity remained unreadable", { cause: error })
|
|
421
|
+
await sleep(WATCHDOG_POLL_MS)
|
|
422
|
+
continue
|
|
423
|
+
}
|
|
424
|
+
if (!parentAlive) break
|
|
425
|
+
await sleep(WATCHDOG_POLL_MS)
|
|
426
|
+
}
|
|
427
|
+
const deadline = monotonicNow() + enforcementMs
|
|
428
|
+
const remaining = () => {
|
|
429
|
+
const milliseconds = deadline - monotonicNow()
|
|
430
|
+
if (!Number.isSafeInteger(milliseconds) || milliseconds <= 0) throw new Error("thaw watchdog recovery deadline expired")
|
|
431
|
+
return Math.min(20_000, milliseconds)
|
|
432
|
+
}
|
|
433
|
+
for (;;) {
|
|
434
|
+
const state = await pausedTargetStateAsync(target, run, remaining())
|
|
435
|
+
remaining()
|
|
436
|
+
if (!state.running || state.restarting || state.dead) throw new Error("thaw watchdog target state drifted")
|
|
437
|
+
if (state.paused) {
|
|
438
|
+
await run(["unpause", targetContainerId], remaining())
|
|
439
|
+
remaining()
|
|
440
|
+
}
|
|
441
|
+
else if (deadline - monotonicNow() <= recoveryPollMs) {
|
|
442
|
+
remaining()
|
|
443
|
+
terminalReceipt("parent-death-recovered")
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
const sleepMs = Math.min(recoveryPollMs, deadline - monotonicNow())
|
|
447
|
+
if (sleepMs <= 0) throw new Error("thaw watchdog recovery deadline expired")
|
|
448
|
+
await sleep(sleepMs)
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function withPausedTarget(target, operation, dependencies = {}) {
|
|
453
|
+
const run = dependencies.runDocker ?? runDocker
|
|
454
|
+
const armWatchdog = dependencies.armWatchdog ?? (dependencies.runDocker ? () => ({ disarm: () => ({ status: "test-disarmed" }) }) : armThawWatchdog)
|
|
455
|
+
const original = pausedTargetState(target, run)
|
|
456
|
+
if (!original.running || original.paused || original.restarting || original.dead) throw new Error("target original running state is invalid")
|
|
457
|
+
const watchdog = armWatchdog(target)
|
|
458
|
+
watchdog.assertLive?.()
|
|
459
|
+
let result
|
|
460
|
+
let operationError
|
|
461
|
+
let restoreError
|
|
462
|
+
try {
|
|
463
|
+
run(["pause", target.targetContainerId])
|
|
464
|
+
watchdog.assertLive?.()
|
|
465
|
+
const paused = pausedTargetState(target, run)
|
|
466
|
+
if (!paused.running || !paused.paused || paused.restarting || paused.dead) throw new Error("target paused state is invalid")
|
|
467
|
+
watchdog.assertLive?.()
|
|
468
|
+
result = operation()
|
|
469
|
+
watchdog.assertLive?.()
|
|
470
|
+
} catch (error) {
|
|
471
|
+
operationError = error
|
|
472
|
+
} finally {
|
|
473
|
+
try {
|
|
474
|
+
restorePausedTarget(target, run)
|
|
475
|
+
} catch (error) {
|
|
476
|
+
restoreError = error
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (!restoreError) {
|
|
480
|
+
try { watchdog.disarm() } catch (error) { restoreError = error }
|
|
481
|
+
}
|
|
482
|
+
if (restoreError && operationError) throw new AggregateError([operationError, restoreError], "target scan and restoration both failed")
|
|
483
|
+
if (restoreError) throw new Error("target failed to restore its original running state", { cause: restoreError })
|
|
484
|
+
if (operationError) throw operationError
|
|
485
|
+
return result
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function dockerTopology(run = runDocker) {
|
|
489
|
+
const rows = run(["container", "ls", "-a", "--no-trunc", "--format", '{"ID":{{json .ID}},"Names":{{json .Names}}}'])
|
|
490
|
+
return rows.split(/\r?\n/u).filter(Boolean).map((line) => JSON.parse(line))
|
|
491
|
+
.filter((row) => typeof row.Names === "string" && CANONICAL_NAMES.includes(row.Names))
|
|
492
|
+
.map((row) => {
|
|
493
|
+
if (!CONTAINER_ID.test(row.ID)) throw new Error("canonical Butler list identity is invalid")
|
|
494
|
+
return { id: row.ID, name: row.Names }
|
|
495
|
+
})
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function autostartNames() {
|
|
499
|
+
const content = readFileSync("/var/lib/docker/unraid-autostart", "utf8")
|
|
500
|
+
const names = content.split(/\r?\n/u).map((line) => line.trim().split(/\s+/u)[0]).filter((name) => CANONICAL_NAMES.includes(name))
|
|
501
|
+
if (new Set(names).size !== names.length) throw new Error("canonical autostart inventory is ambiguous")
|
|
502
|
+
return new Set(names)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function captureCanonicalRecords(dependencies = {}) {
|
|
506
|
+
const listed = (dependencies.dockerTopology ?? dockerTopology)()
|
|
507
|
+
if (new Set(listed.map(({ name }) => name)).size !== listed.length || new Set(listed.map(({ id }) => id)).size !== listed.length) throw new Error("canonical Butler topology is ambiguous")
|
|
508
|
+
if (listed.length === 0) throw new Error("canonical Butler topology is empty")
|
|
509
|
+
const inspections = dependencies.inspectCanonical
|
|
510
|
+
? dependencies.inspectCanonical(listed.map(({ id }) => id))
|
|
511
|
+
: JSON.parse(runDocker(["inspect", ...listed.map(({ id }) => id)]).trim())
|
|
512
|
+
if (!Array.isArray(inspections) || inspections.length !== listed.length) throw new Error("canonical Butler inspection is incomplete")
|
|
513
|
+
const starts = new Set((dependencies.autostartNames ?? (() => [...autostartNames()]))())
|
|
514
|
+
const graphqlRecords = await (dependencies.graphqlAutostartNames ?? queryGraphqlAutostart)()
|
|
515
|
+
if (!(graphqlRecords instanceof Map)) throw new Error("Unraid GraphQL autostart state is invalid")
|
|
516
|
+
const records = inspections.map((entry) => {
|
|
517
|
+
const name = String(entry.Name).replace(/^\//u, "")
|
|
518
|
+
const listedRecord = listed.find(({ id }) => id === entry.Id)
|
|
519
|
+
if (!listedRecord || listedRecord.name !== name) throw new Error("canonical Butler identity changed during atomic inspection")
|
|
520
|
+
return {
|
|
521
|
+
id: entry.Id,
|
|
522
|
+
names: [entry.Name],
|
|
523
|
+
imageId: entry.Image,
|
|
524
|
+
running: entry.State?.Running,
|
|
525
|
+
autoStart: starts.has(name),
|
|
526
|
+
restartPolicy: entry.HostConfig?.RestartPolicy?.Name,
|
|
527
|
+
pid: entry.State?.Pid,
|
|
528
|
+
networkMode: entry.HostConfig?.NetworkMode,
|
|
529
|
+
}
|
|
530
|
+
})
|
|
531
|
+
const recordNames = new Set(records.map(({ names }) => names[0].replace(/^\//u, "")))
|
|
532
|
+
if (!exactSet(recordNames, new Set(graphqlRecords.keys()))) throw new Error("Unraid GraphQL canonical topology presence disagrees with Docker")
|
|
533
|
+
const graphqlStarts = new Set([...graphqlRecords].filter(([, identity]) => identity?.autoStart === true).map(([name]) => name))
|
|
534
|
+
if (!exactSet(starts, graphqlStarts)) throw new Error("Unraid GraphQL and durable autostart state disagree")
|
|
535
|
+
for (const [name, identity] of graphqlRecords) {
|
|
536
|
+
const record = records.find((candidate) => candidate.names[0].replace(/^\//u, "") === name)
|
|
537
|
+
if (!record || record.id !== identity?.containerId || record.autoStart !== identity?.autoStart) throw new Error("Unraid GraphQL autostart identity disagrees with Docker")
|
|
538
|
+
}
|
|
539
|
+
return records
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function parseProcNet(content, ipv6) {
|
|
543
|
+
return content.split(/\r?\n/u).slice(1).filter(Boolean).map((line) => line.trim().split(/\s+/u)).filter((fields) => fields[3] === "0A").map((fields) => {
|
|
544
|
+
const [address, portHex] = fields[1].split(":")
|
|
545
|
+
const localAddress = ipv6 ? address : [6, 4, 2, 0].map((offset) => Number.parseInt(address.slice(offset, offset + 2), 16)).join(".")
|
|
546
|
+
return { inode: fields[9], localAddress, port: Number.parseInt(portHex, 16) }
|
|
547
|
+
})
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function parseProcUdp(content, ipv6) {
|
|
551
|
+
return content.split(/\r?\n/u).slice(1).filter(Boolean).map((line) => line.trim().split(/\s+/u)).map((fields) => {
|
|
552
|
+
const [address, portHex] = fields[1].split(":")
|
|
553
|
+
const localAddress = ipv6 ? address : [6, 4, 2, 0].map((offset) => Number.parseInt(address.slice(offset, offset + 2), 16)).join(".")
|
|
554
|
+
return { inode: fields[9], localAddress, port: Number.parseInt(portHex, 16) }
|
|
555
|
+
}).filter(({ port }) => port !== 0)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function cgroupProcessIds(rootPid, containerId, dependencies = {}) {
|
|
559
|
+
if (!Number.isSafeInteger(rootPid) || rootPid <= 0 || !CONTAINER_ID.test(containerId)) throw new Error("target cgroup identity is invalid")
|
|
560
|
+
const readCgroup = dependencies.readCgroup ?? ((pid) => readFileSync(`/proc/${pid}/cgroup`, "utf8"))
|
|
561
|
+
const readCgroupMembership = dependencies.readCgroupMembership ?? ((file) => readFileSync(file, "utf8"))
|
|
562
|
+
const lines = readCgroup(rootPid).split(/\r?\n/u).filter(Boolean)
|
|
563
|
+
const expectedPath = `/docker/${containerId}`
|
|
564
|
+
if (lines.length !== 1 || lines[0] !== `0::${expectedPath}`) throw new Error("target cgroup is not the exact Docker cgroup-v2 identity")
|
|
565
|
+
const membership = (file, label) => {
|
|
566
|
+
const raw = readCgroupMembership(`/sys/fs/cgroup${expectedPath}/${file}`).trim()
|
|
567
|
+
const ids = raw ? raw.split(/\s+/u).map(Number) : []
|
|
568
|
+
if (ids.length === 0 || ids.some((pid) => !Number.isSafeInteger(pid) || pid <= 0) || new Set(ids).size !== ids.length) throw new Error(`target cgroup ${label} membership is invalid`)
|
|
569
|
+
return ids.sort((left, right) => left - right)
|
|
570
|
+
}
|
|
571
|
+
const processIds = membership("cgroup.procs", "process")
|
|
572
|
+
const threadIds = membership("cgroup.threads", "thread")
|
|
573
|
+
if (!processIds.includes(rootPid)) throw new Error("target root PID is absent from its cgroup")
|
|
574
|
+
if (!processIds.every((pid) => threadIds.includes(pid))) throw new Error("target cgroup thread membership is incomplete")
|
|
575
|
+
return { path: expectedPath, processIds, threadIds }
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function ownedSocketInodes(threadIds, dependencies = {}) {
|
|
579
|
+
const listFileDescriptors = dependencies.listFileDescriptors ?? ((tid) => readdirSync(`/proc/${tid}/fd`))
|
|
580
|
+
const readDescriptorLink = dependencies.readDescriptorLink ?? ((tid, fd) => readlinkSync(`/proc/${tid}/fd/${fd}`))
|
|
581
|
+
const inodes = []
|
|
582
|
+
for (const tid of threadIds) for (const fd of listFileDescriptors(tid)) {
|
|
583
|
+
const match = /^socket:\[([0-9]+)\]$/u.exec(readDescriptorLink(tid, fd))
|
|
584
|
+
if (match) inodes.push(match[1])
|
|
585
|
+
}
|
|
586
|
+
return inodes
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
function parseProcUnix(content) {
|
|
591
|
+
return content.split(/\r?\n/u).slice(1).filter(Boolean).map((line) => line.trim().split(/\s+/u))
|
|
592
|
+
.filter((fields) => /^[0-9]+$/u.test(fields[6] ?? ""))
|
|
593
|
+
.map((fields) => ({ inode: fields[6], path: fields.slice(7).join(" "), flags: fields[3], type: fields[4], state: fields[5] }))
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function sortedUnique(values) {
|
|
597
|
+
return [...new Set(values)].sort((left, right) => String(left).localeCompare(String(right)))
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function sortedRecords(values) {
|
|
601
|
+
return [...values].sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)))
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function sameMembership(left, right) {
|
|
605
|
+
return left.path === right.path
|
|
606
|
+
&& exactSet(new Set(left.processIds), new Set(right.processIds))
|
|
607
|
+
&& exactSet(new Set(left.threadIds), new Set(right.threadIds))
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function completeContainmentSnapshot(provisional, readers) {
|
|
611
|
+
const netnsBefore = readers.readNetns(provisional.targetPid)
|
|
612
|
+
const membershipBefore = readers.readMembership(provisional.targetPid, provisional.targetContainerId)
|
|
613
|
+
const socketInodesBefore = sortedUnique(readers.readSockets(membershipBefore.threadIds))
|
|
614
|
+
const tcpListenersRaw = readers.readTcp(provisional.targetPid)
|
|
615
|
+
const udpListenersRaw = readers.readUdp(provisional.targetPid)
|
|
616
|
+
const unixSocketsRaw = readers.readUnix(provisional.targetPid)
|
|
617
|
+
const membershipAfter = readers.readMembership(provisional.targetPid, provisional.targetContainerId)
|
|
618
|
+
const socketInodesAfter = sortedUnique(readers.readSockets(membershipAfter.threadIds))
|
|
619
|
+
const netnsAfter = readers.readNetns(provisional.targetPid)
|
|
620
|
+
if (netnsBefore !== netnsAfter || !sameMembership(membershipBefore, membershipAfter)
|
|
621
|
+
|| !exactSet(new Set(socketInodesBefore), new Set(socketInodesAfter))) return null
|
|
622
|
+
const owned = new Set(socketInodesAfter)
|
|
623
|
+
return {
|
|
624
|
+
netns: netnsAfter,
|
|
625
|
+
membership: membershipAfter,
|
|
626
|
+
socketInodes: socketInodesAfter,
|
|
627
|
+
tcpListeners: sortedRecords(tcpListenersRaw.filter((listener) => owned.has(listener.inode))),
|
|
628
|
+
udpListeners: sortedRecords(udpListenersRaw.filter((listener) => owned.has(listener.inode))),
|
|
629
|
+
unixSockets: sortedRecords(unixSocketsRaw.filter((socket) => owned.has(socket.inode))),
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function convergedTerminalContainment(provisional, readers) {
|
|
634
|
+
let previous = null
|
|
635
|
+
for (let attempt = 0; attempt < TERMINAL_CONTAINMENT_MAX_SAMPLES; attempt += 1) {
|
|
636
|
+
const current = completeContainmentSnapshot(provisional, readers)
|
|
637
|
+
if (current && previous && JSON.stringify(current) === JSON.stringify(previous)) return current
|
|
638
|
+
previous = current
|
|
639
|
+
}
|
|
640
|
+
throw new Error("terminal containment snapshot did not converge")
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
async function runDeploymentTargetAudit(profileName, expectedImageId, dependencies = {}) {
|
|
644
|
+
const capture = dependencies.captureCanonicalRecords ?? captureCanonicalRecords
|
|
645
|
+
const before = await capture()
|
|
646
|
+
const provisional = attestDeploymentTarget({ profile: profileName, expectedImageId, topologyBefore: before, inspected: before, topologyAfter: before })
|
|
647
|
+
const readNetns = dependencies.readNetns ?? ((pid) => readlinkSync(`/proc/${pid}/ns/net`))
|
|
648
|
+
const readMembership = dependencies.cgroupProcessIds ?? cgroupProcessIds
|
|
649
|
+
const readSockets = dependencies.ownedSocketInodes ?? ownedSocketInodes
|
|
650
|
+
const readTcp = dependencies.readTcpListeners ?? ((pid) => [...parseProcNet(readFileSync(`/proc/${pid}/net/tcp`, "utf8"), false), ...parseProcNet(readFileSync(`/proc/${pid}/net/tcp6`, "utf8"), true)])
|
|
651
|
+
const readUdp = dependencies.readUdpListeners ?? ((pid) => [...parseProcUdp(readFileSync(`/proc/${pid}/net/udp`, "utf8"), false), ...parseProcUdp(readFileSync(`/proc/${pid}/net/udp6`, "utf8"), true)])
|
|
652
|
+
const readUnix = dependencies.readUnixSockets ?? ((pid) => parseProcUnix(readFileSync(`/proc/${pid}/net/unix`, "utf8")))
|
|
653
|
+
const quiesceTarget = dependencies.quiesceTarget ?? withPausedTarget
|
|
654
|
+
const containment = quiesceTarget(provisional, () => {
|
|
655
|
+
const netnsBefore = readNetns(provisional.targetPid)
|
|
656
|
+
const membershipBefore = readMembership(provisional.targetPid, provisional.targetContainerId)
|
|
657
|
+
const processIdsBefore = membershipBefore.processIds
|
|
658
|
+
const socketInodesBefore = readSockets(membershipBefore.threadIds)
|
|
659
|
+
const tcpListenersBefore = readTcp(provisional.targetPid)
|
|
660
|
+
const udpListenersBefore = readUdp(provisional.targetPid)
|
|
661
|
+
const unixSocketsBefore = readUnix(provisional.targetPid)
|
|
662
|
+
const membershipAfter = readMembership(provisional.targetPid, provisional.targetContainerId)
|
|
663
|
+
if (membershipBefore.path !== membershipAfter.path) throw new Error("target cgroup changed")
|
|
664
|
+
if (JSON.stringify(processIdsBefore) !== JSON.stringify(membershipAfter.processIds)) throw new Error("target cgroup process membership changed")
|
|
665
|
+
if (JSON.stringify(membershipBefore.threadIds) !== JSON.stringify(membershipAfter.threadIds)) throw new Error("target cgroup thread membership changed")
|
|
666
|
+
const processIdsAfter = membershipAfter.processIds
|
|
667
|
+
const socketInodesAfter = readSockets(membershipAfter.threadIds)
|
|
668
|
+
const tcpListenersAfter = readTcp(provisional.targetPid)
|
|
669
|
+
const udpListenersAfter = readUdp(provisional.targetPid)
|
|
670
|
+
const unixSocketsAfter = readUnix(provisional.targetPid)
|
|
671
|
+
const netnsAfter = readNetns(provisional.targetPid)
|
|
672
|
+
const terminal = convergedTerminalContainment(provisional, { readNetns, readMembership, readSockets, readTcp, readUdp, readUnix })
|
|
673
|
+
return { membershipBefore, processIdsBefore, socketInodesBefore, tcpListenersBefore, udpListenersBefore, unixSocketsBefore, membershipAfter, processIdsAfter, socketInodesAfter, tcpListenersAfter, udpListenersAfter, unixSocketsAfter, netnsBefore, netnsAfter, terminal }
|
|
674
|
+
})
|
|
675
|
+
const after = await capture()
|
|
676
|
+
const { membershipBefore, processIdsBefore, socketInodesBefore, tcpListenersBefore, udpListenersBefore, unixSocketsBefore, membershipAfter, processIdsAfter, socketInodesAfter, tcpListenersAfter, udpListenersAfter, unixSocketsAfter, netnsBefore, netnsAfter, terminal } = containment
|
|
677
|
+
const membershipTerminal = terminal.membership
|
|
678
|
+
const socketInodesTerminal = terminal.socketInodes
|
|
679
|
+
const tcpListenersTerminal = terminal.tcpListeners
|
|
680
|
+
const udpListenersTerminal = terminal.udpListeners
|
|
681
|
+
const unixSocketsTerminal = terminal.unixSockets
|
|
682
|
+
const sameIds = (left, right) => exactSet(new Set(left), new Set(right))
|
|
683
|
+
if (netnsBefore !== netnsAfter || netnsAfter !== terminal.netns) throw new Error("target network namespace changed")
|
|
684
|
+
if (membershipBefore.path !== membershipTerminal.path || membershipAfter.path !== membershipTerminal.path) throw new Error("target cgroup changed")
|
|
685
|
+
if (!sameIds(processIdsBefore, membershipTerminal.processIds) || !sameIds(processIdsAfter, membershipTerminal.processIds)) throw new Error("target cgroup process membership changed")
|
|
686
|
+
if (!sameIds(membershipBefore.threadIds, membershipTerminal.threadIds) || !sameIds(membershipAfter.threadIds, membershipTerminal.threadIds)) throw new Error("target cgroup thread membership changed")
|
|
687
|
+
if (!exactSet(new Set(socketInodesBefore), new Set(socketInodesTerminal)) || !exactSet(new Set(socketInodesAfter), new Set(socketInodesTerminal))) throw new Error("target socket ownership changed")
|
|
688
|
+
const deployment = attestDeploymentTarget({ profile: profileName, expectedImageId, topologyBefore: before, inspected: before, topologyAfter: after })
|
|
689
|
+
const listeners = { ...attestOwnedListeners({ rootPid: deployment.targetPid, netnsBefore, netnsAfter, processIdsBefore, processIdsAfter, socketInodesBefore, socketInodesAfter, socketInodesTerminal, tcpListenersBefore, tcpListenersAfter, tcpListenersTerminal, udpListenersBefore, udpListenersAfter, udpListenersTerminal, unixSocketsBefore, unixSocketsAfter, unixSocketsTerminal }), cgroupPath: membershipTerminal.path, threadCount: membershipTerminal.threadIds.length }
|
|
690
|
+
return { schemaVersion: "sanctuary-effective-deployment-v1", deployment, listeners }
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (process.argv[1]?.endsWith("sanctuary-deployment-target.mjs") && process.argv[2] === "--thaw-watchdog") {
|
|
694
|
+
const [targetContainerId, rawTargetPid, rawParentPid, parentBootId, parentStarttime, root] = process.argv.slice(3)
|
|
695
|
+
try { await runThawWatchdog(targetContainerId, Number(rawTargetPid), Number(rawParentPid), parentBootId, parentStarttime, root) } catch (error) {
|
|
696
|
+
if (/^\/run\/ouro-thaw-watchdog\.[0-9]+\.[0-9]+$/u.test(root ?? "") && !existsSync(`${root}/watchdog-terminal.json`)) {
|
|
697
|
+
try { writeFileSync(`${root}/watchdog-terminal.json`, `${JSON.stringify({ status: "failed", containerId: targetContainerId, pid: Number(rawTargetPid), parentBootId, parentStarttime, error: error instanceof Error ? error.message.slice(0, 240) : "unknown" })}\n`, { flag: "wx", mode: 0o600 }) } catch {}
|
|
698
|
+
}
|
|
699
|
+
process.exitCode = 1
|
|
700
|
+
}
|
|
701
|
+
} else if (process.argv[1]?.endsWith("sanctuary-deployment-target.mjs")) {
|
|
702
|
+
const [profile, expectedImageId] = process.argv.slice(2)
|
|
703
|
+
if (process.argv.length !== 4) process.exitCode = 2
|
|
704
|
+
else {
|
|
705
|
+
try { process.stdout.write(`${JSON.stringify(await runDeploymentTargetAudit(profile, expectedImageId))}\n`) }
|
|
706
|
+
catch { process.exitCode = 1 }
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
export { attestDeploymentTarget, attestOwnedListeners, armThawWatchdog, captureCanonicalRecords, cgroupProcessIds, dockerTopology, ownedSocketInodes, parseProcNet, parseProcStatIdentity, parseProcUdp, parseProcUnix, queryGraphqlAutostart, runDeploymentTargetAudit, runKillableCommand, runThawWatchdog, targetProfile, withPausedTarget }
|