@ouro.bot/cli 0.1.0-alpha.80 → 0.1.0-alpha.800
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +154 -23
- package/RepairGuide.ouro/agent.json +5 -0
- package/RepairGuide.ouro/psyche/IDENTITY.md +19 -0
- package/RepairGuide.ouro/psyche/SOUL.md +55 -0
- package/RepairGuide.ouro/skills/diagnose-broken-remote.md +63 -0
- package/RepairGuide.ouro/skills/diagnose-stacked-typed-issues.md +35 -0
- package/RepairGuide.ouro/skills/diagnose-sync-blocked.md +54 -0
- package/RepairGuide.ouro/skills/diagnose-vault-expired.md +60 -0
- package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/agent.json +4 -2
- package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/SOUL.md +2 -2
- package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-serpent.md +1 -1
- package/assets/bluebubbles-host +264 -0
- package/changelog.json +5117 -16
- package/deploy/unraid/Dockerfile +48 -0
- package/deploy/unraid/README.txt +3012 -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 +526 -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-deployment-target.mjs +709 -0
- package/deploy/unraid/sanctuary-safe-rename.py +24 -0
- package/deploy/unraid/sanctuary-unit16-host-broker.mjs +2030 -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 +24 -0
- package/deploy/unraid/sanctuary.ouro/arc/README.md +3 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +5 -0
- package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +8 -0
- package/deploy/unraid/sanctuary.ouro/provider-readiness.json +11 -0
- package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +5 -0
- package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +5 -0
- package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +11 -0
- package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +25 -0
- package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +11 -0
- package/deploy/unraid/sanctuary.ouro/state/policy/steward.json +7 -0
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +23 -0
- package/deploy/unraid/sanctuary.xml +27 -0
- package/dist/a2a/card.js +57 -0
- package/dist/a2a/client.js +180 -0
- package/dist/a2a/config.js +50 -0
- package/dist/a2a/delegation-stores.js +56 -0
- package/dist/a2a/did-resolution.js +93 -0
- package/dist/a2a/identity.js +170 -0
- package/dist/a2a/inbound-share.js +107 -0
- package/dist/a2a/mission-result-wire.js +196 -0
- package/dist/a2a/onboarding.js +115 -0
- package/dist/a2a/pin-store.js +132 -0
- package/dist/a2a/seen-ledger.js +120 -0
- package/dist/a2a/server.js +604 -0
- package/dist/a2a/task-store.js +69 -0
- package/dist/a2a/transport.js +45 -0
- package/dist/a2a/types.js +3 -0
- package/dist/arc/attention-types.js +8 -0
- package/dist/arc/cares.js +360 -0
- package/dist/arc/episodes.js +118 -0
- package/dist/arc/evolution.js +490 -0
- package/dist/arc/flight-recorder.js +1034 -0
- package/dist/arc/intentions.js +134 -0
- package/dist/arc/json-store.js +117 -0
- package/dist/arc/obligations.js +386 -0
- package/dist/arc/packets.js +336 -0
- package/dist/arc/presence.js +185 -0
- package/dist/arc/task-lifecycle.js +57 -0
- package/dist/commerce/store.js +755 -0
- package/dist/commerce/types.js +3 -0
- package/dist/heart/active-work.js +820 -29
- package/dist/heart/agent-entry.js +252 -3
- package/dist/heart/approval-files.js +113 -0
- package/dist/heart/approval-store.js +950 -0
- package/dist/heart/attachments/image-normalize.js +194 -0
- package/dist/heart/attachments/materialize.js +97 -0
- package/dist/heart/attachments/originals.js +88 -0
- package/dist/heart/attachments/render.js +29 -0
- package/dist/heart/attachments/sources/bluebubbles.js +156 -0
- package/dist/heart/attachments/sources/cli-local-file.js +78 -0
- package/dist/heart/attachments/sources/index.js +18 -0
- package/dist/heart/attachments/sources/telegram.js +71 -0
- package/dist/heart/attachments/store.js +132 -0
- package/dist/heart/attachments/types.js +93 -0
- package/dist/heart/auth/auth-flow.js +495 -0
- package/dist/heart/autonomy-budget.js +478 -0
- package/dist/heart/awaiting/await-alert.js +148 -0
- package/dist/heart/awaiting/await-expiry.js +143 -0
- package/dist/heart/awaiting/await-loader.js +91 -0
- package/dist/heart/awaiting/await-parser.js +149 -0
- package/dist/heart/awaiting/await-runtime-state.js +100 -0
- package/dist/heart/awaiting/await-scheduler.js +382 -0
- package/dist/heart/background-operations.js +281 -0
- package/dist/heart/bridges/manager.js +137 -17
- package/dist/heart/bridges/store.js +14 -2
- package/dist/heart/bundle-state.js +168 -0
- package/dist/heart/commitments.js +64 -18
- package/dist/heart/config-registry.js +331 -0
- package/dist/heart/config.js +142 -129
- package/dist/heart/context-loss-gauntlet.js +387 -0
- package/dist/heart/context-loss-sentinel.js +1157 -0
- package/dist/heart/core.js +2132 -572
- 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 +836 -0
- package/dist/heart/daemon/cli-desk.js +322 -0
- package/dist/heart/daemon/cli-exec.js +8730 -0
- package/dist/heart/daemon/cli-help.js +710 -0
- package/dist/heart/daemon/cli-parse.js +2719 -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 +170 -0
- package/dist/heart/daemon/container-spec-auditor.js +236 -0
- package/dist/heart/daemon/daemon-bootstrap-startup.js +107 -0
- package/dist/heart/daemon/daemon-cli.js +29 -2284
- package/dist/heart/daemon/daemon-entry.js +920 -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 +2010 -88
- package/dist/heart/daemon/dns-workflow.js +394 -0
- package/dist/heart/daemon/doctor-types.js +8 -0
- package/dist/heart/daemon/doctor.js +1755 -0
- package/dist/heart/daemon/freshness.js +345 -0
- package/dist/heart/daemon/habit-private-wake.js +61 -0
- package/dist/heart/daemon/health-monitor.js +142 -11
- package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
- package/dist/heart/daemon/hooks/bundle-meta.js +135 -1
- package/dist/heart/daemon/http-health-probe.js +80 -0
- package/dist/heart/daemon/human-command-screens.js +234 -0
- package/dist/heart/daemon/human-readiness.js +114 -0
- package/dist/heart/daemon/inner-status.js +78 -0
- package/dist/heart/daemon/interactive-repair.js +394 -0
- package/dist/heart/daemon/launchd.js +37 -8
- package/dist/heart/daemon/log-tailer.js +79 -10
- package/dist/heart/daemon/logs-prune.js +132 -0
- package/dist/heart/daemon/mcp-canary.js +524 -0
- package/dist/heart/daemon/message-router.js +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 +2609 -0
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +1866 -0
- package/dist/heart/daemon/sanctuary-acceptance-marker.js +267 -0
- package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +976 -0
- package/dist/heart/daemon/sanctuary-bundle-migration.js +728 -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/sense-manager.js +689 -48
- package/dist/heart/daemon/session-id-resolver.js +131 -0
- package/dist/heart/daemon/skill-management-installer.js +1 -1
- package/dist/heart/daemon/socket-client.js +180 -12
- package/dist/heart/daemon/stale-bundle-prune.js +113 -0
- package/dist/heart/daemon/startup-tui.js +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 +1414 -0
- package/dist/heart/habits/habit-cancel.js +810 -0
- package/dist/heart/habits/habit-lifecycle.js +1327 -0
- package/dist/heart/habits/habit-migration.js +194 -0
- package/dist/heart/habits/habit-parser.js +278 -0
- package/dist/heart/habits/habit-runtime-state.js +93 -0
- package/dist/heart/habits/habit-scheduler.js +653 -0
- package/dist/heart/habits/habit-session-summary.js +318 -0
- package/dist/heart/habits/habit-session.js +636 -0
- package/dist/heart/{daemon → hatch}/hatch-flow.js +55 -62
- package/dist/heart/{daemon → hatch}/hatch-specialist.js +6 -8
- package/dist/heart/{daemon → hatch}/specialist-prompt.js +12 -9
- package/dist/heart/{daemon → hatch}/specialist-tools.js +53 -18
- package/dist/heart/identity.js +184 -59
- package/dist/heart/kept-notes.js +289 -0
- package/dist/heart/kicks.js +1 -1
- package/dist/heart/machine-identity.js +161 -0
- package/dist/heart/mail-import-discovery.js +353 -0
- package/dist/heart/mailbox/mailbox-http-hooks.js +98 -0
- package/dist/heart/mailbox/mailbox-http-response.js +7 -0
- package/dist/heart/mailbox/mailbox-http-routes.js +398 -0
- package/dist/heart/mailbox/mailbox-http-static.js +103 -0
- package/dist/heart/mailbox/mailbox-http-transport.js +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 +232 -0
- package/dist/heart/mailbox/readers/shared.js +111 -0
- package/dist/heart/mcp/mcp-server.js +696 -0
- package/dist/heart/migrate-config.js +100 -0
- package/dist/heart/model-capabilities.js +11 -0
- package/dist/heart/orientation-frame.js +345 -0
- package/dist/heart/platform.js +81 -0
- package/dist/heart/primitives.js +1 -1
- package/dist/heart/private-runtime/decision-renderer.js +158 -0
- package/dist/heart/private-runtime/index.js +18 -0
- package/dist/heart/private-runtime/ledger.js +221 -0
- package/dist/heart/private-runtime/policy.js +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 +90 -24
- package/dist/heart/session-events.js +1318 -0
- package/dist/heart/session-playback-cli-main.js +5 -0
- package/dist/heart/session-playback-cli.js +36 -0
- package/dist/heart/session-playback.js +231 -0
- package/dist/heart/session-stats-cli-main.js +5 -0
- package/dist/heart/session-stats.js +182 -0
- package/dist/heart/session-transcript.js +133 -0
- package/dist/heart/start-of-turn-packet.js +372 -0
- package/dist/heart/steward-policy.js +302 -0
- package/dist/heart/streaming.js +604 -207
- package/dist/heart/structured-output.js +196 -0
- package/dist/heart/sync-classification.js +176 -0
- package/dist/heart/sync.js +449 -0
- package/dist/heart/target-resolution.js +11 -7
- package/dist/heart/tempo.js +93 -0
- package/dist/heart/temporal-view.js +41 -0
- package/dist/heart/timeouts.js +101 -0
- package/dist/heart/tool-activity-callbacks.js +59 -0
- package/dist/heart/tool-approval.js +342 -0
- package/dist/heart/tool-description.js +155 -0
- package/dist/heart/tool-friction.js +55 -0
- package/dist/heart/tool-loop.js +200 -0
- package/dist/heart/turn-context.js +481 -0
- package/dist/heart/{daemon → versioning}/ouro-bot-global-installer.js +6 -5
- package/dist/heart/{daemon → versioning}/ouro-bot-wrapper.js +1 -1
- package/dist/heart/versioning/ouro-path-installer.js +432 -0
- package/dist/heart/versioning/ouro-recovery-launcher.js +76 -0
- package/dist/heart/versioning/ouro-version-manager.js +409 -0
- package/dist/heart/{daemon → versioning}/staged-restart.js +40 -8
- package/dist/heart/{daemon → versioning}/update-checker.js +6 -1
- package/dist/heart/versioning/update-hooks.js +154 -0
- package/dist/heart/versioning/version-intent.js +114 -0
- package/dist/heart/work-card.js +380 -0
- package/dist/mailbox-ui/assets/index-Du_9G9WO.css +1 -0
- package/dist/mailbox-ui/assets/index-J8zDwHmE.js +1 -0
- package/dist/mailbox-ui/assets/vendor-CcN1XpQ9.js +61 -0
- package/dist/mailbox-ui/index.html +16 -0
- package/dist/mailroom/attention.js +167 -0
- package/dist/mailroom/autonomy.js +209 -0
- package/dist/mailroom/blob-store.js +889 -0
- package/dist/mailroom/body-cache.js +61 -0
- package/dist/mailroom/cache-sync-cli.js +58 -0
- package/dist/mailroom/core.js +803 -0
- package/dist/mailroom/entry.js +160 -0
- package/dist/mailroom/file-store.js +568 -0
- package/dist/mailroom/hosted-cache-sync.js +384 -0
- package/dist/mailroom/mbox-import.js +393 -0
- package/dist/mailroom/migration.js +164 -0
- package/dist/mailroom/outbound.js +380 -0
- package/dist/mailroom/policy.js +263 -0
- package/dist/mailroom/reader.js +292 -0
- package/dist/mailroom/search-cache.js +513 -0
- package/dist/mailroom/search-relevance.js +319 -0
- package/dist/mailroom/smtp-ingress.js +176 -0
- package/dist/mailroom/source-state.js +176 -0
- package/dist/mailroom/thread.js +109 -0
- package/dist/mailroom/travel-extract.js +89 -0
- package/dist/mind/bundle-manifest.js +24 -2
- package/dist/mind/context.js +278 -111
- package/dist/mind/desk-section.js +362 -0
- package/dist/mind/diary-integrity.js +60 -0
- package/dist/mind/{memory.js → diary.js} +68 -77
- package/dist/mind/embedding-provider.js +60 -0
- package/dist/mind/file-state.js +179 -0
- package/dist/mind/{associative-recall.js → note-search.js} +47 -58
- package/dist/mind/obligation-steering.js +227 -0
- package/dist/mind/pending.js +34 -8
- package/dist/mind/prompt-budget.js +479 -0
- package/dist/mind/prompt-refresh.js +3 -2
- package/dist/mind/prompt.js +1056 -187
- package/dist/mind/provenance-trust.js +26 -0
- package/dist/mind/record-paths.js +312 -0
- package/dist/mind/scrutiny.js +173 -0
- package/dist/mind/session-transaction.js +415 -0
- package/dist/nerves/cli-logging.js +7 -1
- package/dist/nerves/coverage/audit-rules.js +15 -6
- package/dist/nerves/coverage/audit.js +28 -2
- package/dist/nerves/coverage/cli.js +1 -1
- package/dist/nerves/coverage/contract.js +5 -5
- package/dist/nerves/coverage/file-completeness.js +139 -5
- package/dist/nerves/coverage/run-artifacts.js +55 -35
- package/dist/nerves/coverage/source-scanner.js +4 -4
- package/dist/nerves/event-buffer.js +111 -0
- package/dist/nerves/index.js +293 -9
- package/dist/nerves/observation.js +20 -0
- package/dist/nerves/redact.js +79 -0
- package/dist/nerves/review/cli-main.js +5 -0
- package/dist/nerves/review/cli.js +156 -0
- package/dist/nerves/review/core.js +159 -0
- package/dist/nerves/runtime.js +17 -1
- package/dist/repertoire/ado-client.js +15 -56
- package/dist/repertoire/ado-semantic.js +16 -10
- package/dist/repertoire/api-client.js +97 -0
- package/dist/repertoire/bitwarden-store.js +1045 -0
- package/dist/repertoire/bundle-templates.js +71 -0
- package/dist/repertoire/bw-installer.js +180 -0
- package/dist/repertoire/coding/codex-jsonl.js +64 -0
- package/dist/repertoire/coding/context-pack.js +331 -0
- package/dist/repertoire/coding/feedback.js +218 -30
- package/dist/repertoire/coding/index.js +15 -3
- package/dist/repertoire/coding/manager.js +166 -10
- package/dist/repertoire/coding/spawner.js +58 -11
- package/dist/repertoire/coding/tools.js +242 -14
- package/dist/repertoire/coding/workbench-client.js +272 -0
- package/dist/repertoire/coding/workbench-manager.js +346 -0
- package/dist/repertoire/commerce-errors.js +109 -0
- package/dist/repertoire/commerce-self-test.js +156 -0
- package/dist/repertoire/credential-access.js +178 -0
- package/dist/repertoire/desk/classifier.js +362 -0
- package/dist/repertoire/duffel-client.js +185 -0
- package/dist/repertoire/github-client.js +14 -55
- package/dist/repertoire/graph-client.js +11 -52
- package/dist/repertoire/guardrails.js +811 -18
- package/dist/repertoire/mcp-client.js +44 -3
- package/dist/repertoire/mcp-manager.js +259 -20
- package/dist/repertoire/mcp-tools.js +83 -0
- package/dist/repertoire/plugin-mcp.js +175 -0
- package/dist/repertoire/plugins.js +253 -0
- package/dist/repertoire/relationship-authorization.js +194 -0
- package/dist/repertoire/shell-sessions.js +133 -0
- package/dist/repertoire/skills.js +48 -4
- package/dist/repertoire/stripe-client.js +131 -0
- package/dist/repertoire/tool-arguments.js +148 -0
- package/dist/repertoire/tool-results.js +29 -0
- package/dist/repertoire/tools-a2a.js +699 -0
- package/dist/repertoire/tools-attachments.js +322 -0
- package/dist/repertoire/tools-awaiting.js +575 -0
- package/dist/repertoire/tools-base.js +82 -1139
- package/dist/repertoire/tools-bluebubbles.js +2 -0
- package/dist/repertoire/tools-bridge.js +144 -0
- package/dist/repertoire/tools-bundle.js +993 -0
- package/dist/repertoire/tools-commerce.js +253 -0
- package/dist/repertoire/tools-config.js +186 -0
- package/dist/repertoire/tools-continuity.js +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-session.js +1108 -0
- package/dist/repertoire/tools-shell.js +133 -0
- package/dist/repertoire/tools-steward-policy.js +95 -0
- package/dist/repertoire/tools-stripe.js +224 -0
- package/dist/repertoire/tools-surface.js +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 +484 -0
- package/dist/repertoire/tools-user-profile.js +146 -0
- package/dist/repertoire/tools-vault.js +40 -0
- package/dist/repertoire/tools-voice.js +145 -0
- package/dist/repertoire/tools.js +377 -80
- package/dist/repertoire/travel-api-client.js +360 -0
- package/dist/repertoire/unraid-client.js +155 -0
- package/dist/repertoire/unraid-restart.js +212 -0
- package/dist/repertoire/user-profile.js +131 -0
- package/dist/repertoire/vault-setup.js +246 -0
- package/dist/repertoire/vault-unlock.js +628 -0
- package/dist/rsvp/aisleplanner-client.js +354 -0
- package/dist/rsvp/cli.js +926 -0
- package/dist/rsvp/config.js +327 -0
- package/dist/rsvp/cutover.js +433 -0
- package/dist/rsvp/diagnostics.js +232 -0
- package/dist/rsvp/diff-renderer.js +176 -0
- package/dist/rsvp/habit-policy.js +110 -0
- package/dist/rsvp/habit-stage.js +123 -0
- package/dist/rsvp/incident-bundle.js +97 -0
- package/dist/rsvp/migration.js +204 -0
- package/dist/rsvp/native-habit-runner.js +630 -0
- package/dist/rsvp/outbound-state.js +628 -0
- package/dist/rsvp/query.js +118 -0
- package/dist/rsvp/replay.js +233 -0
- package/dist/rsvp/snapshot.js +232 -0
- package/dist/rsvp/spend-ledger.js +175 -0
- package/dist/scripts/claude-code-hook.js +41 -0
- package/dist/scripts/claude-code-stop-hook.js +47 -0
- package/dist/senses/a2a-entry.js +115 -0
- package/dist/senses/attention-queue.js +186 -0
- package/dist/senses/await-turn-message.js +58 -0
- package/dist/senses/bluebubbles/active-turns.js +216 -0
- package/dist/senses/bluebubbles/attachment-cache.js +53 -0
- package/dist/senses/bluebubbles/attachment-download.js +137 -0
- package/dist/senses/{bluebubbles-client.js → bluebubbles/client.js} +355 -40
- package/dist/senses/bluebubbles/context-packet.js +207 -0
- package/dist/senses/bluebubbles/context-smoke.js +144 -0
- package/dist/senses/bluebubbles/entry.js +90 -0
- package/dist/senses/{bluebubbles-inbound-log.js → bluebubbles/inbound-log.js} +21 -3
- package/dist/senses/bluebubbles/index.js +3839 -0
- package/dist/senses/bluebubbles/latest-turn.js +311 -0
- package/dist/senses/{bluebubbles-media.js → bluebubbles/media.js} +121 -71
- package/dist/senses/{bluebubbles-model.js → bluebubbles/model.js} +159 -28
- package/dist/senses/{bluebubbles-mutation-log.js → bluebubbles/mutation-log.js} +24 -13
- package/dist/senses/bluebubbles/outbound-state.js +308 -0
- package/dist/senses/bluebubbles/processed-log.js +133 -0
- package/dist/senses/bluebubbles/reaction-policy.js +47 -0
- package/dist/senses/bluebubbles/replay.js +284 -0
- package/dist/senses/{bluebubbles-runtime-state.js → bluebubbles/runtime-state.js} +30 -2
- package/dist/senses/bluebubbles/semantic-receipts.js +1986 -0
- package/dist/senses/{bluebubbles-session-cleanup.js → bluebubbles/session-cleanup.js} +1 -1
- package/dist/senses/bluebubbles/webhook-registration.js +228 -0
- package/dist/senses/bluebubbles-meta-guard.js +39 -0
- package/dist/senses/cli/bracketed-paste.js +82 -0
- package/dist/senses/cli/image-paste.js +287 -0
- package/dist/senses/cli/image-ref-navigation.js +75 -0
- package/dist/senses/cli/ink-app.js +156 -0
- package/dist/senses/cli/inline-diff.js +64 -0
- package/dist/senses/cli/input-keys.js +174 -0
- package/dist/senses/cli/kill-ring.js +86 -0
- package/dist/senses/cli/message-list.js +51 -0
- package/dist/senses/cli/ouro-tui.js +607 -0
- package/dist/senses/cli/spinner-imperative.js +135 -0
- package/dist/senses/cli/spinner.js +101 -0
- package/dist/senses/cli/status-line.js +60 -0
- package/dist/senses/cli/streaming-markdown.js +526 -0
- package/dist/senses/cli/tool-display.js +85 -0
- package/dist/senses/cli/tool-render.js +85 -0
- package/dist/senses/cli/tui-store.js +241 -0
- package/dist/senses/cli/virtual-list.js +35 -0
- package/dist/senses/cli-entry.js +60 -8
- package/dist/senses/cli.js +600 -317
- package/dist/senses/commands.js +66 -3
- package/dist/senses/context-packet-ledger.js +371 -0
- package/dist/senses/context-packets.js +154 -0
- package/dist/senses/habit-lifecycle-guard.js +24 -0
- package/dist/senses/habit-turn-message.js +160 -0
- package/dist/senses/ingress-evidence.js +2 -0
- package/dist/senses/mail-entry.js +66 -0
- package/dist/senses/mail.js +524 -0
- package/dist/senses/pipeline.js +1110 -204
- package/dist/senses/private-runtime-worker.js +859 -0
- package/dist/senses/private-runtime.js +1496 -0
- package/dist/senses/proactive-content-guard.js +51 -0
- package/dist/senses/sanctuary-download-credit-presentation.js +31 -0
- package/dist/senses/sanctuary-full-visibility-contract.js +302 -0
- package/dist/senses/sanctuary-grounding.js +118 -0
- package/dist/senses/sanctuary-health-acceptance-probe-entry.js +12 -0
- package/dist/senses/sanctuary-health-acceptance-probe.js +774 -0
- package/dist/senses/sanctuary-health-runner.js +101 -0
- package/dist/senses/sanctuary-health.js +408 -0
- package/dist/senses/sanctuary-interactive-control.js +306 -0
- package/dist/senses/sanctuary-media-catalog-contract.js +235 -0
- package/dist/senses/sanctuary-media-optimization.js +514 -0
- package/dist/senses/sanctuary-runtime.js +338 -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 +571 -0
- package/dist/senses/surface-tool.js +109 -0
- package/dist/senses/teams-entry.js +60 -8
- package/dist/senses/teams.js +515 -217
- package/dist/senses/telegram-admission.js +711 -0
- package/dist/senses/telegram-approval-runtime.js +566 -0
- package/dist/senses/telegram-attachments.js +182 -0
- package/dist/senses/telegram-audit-ledger.js +220 -0
- package/dist/senses/telegram-client.js +1677 -0
- package/dist/senses/telegram-effect-adapter.js +859 -0
- package/dist/senses/telegram-entry.js +83 -0
- package/dist/senses/telegram.js +2026 -0
- package/dist/senses/trust-gate.js +104 -9
- package/dist/senses/voice/audio-playback.js +237 -0
- package/dist/senses/voice/audio-routing.js +119 -0
- package/dist/senses/voice/elevenlabs.js +202 -0
- package/dist/senses/voice/floor-control.js +431 -0
- package/dist/senses/voice/floor-controller.js +115 -0
- package/dist/senses/voice/golden-path.js +116 -0
- package/dist/senses/voice/index.js +29 -0
- package/dist/senses/voice/meeting.js +113 -0
- package/dist/senses/voice/outbound.js +190 -0
- package/dist/senses/voice/phone.js +33 -0
- package/dist/senses/voice/playback.js +139 -0
- package/dist/senses/voice/realtime-eval.js +496 -0
- package/dist/senses/voice/realtime-trace.js +531 -0
- package/dist/senses/voice/transcript.js +70 -0
- package/dist/senses/voice/turn.js +191 -0
- package/dist/senses/voice/twilio-phone-runtime.js +807 -0
- package/dist/senses/voice/twilio-phone.js +5081 -0
- package/dist/senses/voice/types.js +2 -0
- package/dist/senses/voice/whisper.js +161 -0
- package/dist/senses/voice-entry.js +81 -0
- package/dist/senses/voice-realtime-eval-command.js +99 -0
- package/dist/senses/voice-realtime-eval-entry.js +21 -0
- package/dist/senses/voice-twilio-entry.js +87 -0
- package/dist/trips/core.js +138 -0
- package/dist/trips/store.js +265 -0
- package/dist/util/frontmatter.js +69 -0
- package/npm-shrinkwrap.json +8144 -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 -164
- package/dist/heart/daemon/update-hooks.js +0 -138
- package/dist/heart/obligations.js +0 -141
- package/dist/heart/safe-workspace.js +0 -228
- package/dist/heart/session-recall.js +0 -116
- package/dist/mind/friends/channel.js +0 -84
- package/dist/mind/friends/group-context.js +0 -144
- package/dist/mind/friends/resolver.js +0 -84
- package/dist/mind/friends/store-file.js +0 -190
- package/dist/mind/friends/store.js +0 -4
- package/dist/mind/friends/tokens.js +0 -26
- package/dist/mind/friends/trust-explanation.js +0 -74
- package/dist/mind/friends/types.js +0 -29
- package/dist/repertoire/tasks/board.js +0 -134
- package/dist/repertoire/tasks/index.js +0 -224
- package/dist/repertoire/tasks/lifecycle.js +0 -80
- package/dist/repertoire/tasks/middleware.js +0 -65
- package/dist/repertoire/tasks/parser.js +0 -173
- package/dist/repertoire/tasks/scanner.js +0 -132
- package/dist/repertoire/tasks/transitions.js +0 -144
- package/dist/senses/bluebubbles-entry.js +0 -13
- package/dist/senses/bluebubbles.js +0 -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 @@
|
|
|
1
|
+
{"scheduler":"supercronic","updates":"disabled"}
|
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
#!/usr/local/bin/node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from "node:child_process"
|
|
4
|
+
import { createHash } from "node:crypto"
|
|
5
|
+
import { closeSync, constants, fchmodSync, fchownSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, unlinkSync, writeFileSync } from "node:fs"
|
|
6
|
+
import { basename, dirname, resolve } from "node:path"
|
|
7
|
+
import { fileURLToPath } from "node:url"
|
|
8
|
+
|
|
9
|
+
import xmlValidator from "./docker-man-template-xml.cjs"
|
|
10
|
+
|
|
11
|
+
const TARGET_PATH = "/boot/config/plugins/dockerMan/templates-user/my-ouro-butler.xml"
|
|
12
|
+
const JOURNAL_PATH = "/boot/config/custom/ouro-butler/docker-man-template-transaction.json"
|
|
13
|
+
const TEMPLATE_URL = "https://raw.githubusercontent.com/ourostack/ouroboros/main/deploy/unraid/sanctuary.xml"
|
|
14
|
+
const ICON = "https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png"
|
|
15
|
+
const COMMUNITY_APPS_ENTRY_PATH = "/usr/local/emhttp/plugins/community.applications/include/exec.php"
|
|
16
|
+
const COMMUNITY_APPS_HELPER_PATH = "/usr/local/emhttp/plugins/community.applications/include/previous_apps_helpers.php"
|
|
17
|
+
const IMAGE_ID = /^sha256:[0-9a-f]{64}$/u
|
|
18
|
+
const CONTAINER_ID = /^[0-9a-f]{64}$/u
|
|
19
|
+
const VERSION_TAG = /^ghcr\.io\/ourostack\/ouroboros-butler:[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/u
|
|
20
|
+
const JELLYFIN_STATES = new Set(["created", "running", "paused", "restarting", "removing", "exited", "dead"])
|
|
21
|
+
const JELLYFIN_FORMAT = '{"name":{{json .Name}},"containerId":{{json .Id}},"imageId":{{json .Image}},"state":{{json .State.Status}},"restartCount":{{json .RestartCount}}}'
|
|
22
|
+
|
|
23
|
+
function object(value, label) {
|
|
24
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`)
|
|
25
|
+
return value
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function exactKeys(value, expected, label) {
|
|
29
|
+
const keys = Object.keys(value).sort()
|
|
30
|
+
if (JSON.stringify(keys) !== JSON.stringify([...expected].sort())) throw new Error(`${label} has unexpected fields`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function validateJellyfinState(raw, label = "Jellyfin checkpoint") {
|
|
34
|
+
const value = object(raw, label)
|
|
35
|
+
exactKeys(value, ["containerId", "imageId", "state", "restartCount"], label)
|
|
36
|
+
if (!CONTAINER_ID.test(value.containerId) || !IMAGE_ID.test(value.imageId) || !JELLYFIN_STATES.has(value.state) || !Number.isSafeInteger(value.restartCount) || value.restartCount < 0) throw new Error(`${label} is invalid`)
|
|
37
|
+
return { containerId: value.containerId, imageId: value.imageId, state: value.state, restartCount: value.restartCount }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function inspectCurrentJellyfinState() {
|
|
41
|
+
let inspected
|
|
42
|
+
try {
|
|
43
|
+
inspected = JSON.parse(execFileSync("/usr/bin/docker", ["container", "inspect", "--format", JELLYFIN_FORMAT, "jellyfin"], { encoding: "utf8", maxBuffer: 65_536, stdio: ["ignore", "pipe", "ignore"] }).trim())
|
|
44
|
+
} catch {
|
|
45
|
+
throw new Error("Jellyfin container inspection failed")
|
|
46
|
+
}
|
|
47
|
+
const value = object(inspected, "Jellyfin container inspection")
|
|
48
|
+
exactKeys(value, ["name", "containerId", "imageId", "state", "restartCount"], "Jellyfin container inspection")
|
|
49
|
+
if (value.name !== "/jellyfin") throw new Error("Jellyfin container identity is invalid")
|
|
50
|
+
return validateJellyfinState({ containerId: value.containerId, imageId: value.imageId, state: value.state, restartCount: value.restartCount })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function assertJellyfinUnchanged(expected) {
|
|
54
|
+
const current = inspectCurrentJellyfinState()
|
|
55
|
+
if (JSON.stringify(current) !== JSON.stringify(expected)) throw new Error("Jellyfin changed during the Butler transaction")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function digest(bytes) {
|
|
59
|
+
return `sha256:${createHash("sha256").update(bytes).digest("hex")}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function recordDigest(record) {
|
|
63
|
+
const { digest: _ignored, ...unsigned } = record
|
|
64
|
+
return digest(Buffer.from(JSON.stringify(unsigned)))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function directChildren(document, name) {
|
|
68
|
+
return document.children.filter((child) => child.name === name)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function singleTextChild(document, name) {
|
|
72
|
+
const children = directChildren(document, name)
|
|
73
|
+
if (children.length !== 1 || children[0].form !== "text" || Object.keys(children[0].attributes).length !== 0) throw new Error(`template ${name} must appear exactly once as plain text`)
|
|
74
|
+
return children[0].text
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function templateIdentity(bytes, expectedRepository) {
|
|
78
|
+
const document = xmlValidator.parseDockerManTemplateXml(bytes)
|
|
79
|
+
if (!document) throw new Error("canonical DockerMan XML structure is invalid")
|
|
80
|
+
const identity = {
|
|
81
|
+
name: singleTextChild(document, "Name"),
|
|
82
|
+
repository: singleTextChild(document, "Repository"),
|
|
83
|
+
templateUrl: singleTextChild(document, "TemplateURL"),
|
|
84
|
+
icon: singleTextChild(document, "Icon"),
|
|
85
|
+
}
|
|
86
|
+
if (identity.name !== "ouro-butler" || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]+$/u.test(identity.name)) throw new Error("template technical name is invalid")
|
|
87
|
+
if (identity.repository !== expectedRepository || !VERSION_TAG.test(identity.repository)) throw new Error("template version reference is invalid")
|
|
88
|
+
if (identity.templateUrl !== TEMPLATE_URL || identity.icon !== ICON) throw new Error("template identity is invalid")
|
|
89
|
+
const webUi = directChildren(document, "WebUI")
|
|
90
|
+
if (webUi.length !== 1 || webUi[0].form !== "empty" || Object.keys(webUi[0].attributes).length !== 0) throw new Error("template WebUI must be exactly empty")
|
|
91
|
+
return identity
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function validateDirectory(path, expectedUid, expectedGid) {
|
|
95
|
+
const metadata = lstatSync(path)
|
|
96
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink() || metadata.uid !== expectedUid || metadata.gid !== expectedGid || (metadata.mode & 0o777) !== 0o700 || realpathSync(path) !== resolve(path)) throw new Error("transaction parent is unsafe")
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function validateRegularFile(path, expectedUid, expectedGid, label) {
|
|
100
|
+
const metadata = lstatSync(path)
|
|
101
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== expectedUid || metadata.gid !== expectedGid || (metadata.mode & 0o777) !== 0o600) throw new Error(`${label} metadata is invalid`)
|
|
102
|
+
return metadata
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function metadataIfPresent(path) {
|
|
106
|
+
try {
|
|
107
|
+
return lstatSync(path)
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error?.code === "ENOENT") return null
|
|
110
|
+
throw error
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function isSafeRecoveryTemporaryMetadata(metadata, expectedUid, expectedGid) {
|
|
115
|
+
return Boolean(metadata
|
|
116
|
+
&& typeof metadata.isFile === "function"
|
|
117
|
+
&& typeof metadata.isSymbolicLink === "function"
|
|
118
|
+
&& metadata.isFile()
|
|
119
|
+
&& !metadata.isSymbolicLink()
|
|
120
|
+
&& metadata.uid === expectedUid
|
|
121
|
+
&& metadata.gid === expectedGid
|
|
122
|
+
&& (metadata.mode & 0o777) === 0o600)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function paths(options = {}) {
|
|
126
|
+
const targetPath = options.targetPath ?? TARGET_PATH
|
|
127
|
+
const journalPath = options.journalPath ?? JOURNAL_PATH
|
|
128
|
+
const expectedUid = options.expectedUid ?? 0
|
|
129
|
+
const expectedGid = options.expectedGid ?? 0
|
|
130
|
+
return {
|
|
131
|
+
targetPath,
|
|
132
|
+
journalPath,
|
|
133
|
+
templateParent: dirname(targetPath),
|
|
134
|
+
journalParent: dirname(journalPath),
|
|
135
|
+
targetTemporary: `${dirname(targetPath)}/.${basename(targetPath)}.ouro-transaction.tmp`,
|
|
136
|
+
journalTemporary: `${dirname(journalPath)}/.${basename(journalPath)}.ouro-transaction.tmp`,
|
|
137
|
+
expectedUid,
|
|
138
|
+
expectedGid,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function validateTemplateParent(state, allowTemporaryResidue = false) {
|
|
143
|
+
validateDirectory(state.templateParent, state.expectedUid, state.expectedGid)
|
|
144
|
+
const matches = readdirSync(state.templateParent).filter((entry) => entry.toLowerCase() === basename(state.targetPath).toLowerCase())
|
|
145
|
+
if (matches.length > 1 || (matches.length === 1 && matches[0] !== basename(state.targetPath))) throw new Error("case-folded DockerMan template name is ambiguous")
|
|
146
|
+
if (!allowTemporaryResidue && metadataIfPresent(state.targetTemporary)) throw new Error("template transaction temporary state is ambiguous")
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function validateJournalParent(state, allowTemporaryResidue = false) {
|
|
150
|
+
validateDirectory(state.journalParent, state.expectedUid, state.expectedGid)
|
|
151
|
+
if (!allowTemporaryResidue && metadataIfPresent(state.journalTemporary)) throw new Error("template transaction temporary state is ambiguous")
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function validateParents(state, allowTemporaryResidue = false) {
|
|
155
|
+
validateTemplateParent(state, allowTemporaryResidue)
|
|
156
|
+
validateJournalParent(state, allowTemporaryResidue)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function ensureJournalParent(state) {
|
|
160
|
+
if (!metadataIfPresent(state.journalParent)) {
|
|
161
|
+
const parent = dirname(state.journalParent)
|
|
162
|
+
validateDirectory(parent, state.expectedUid, state.expectedGid)
|
|
163
|
+
mkdirSync(state.journalParent, { mode: 0o700 })
|
|
164
|
+
syncDirectory(parent)
|
|
165
|
+
}
|
|
166
|
+
validateJournalParent(state)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function syncDirectory(path) {
|
|
170
|
+
const fd = openSync(path, constants.O_RDONLY)
|
|
171
|
+
try { fsyncSync(fd) } finally { closeSync(fd) }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function atomicWrite(path, temporaryPath, bytes, metadata, expectedUid, expectedGid) {
|
|
175
|
+
const fd = openSync(temporaryPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, metadata.mode)
|
|
176
|
+
try {
|
|
177
|
+
writeFileSync(fd, bytes)
|
|
178
|
+
fchmodSync(fd, metadata.mode)
|
|
179
|
+
fchownSync(fd, metadata.uid ?? expectedUid, metadata.gid ?? expectedGid)
|
|
180
|
+
fsyncSync(fd)
|
|
181
|
+
} finally {
|
|
182
|
+
closeSync(fd)
|
|
183
|
+
}
|
|
184
|
+
renameSync(temporaryPath, path)
|
|
185
|
+
syncDirectory(dirname(path))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function deleteDurably(path) {
|
|
189
|
+
unlinkSync(path)
|
|
190
|
+
syncDirectory(dirname(path))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function decodeCanonicalBase64(value) {
|
|
194
|
+
if (typeof value !== "string" || Buffer.from(value, "base64").toString("base64") !== value) throw new Error("template transaction journal prior bytes are invalid")
|
|
195
|
+
return Buffer.from(value, "base64")
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function validateJournalRecord(raw, state) {
|
|
199
|
+
const record = object(raw, "template transaction journal")
|
|
200
|
+
exactKeys(record, ["schemaVersion", "state", "target", "priorTemplate", "priorTemplateDigest", "targetTemplateDigest", "canonicalVersionTag", "reviewedManifestDigest", "rollbackImageId", "targetImageId", "jellyfin", "digest"], "template transaction journal")
|
|
201
|
+
if (record.schemaVersion !== 1 || !["rollback", "committing"].includes(record.state)) throw new Error("template transaction journal state is invalid")
|
|
202
|
+
const target = object(record.target, "template transaction journal target")
|
|
203
|
+
exactKeys(target, ["path", "name", "templateUrl", "icon"], "template transaction journal target")
|
|
204
|
+
if (target.path !== state.targetPath || target.name !== "ouro-butler" || target.templateUrl !== TEMPLATE_URL || target.icon !== ICON) throw new Error("template transaction journal target is invalid")
|
|
205
|
+
const prior = object(record.priorTemplate, "template transaction journal prior template")
|
|
206
|
+
exactKeys(prior, ["present", "bytesBase64", "metadata"], "template transaction journal prior template")
|
|
207
|
+
if (typeof prior.present !== "boolean") throw new Error("template transaction journal prior presence is invalid")
|
|
208
|
+
let priorBytes = null
|
|
209
|
+
if (prior.present) {
|
|
210
|
+
priorBytes = decodeCanonicalBase64(prior.bytesBase64)
|
|
211
|
+
const metadata = object(prior.metadata, "template transaction journal prior metadata")
|
|
212
|
+
exactKeys(metadata, ["uid", "gid", "mode"], "template transaction journal prior metadata")
|
|
213
|
+
if (metadata.uid !== state.expectedUid || metadata.gid !== state.expectedGid || metadata.mode !== 0o600 || record.priorTemplateDigest !== digest(priorBytes)) throw new Error("template transaction journal prior metadata is invalid")
|
|
214
|
+
} else if (prior.bytesBase64 !== null || prior.metadata !== null || record.priorTemplateDigest !== null) {
|
|
215
|
+
throw new Error("template transaction journal absent prior state is invalid")
|
|
216
|
+
}
|
|
217
|
+
if (!VERSION_TAG.test(record.canonicalVersionTag) || !IMAGE_ID.test(record.reviewedManifestDigest) || !IMAGE_ID.test(record.rollbackImageId) || !IMAGE_ID.test(record.targetImageId) || record.rollbackImageId === record.targetImageId || !IMAGE_ID.test(record.targetTemplateDigest)) throw new Error("template transaction journal release identity is invalid")
|
|
218
|
+
const jellyfin = validateJellyfinState(record.jellyfin, "template transaction journal Jellyfin checkpoint")
|
|
219
|
+
if (!IMAGE_ID.test(record.digest) || record.digest !== recordDigest(record)) throw new Error("template transaction journal digest is invalid")
|
|
220
|
+
return { ...record, jellyfin, priorBytes }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function readJournal(state) {
|
|
224
|
+
if (!metadataIfPresent(state.journalPath)) return null
|
|
225
|
+
validateRegularFile(state.journalPath, state.expectedUid, state.expectedGid, "template transaction journal")
|
|
226
|
+
let parsed
|
|
227
|
+
try { parsed = JSON.parse(readFileSync(state.journalPath, "utf8")) } catch { throw new Error("template transaction journal JSON is invalid") }
|
|
228
|
+
return validateJournalRecord(parsed, state)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function writeJournal(record, state) {
|
|
232
|
+
const complete = { ...record, digest: recordDigest(record) }
|
|
233
|
+
atomicWrite(state.journalPath, state.journalTemporary, Buffer.from(`${JSON.stringify(complete)}\n`), { uid: state.expectedUid, gid: state.expectedGid, mode: 0o600 }, state.expectedUid, state.expectedGid)
|
|
234
|
+
return complete
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function currentTargetDigest(state) {
|
|
238
|
+
if (!metadataIfPresent(state.targetPath)) return null
|
|
239
|
+
validateRegularFile(state.targetPath, state.expectedUid, state.expectedGid, "DockerMan template")
|
|
240
|
+
return digest(readFileSync(state.targetPath))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function inspectRecoveryTemporary(state) {
|
|
244
|
+
validateParents(state, true)
|
|
245
|
+
const residues = [state.targetTemporary, state.journalTemporary]
|
|
246
|
+
.map((path) => ({ path, metadata: metadataIfPresent(path) }))
|
|
247
|
+
.filter((entry) => entry.metadata)
|
|
248
|
+
if (residues.length === 0) return null
|
|
249
|
+
if (residues.length !== 1) throw new Error("template transaction recovery temporary state is ambiguous")
|
|
250
|
+
const residue = residues[0]
|
|
251
|
+
if (!isSafeRecoveryTemporaryMetadata(residue.metadata, state.expectedUid, state.expectedGid)) throw new Error("template transaction recovery temporary file is unsafe")
|
|
252
|
+
const journalMetadata = metadataIfPresent(state.journalPath)
|
|
253
|
+
if (journalMetadata) readJournal(state)
|
|
254
|
+
if (residue.path === state.targetTemporary) {
|
|
255
|
+
if (!journalMetadata) throw new Error("template transaction recovery target temporary file is orphaned")
|
|
256
|
+
const journal = readJournal(state)
|
|
257
|
+
const installedDigest = currentTargetDigest(state)
|
|
258
|
+
const allowedDigests = [journal.targetTemplateDigest, journal.priorTemplateDigest]
|
|
259
|
+
if (!(allowedDigests.includes(installedDigest) || (!journal.priorTemplate.present && installedDigest === null))) throw new Error("template transaction recovery target state is ambiguous")
|
|
260
|
+
}
|
|
261
|
+
return residue
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function cleanupRecoveryTemporary(state) {
|
|
265
|
+
const residue = inspectRecoveryTemporary(state)
|
|
266
|
+
if (!residue) return
|
|
267
|
+
deleteDurably(residue.path)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function publicJournalRecord(result) {
|
|
271
|
+
if (!result) return null
|
|
272
|
+
const { priorBytes: _ignored, ...record } = result
|
|
273
|
+
return record
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function inspectDockerManTemplateTransactionRecoveryIdentity(options = {}) {
|
|
277
|
+
const state = paths(options)
|
|
278
|
+
inspectRecoveryTemporary(state)
|
|
279
|
+
const current = readJournal(state)
|
|
280
|
+
const jellyfin = inspectCurrentJellyfinState()
|
|
281
|
+
if (current && JSON.stringify(jellyfin) !== JSON.stringify(current.jellyfin)) throw new Error("Jellyfin changed during the Butler transaction")
|
|
282
|
+
return publicJournalRecord(current)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function inspectDockerManTemplateTransactionForRecovery(options = {}) {
|
|
286
|
+
const state = paths(options)
|
|
287
|
+
const expected = inspectDockerManTemplateTransactionRecoveryIdentity(options)
|
|
288
|
+
cleanupRecoveryTemporary(state)
|
|
289
|
+
const current = inspectDockerManTemplateTransaction(options)
|
|
290
|
+
if (JSON.stringify(current) !== JSON.stringify(expected)) throw new Error("template transaction changed during recovery")
|
|
291
|
+
return current
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function inspectDockerManTemplateTransaction(options = {}) {
|
|
295
|
+
const state = paths(options)
|
|
296
|
+
validateParents(state)
|
|
297
|
+
return publicJournalRecord(readJournal(state))
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function validateTemplateSource(input, state) {
|
|
301
|
+
validateTemplateParent(state)
|
|
302
|
+
if (metadataIfPresent(state.targetPath)) validateRegularFile(state.targetPath, state.expectedUid, state.expectedGid, "DockerMan template")
|
|
303
|
+
validateDirectory(dirname(input.sourceTemplatePath), state.expectedUid, state.expectedGid)
|
|
304
|
+
validateRegularFile(input.sourceTemplatePath, state.expectedUid, state.expectedGid, "source template")
|
|
305
|
+
if (!VERSION_TAG.test(input.canonicalVersionTag) || !IMAGE_ID.test(input.reviewedManifestDigest) || !IMAGE_ID.test(input.rollbackImageId) || !IMAGE_ID.test(input.targetImageId) || input.rollbackImageId === input.targetImageId) throw new Error("reviewed release identity is invalid")
|
|
306
|
+
const sourceBytes = readFileSync(input.sourceTemplatePath)
|
|
307
|
+
const identity = templateIdentity(sourceBytes, input.canonicalVersionTag)
|
|
308
|
+
return { sourceBytes, identity }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function prepareDockerManTemplateTransaction(input, options = {}) {
|
|
312
|
+
const state = paths(options)
|
|
313
|
+
const { sourceBytes, identity } = validateTemplateSource(input, state)
|
|
314
|
+
const jellyfin = inspectCurrentJellyfinState()
|
|
315
|
+
ensureJournalParent(state)
|
|
316
|
+
const existing = readJournal(state)
|
|
317
|
+
if (existing) {
|
|
318
|
+
if (existing.canonicalVersionTag !== input.canonicalVersionTag || existing.reviewedManifestDigest !== input.reviewedManifestDigest || existing.rollbackImageId !== input.rollbackImageId || existing.targetImageId !== input.targetImageId || existing.targetTemplateDigest !== digest(sourceBytes) || currentTargetDigest(state) !== existing.targetTemplateDigest || JSON.stringify(existing.jellyfin) !== JSON.stringify(jellyfin)) throw new Error("pending template transaction does not match the reviewed release")
|
|
319
|
+
const { priorBytes: _ignored, ...record } = existing
|
|
320
|
+
return record
|
|
321
|
+
}
|
|
322
|
+
let priorTemplate
|
|
323
|
+
let priorTemplateDigest = null
|
|
324
|
+
if (metadataIfPresent(state.targetPath)) {
|
|
325
|
+
const metadata = validateRegularFile(state.targetPath, state.expectedUid, state.expectedGid, "DockerMan template")
|
|
326
|
+
const bytes = readFileSync(state.targetPath)
|
|
327
|
+
priorTemplateDigest = digest(bytes)
|
|
328
|
+
priorTemplate = { present: true, bytesBase64: bytes.toString("base64"), metadata: { uid: metadata.uid, gid: metadata.gid, mode: metadata.mode & 0o777 } }
|
|
329
|
+
} else {
|
|
330
|
+
priorTemplate = { present: false, bytesBase64: null, metadata: null }
|
|
331
|
+
}
|
|
332
|
+
const unsigned = {
|
|
333
|
+
schemaVersion: 1,
|
|
334
|
+
state: "rollback",
|
|
335
|
+
target: { path: state.targetPath, name: identity.name, templateUrl: identity.templateUrl, icon: identity.icon },
|
|
336
|
+
priorTemplate,
|
|
337
|
+
priorTemplateDigest,
|
|
338
|
+
targetTemplateDigest: digest(sourceBytes),
|
|
339
|
+
canonicalVersionTag: input.canonicalVersionTag,
|
|
340
|
+
reviewedManifestDigest: input.reviewedManifestDigest,
|
|
341
|
+
rollbackImageId: input.rollbackImageId,
|
|
342
|
+
targetImageId: input.targetImageId,
|
|
343
|
+
jellyfin,
|
|
344
|
+
}
|
|
345
|
+
const record = writeJournal(unsigned, state)
|
|
346
|
+
options.checkpoint?.("after-template-journal")
|
|
347
|
+
atomicWrite(state.targetPath, state.targetTemporary, sourceBytes, { uid: state.expectedUid, gid: state.expectedGid, mode: 0o600 }, state.expectedUid, state.expectedGid)
|
|
348
|
+
options.checkpoint?.("after-template-replacement")
|
|
349
|
+
return record
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function markDockerManTemplateTransactionCommitting(options = {}) {
|
|
353
|
+
const state = paths(options)
|
|
354
|
+
validateParents(state)
|
|
355
|
+
const current = readJournal(state)
|
|
356
|
+
if (!current) throw new Error("template transaction journal is absent")
|
|
357
|
+
if (currentTargetDigest(state) !== current.targetTemplateDigest) throw new Error("installed DockerMan template does not match the transaction")
|
|
358
|
+
assertJellyfinUnchanged(current.jellyfin)
|
|
359
|
+
if (current.state === "committing") {
|
|
360
|
+
const { priorBytes: _ignored, ...record } = current
|
|
361
|
+
return record
|
|
362
|
+
}
|
|
363
|
+
const { priorBytes: _ignored, digest: _digest, ...record } = current
|
|
364
|
+
const committing = writeJournal({ ...record, state: "committing" }, state)
|
|
365
|
+
options.checkpoint?.("after-template-committing")
|
|
366
|
+
return committing
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function rollbackDockerManTemplateTransaction(options = {}) {
|
|
370
|
+
const state = paths(options)
|
|
371
|
+
validateParents(state)
|
|
372
|
+
const current = readJournal(state)
|
|
373
|
+
if (!current) return false
|
|
374
|
+
const installedDigest = currentTargetDigest(state)
|
|
375
|
+
assertJellyfinUnchanged(current.jellyfin)
|
|
376
|
+
if (current.priorTemplate.present) {
|
|
377
|
+
if (installedDigest !== current.targetTemplateDigest && installedDigest !== current.priorTemplateDigest) throw new Error("installed DockerMan template cannot be safely restored")
|
|
378
|
+
if (installedDigest !== current.priorTemplateDigest) atomicWrite(state.targetPath, state.targetTemporary, current.priorBytes, current.priorTemplate.metadata, state.expectedUid, state.expectedGid)
|
|
379
|
+
} else if (installedDigest !== null) {
|
|
380
|
+
if (installedDigest !== current.targetTemplateDigest) throw new Error("installed DockerMan template cannot be safely removed")
|
|
381
|
+
deleteDurably(state.targetPath)
|
|
382
|
+
}
|
|
383
|
+
options.checkpoint?.("after-template-restore")
|
|
384
|
+
deleteDurably(state.journalPath)
|
|
385
|
+
return true
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function validateFinalProof(proof, record, state) {
|
|
389
|
+
const root = object(proof, "final install proof")
|
|
390
|
+
const container = object(root.container, "final container proof")
|
|
391
|
+
const labels = object(container.labels, "final container labels")
|
|
392
|
+
if (container.name !== "/ouro-butler" || container.imageId !== record.targetImageId || container.imageReference !== record.canonicalVersionTag || container.running !== true || container.healthy !== true || container.autostart !== true) throw new Error("final container proof is invalid")
|
|
393
|
+
if (labels["net.unraid.docker.managed"] !== "dockerman" || labels["net.unraid.docker.icon"] !== record.target.icon || Object.prototype.hasOwnProperty.call(labels, "net.unraid.docker.webui")) throw new Error("final container install labels are invalid")
|
|
394
|
+
const bundle = object(root.bundle, "final bundle proof")
|
|
395
|
+
const bundleData = object(bundle.data, "final bundle proof data")
|
|
396
|
+
if (bundle.ok !== true || bundleData.parity !== "exact" || bundleData.journalState !== "absent" || bundleData.ready !== true) throw new Error("final bundle proof is invalid")
|
|
397
|
+
const dockerMan = object(root.dockerMan, "final DockerMan proof")
|
|
398
|
+
if (dockerMan.templatePath !== state.targetPath || dockerMan.name !== "ouro-butler" || dockerMan.repository !== record.canonicalVersionTag || dockerMan.templateUrl !== record.target.templateUrl || dockerMan.icon !== record.target.icon) throw new Error("final DockerMan proof is invalid")
|
|
399
|
+
const communityApps = object(root.communityApps, "final Community Apps proof")
|
|
400
|
+
const inlineImplementation = communityApps.stateModel === "previous-apps-inline-v1" && communityApps.entryPath === COMMUNITY_APPS_ENTRY_PATH && communityApps.entryFunction === "previous_apps" && communityApps.implementationPath === COMMUNITY_APPS_ENTRY_PATH && communityApps.implementationSymbol === "previous_apps"
|
|
401
|
+
const helperImplementation = communityApps.stateModel === "previous-apps-helper-v1" && communityApps.entryPath === COMMUNITY_APPS_ENTRY_PATH && communityApps.entryFunction === "previous_apps" && communityApps.implementationPath === COMMUNITY_APPS_HELPER_PATH && communityApps.implementationSymbol === "PreviousAppsHelpers::collectDockerApplications"
|
|
402
|
+
const recognizedImplementation = inlineImplementation || helperImplementation
|
|
403
|
+
if (communityApps.installed !== true || communityApps.name !== "ouro-butler" || communityApps.repository !== record.canonicalVersionTag || communityApps.templateUrl !== record.target.templateUrl || !recognizedImplementation) throw new Error("final Community Apps proof is invalid")
|
|
404
|
+
const jellyfin = validateJellyfinState(root.jellyfin, "final Jellyfin proof")
|
|
405
|
+
if (JSON.stringify(jellyfin) !== JSON.stringify(record.jellyfin)) throw new Error("final Jellyfin proof is invalid")
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function commitDockerManTemplateTransaction(proof, options = {}) {
|
|
409
|
+
const state = paths(options)
|
|
410
|
+
validateParents(state)
|
|
411
|
+
const current = readJournal(state)
|
|
412
|
+
if (!current) return false
|
|
413
|
+
if (current.state !== "committing" || currentTargetDigest(state) !== current.targetTemplateDigest) throw new Error("template transaction is not ready to commit")
|
|
414
|
+
validateFinalProof(proof, current, state)
|
|
415
|
+
assertJellyfinUnchanged(current.jellyfin)
|
|
416
|
+
deleteDurably(state.journalPath)
|
|
417
|
+
return true
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export function verifyDockerManTemplateTransactionJellyfin(options = {}) {
|
|
421
|
+
const state = paths(options)
|
|
422
|
+
validateParents(state)
|
|
423
|
+
const current = readJournal(state)
|
|
424
|
+
if (!current) throw new Error("template transaction journal is absent")
|
|
425
|
+
assertJellyfinUnchanged(current.jellyfin)
|
|
426
|
+
return true
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function decideDockerManTemplateRecovery(record, evidence) {
|
|
430
|
+
const state = record?.state
|
|
431
|
+
const bundle = evidence?.bundleJournalState
|
|
432
|
+
const production = evidence?.production
|
|
433
|
+
if (!["rollback", "committing"].includes(state) || !["absent", "rollback", "committing"].includes(bundle) || !["rollback-exact", "target-exact-committing", "target-exact-ready", "adoption-source-exact", "adoption-target-exact-ready", "adoption-evidence-exact-stopped"].includes(production)) throw new Error("template transaction recovery evidence is invalid")
|
|
434
|
+
if (bundle === "rollback" && production === "rollback-exact") return "rollback-both"
|
|
435
|
+
if (state === "committing" && bundle === "committing" && production === "target-exact-committing") {
|
|
436
|
+
const inspection = evidence?.inspection
|
|
437
|
+
if (inspection?.ok !== true || inspection?.data?.parity !== "exact" || inspection?.data?.journalState !== "committing" || inspection?.data?.ready !== false) throw new Error("template transaction recovery inspection is invalid")
|
|
438
|
+
return "finish-bundle-commit"
|
|
439
|
+
}
|
|
440
|
+
if (state === "committing" && bundle === "absent" && production === "target-exact-ready") {
|
|
441
|
+
const inspection = evidence?.inspection
|
|
442
|
+
if (inspection?.ok !== true || inspection?.data?.parity !== "exact" || inspection?.data?.journalState !== "absent" || inspection?.data?.ready !== true) throw new Error("template transaction recovery inspection is invalid")
|
|
443
|
+
return "finish-template-commit"
|
|
444
|
+
}
|
|
445
|
+
if (bundle === "absent" && production === "rollback-exact") return "restore-prior-template"
|
|
446
|
+
if (state === "rollback" && bundle === "absent" && production === "adoption-source-exact") return "restore-prior-template"
|
|
447
|
+
if (state === "rollback" && bundle === "absent" && production === "adoption-target-exact-ready") {
|
|
448
|
+
const inspection = evidence?.inspection
|
|
449
|
+
if (inspection?.ok !== true || inspection?.data?.parity !== "exact" || inspection?.data?.journalState !== "absent" || inspection?.data?.ready !== true) throw new Error("template transaction recovery inspection is invalid")
|
|
450
|
+
return "roll-forward-adoption"
|
|
451
|
+
}
|
|
452
|
+
if (state === "rollback" && bundle === "absent" && production === "adoption-evidence-exact-stopped") return "quarantine-adoption"
|
|
453
|
+
throw new Error("template and bundle transaction topology is incompatible")
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function parseArguments(argv) {
|
|
457
|
+
const [operation, ...rest] = argv
|
|
458
|
+
const values = new Map()
|
|
459
|
+
for (let index = 0; index < rest.length; index += 2) {
|
|
460
|
+
const flag = rest[index]
|
|
461
|
+
const value = rest[index + 1]
|
|
462
|
+
if (!flag?.startsWith("--") || !value || values.has(flag)) throw new Error("template transaction arguments are invalid")
|
|
463
|
+
values.set(flag, value)
|
|
464
|
+
}
|
|
465
|
+
return { operation, values }
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function readRootJson(path, label) {
|
|
469
|
+
validateRegularFile(path, 0, 0, label)
|
|
470
|
+
try { return JSON.parse(readFileSync(path, "utf8")) } catch { throw new Error(`${label} JSON is invalid`) }
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function runDockerManTemplateTransactionCli(argv, options = {}, write = (text) => process.stdout.write(text)) {
|
|
474
|
+
const { operation, values } = parseArguments(argv)
|
|
475
|
+
let result
|
|
476
|
+
if (operation === "prepare" && values.size === 5) {
|
|
477
|
+
result = prepareDockerManTemplateTransaction({
|
|
478
|
+
sourceTemplatePath: values.get("--source-template"),
|
|
479
|
+
canonicalVersionTag: values.get("--version-tag"),
|
|
480
|
+
reviewedManifestDigest: values.get("--manifest-digest"),
|
|
481
|
+
rollbackImageId: values.get("--rollback-image-id"),
|
|
482
|
+
targetImageId: values.get("--target-image-id"),
|
|
483
|
+
}, options)
|
|
484
|
+
} else if (operation === "mark-committing" && values.size === 0) {
|
|
485
|
+
ensureJournalParent(paths(options))
|
|
486
|
+
result = markDockerManTemplateTransactionCommitting(options)
|
|
487
|
+
} else if (operation === "rollback" && values.size === 0) {
|
|
488
|
+
ensureJournalParent(paths(options))
|
|
489
|
+
result = { rolledBack: rollbackDockerManTemplateTransaction(options) }
|
|
490
|
+
} else if (operation === "commit" && values.size === 1 && values.get("--proof")) {
|
|
491
|
+
ensureJournalParent(paths(options))
|
|
492
|
+
result = { committed: commitDockerManTemplateTransaction(readRootJson(values.get("--proof"), "final install proof"), options) }
|
|
493
|
+
} else if (operation === "status" && values.size === 0) {
|
|
494
|
+
ensureJournalParent(paths(options))
|
|
495
|
+
result = inspectDockerManTemplateTransaction(options)
|
|
496
|
+
} else if (operation === "recover-status" && values.size === 0) {
|
|
497
|
+
const state = paths(options)
|
|
498
|
+
if (!metadataIfPresent(state.journalParent)) {
|
|
499
|
+
inspectCurrentJellyfinState()
|
|
500
|
+
ensureJournalParent(state)
|
|
501
|
+
}
|
|
502
|
+
result = inspectDockerManTemplateTransactionForRecovery(options)
|
|
503
|
+
} else if (operation === "recovery-identity" && values.size === 0) {
|
|
504
|
+
const state = paths(options)
|
|
505
|
+
if (!metadataIfPresent(state.journalParent)) {
|
|
506
|
+
inspectCurrentJellyfinState()
|
|
507
|
+
result = null
|
|
508
|
+
} else {
|
|
509
|
+
result = inspectDockerManTemplateTransactionRecoveryIdentity(options)
|
|
510
|
+
}
|
|
511
|
+
} else if (operation === "recovery-action" && values.size === 1 && values.get("--evidence")) {
|
|
512
|
+
ensureJournalParent(paths(options))
|
|
513
|
+
const record = inspectDockerManTemplateTransaction(options)
|
|
514
|
+
if (!record) throw new Error("template transaction journal is absent")
|
|
515
|
+
result = { action: decideDockerManTemplateRecovery(record, readRootJson(values.get("--evidence"), "template recovery evidence")) }
|
|
516
|
+
} else if (operation === "verify-jellyfin" && values.size === 0) {
|
|
517
|
+
result = { unchanged: verifyDockerManTemplateTransactionJellyfin(options) }
|
|
518
|
+
} else if (operation === "jellyfin-status" && values.size === 0) {
|
|
519
|
+
result = inspectCurrentJellyfinState()
|
|
520
|
+
} else {
|
|
521
|
+
throw new Error("Usage: docker-man-template-transaction.mjs <prepare|mark-committing|rollback|commit|status|recover-status|recovery-identity|recovery-action|verify-jellyfin|jellyfin-status> [fixed operation arguments]")
|
|
522
|
+
}
|
|
523
|
+
write(`${JSON.stringify(result)}\n`)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) runDockerManTemplateTransactionCli(process.argv.slice(2))
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const { TextDecoder } = require("node:util")
|
|
4
|
+
|
|
5
|
+
const DECLARATION = "<?xml version=\"1.0\"?>"
|
|
6
|
+
const ROOT_OPEN = "<Container version=\"2\">"
|
|
7
|
+
const ROOT_CLOSE = "</Container>"
|
|
8
|
+
const NAME_START = /^[A-Za-z_]$/u
|
|
9
|
+
const NAME_CHARACTER = /^[A-Za-z0-9_.:-]$/u
|
|
10
|
+
const XML_WHITESPACE = /^[\t\n\r ]$/u
|
|
11
|
+
const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true })
|
|
12
|
+
|
|
13
|
+
function decodeUtf8(value) {
|
|
14
|
+
if (typeof value === "string") return value
|
|
15
|
+
if (!(value instanceof Uint8Array)) throw new TypeError("DockerMan template bytes are invalid")
|
|
16
|
+
return UTF8_DECODER.decode(value)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function skipWhitespace(value, start) {
|
|
20
|
+
let index = start
|
|
21
|
+
while (XML_WHITESPACE.test(value[index] ?? "")) index += 1
|
|
22
|
+
return index
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readName(value, start) {
|
|
26
|
+
if (!NAME_START.test(value[start] ?? "")) return null
|
|
27
|
+
let index = start + 1
|
|
28
|
+
while (NAME_CHARACTER.test(value[index] ?? "")) index += 1
|
|
29
|
+
return { name: value.slice(start, index), index }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readOpenTag(xml, start) {
|
|
33
|
+
if (xml[start] !== "<") return null
|
|
34
|
+
const parsedName = readName(xml, start + 1)
|
|
35
|
+
if (!parsedName) return null
|
|
36
|
+
const attributes = new Map()
|
|
37
|
+
let index = parsedName.index
|
|
38
|
+
while (index < xml.length) {
|
|
39
|
+
if (xml[index] === ">") return { name: parsedName.name, attributes: Object.fromEntries(attributes), form: "text", index: index + 1 }
|
|
40
|
+
if (xml.startsWith("/>", index)) return { name: parsedName.name, attributes: Object.fromEntries(attributes), form: "empty", index: index + 2 }
|
|
41
|
+
const whitespaceEnd = skipWhitespace(xml, index)
|
|
42
|
+
if (whitespaceEnd === index) return null
|
|
43
|
+
index = whitespaceEnd
|
|
44
|
+
if (xml[index] === ">" || xml.startsWith("/>", index)) continue
|
|
45
|
+
const attribute = readName(xml, index)
|
|
46
|
+
if (!attribute || attributes.has(attribute.name)) return null
|
|
47
|
+
index = skipWhitespace(xml, attribute.index)
|
|
48
|
+
if (xml[index] !== "=") return null
|
|
49
|
+
index = skipWhitespace(xml, index + 1)
|
|
50
|
+
if (xml[index] !== "\"") return null
|
|
51
|
+
const end = xml.indexOf("\"", index + 1)
|
|
52
|
+
if (end === -1) return null
|
|
53
|
+
const value = xml.slice(index + 1, end)
|
|
54
|
+
if (!/^[\x20-\x7E]*$/u.test(value) || value.includes("<") || value.includes("&")) return null
|
|
55
|
+
attributes.set(attribute.name, value)
|
|
56
|
+
index = end + 1
|
|
57
|
+
}
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function hasValidXmlCharacters(value) {
|
|
62
|
+
for (const character of value) {
|
|
63
|
+
const codePoint = character.codePointAt(0)
|
|
64
|
+
if (!(codePoint === 0x9 || codePoint === 0xA || codePoint === 0xD
|
|
65
|
+
|| (codePoint >= 0x20 && codePoint <= 0xD7FF)
|
|
66
|
+
|| (codePoint >= 0xE000 && codePoint <= 0xFFFD)
|
|
67
|
+
|| (codePoint >= 0x10000 && codePoint <= 0x10FFFF))) return false
|
|
68
|
+
}
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Canonical DockerMan dialect: exact declaration and Container root, flat leaf children, double-quoted ASCII attributes, and literal text without entities or auxiliary XML constructs.
|
|
73
|
+
function parseDockerManTemplateXml(input) {
|
|
74
|
+
let xml
|
|
75
|
+
try { xml = decodeUtf8(input) } catch { return null }
|
|
76
|
+
if (!hasValidXmlCharacters(xml) || !xml.startsWith(DECLARATION)) return null
|
|
77
|
+
let index = skipWhitespace(xml, DECLARATION.length)
|
|
78
|
+
if (!xml.startsWith(ROOT_OPEN, index)) return null
|
|
79
|
+
index += ROOT_OPEN.length
|
|
80
|
+
const children = []
|
|
81
|
+
while (index < xml.length) {
|
|
82
|
+
index = skipWhitespace(xml, index)
|
|
83
|
+
if (xml.startsWith(ROOT_CLOSE, index)) {
|
|
84
|
+
index = skipWhitespace(xml, index + ROOT_CLOSE.length)
|
|
85
|
+
return index === xml.length ? { root: { name: "Container", attributes: { version: "2" } }, children } : null
|
|
86
|
+
}
|
|
87
|
+
const child = readOpenTag(xml, index)
|
|
88
|
+
if (!child) return null
|
|
89
|
+
index = child.index
|
|
90
|
+
if (child.form === "empty") {
|
|
91
|
+
children.push({ name: child.name, attributes: child.attributes, form: child.form, text: "" })
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
const close = `</${child.name}>`
|
|
95
|
+
const end = xml.indexOf("<", index)
|
|
96
|
+
if (end === -1 || !xml.startsWith(close, end)) return null
|
|
97
|
+
const text = xml.slice(index, end)
|
|
98
|
+
if (text.includes("&") || text.includes("]]>")) return null
|
|
99
|
+
children.push({ name: child.name, attributes: child.attributes, form: child.form, text })
|
|
100
|
+
index = end + close.length
|
|
101
|
+
}
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { decodeUtf8, parseDockerManTemplateXml }
|