botmux 3.15.0 → 3.16.0
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/README.en.md +2 -2
- package/README.md +2 -2
- package/dist/.runtime-build-id +1 -1
- package/dist/adapters/backend/destroy-result.d.ts +159 -0
- package/dist/adapters/backend/destroy-result.d.ts.map +1 -0
- package/dist/adapters/backend/destroy-result.js +223 -0
- package/dist/adapters/backend/destroy-result.js.map +1 -0
- package/dist/adapters/backend/herdr-backend.d.ts +4 -0
- package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
- package/dist/adapters/backend/herdr-backend.js +23 -1
- package/dist/adapters/backend/herdr-backend.js.map +1 -1
- package/dist/adapters/backend/mojo-backend.d.ts +577 -0
- package/dist/adapters/backend/mojo-backend.d.ts.map +1 -0
- package/dist/adapters/backend/mojo-backend.js +2471 -0
- package/dist/adapters/backend/mojo-backend.js.map +1 -0
- package/dist/adapters/backend/mojo-budgets.d.ts +21 -0
- package/dist/adapters/backend/mojo-budgets.d.ts.map +1 -0
- package/dist/adapters/backend/mojo-budgets.js +22 -0
- package/dist/adapters/backend/mojo-budgets.js.map +1 -0
- package/dist/adapters/backend/mojo-isolated-workspace.d.ts +69 -0
- package/dist/adapters/backend/mojo-isolated-workspace.d.ts.map +1 -0
- package/dist/adapters/backend/mojo-isolated-workspace.js +213 -0
- package/dist/adapters/backend/mojo-isolated-workspace.js.map +1 -0
- package/dist/adapters/backend/mojo-process-tree.d.ts +292 -0
- package/dist/adapters/backend/mojo-process-tree.d.ts.map +1 -0
- package/dist/adapters/backend/mojo-process-tree.js +486 -0
- package/dist/adapters/backend/mojo-process-tree.js.map +1 -0
- package/dist/adapters/backend/mojo-types.d.ts +542 -0
- package/dist/adapters/backend/mojo-types.d.ts.map +1 -0
- package/dist/adapters/backend/mojo-types.js +682 -0
- package/dist/adapters/backend/mojo-types.js.map +1 -0
- package/dist/adapters/backend/reproduce-command.js +3 -3
- package/dist/adapters/backend/reproduce-command.js.map +1 -1
- package/dist/adapters/backend/riff-backend.js +1 -1
- package/dist/adapters/backend/riff-backend.js.map +1 -1
- package/dist/adapters/backend/sandbox.d.ts +44 -7
- package/dist/adapters/backend/sandbox.d.ts.map +1 -1
- package/dist/adapters/backend/sandbox.js +30 -7
- package/dist/adapters/backend/sandbox.js.map +1 -1
- package/dist/adapters/backend/session-backend-selector.d.ts +14 -1
- package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -1
- package/dist/adapters/backend/session-backend-selector.js +82 -10
- package/dist/adapters/backend/session-backend-selector.js.map +1 -1
- package/dist/adapters/backend/types.d.ts +101 -6
- package/dist/adapters/backend/types.d.ts.map +1 -1
- package/dist/adapters/backend/types.js.map +1 -1
- package/dist/adapters/backend/zmx-backend.d.ts +11 -0
- package/dist/adapters/backend/zmx-backend.d.ts.map +1 -1
- package/dist/adapters/backend/zmx-backend.js +16 -1
- package/dist/adapters/backend/zmx-backend.js.map +1 -1
- package/dist/adapters/cli/codex-app.d.ts.map +1 -1
- package/dist/adapters/cli/codex-app.js +2 -2
- package/dist/adapters/cli/codex-app.js.map +1 -1
- package/dist/adapters/cli/codex.d.ts.map +1 -1
- package/dist/adapters/cli/codex.js +7 -0
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/adapters/cli/copilot.d.ts +3 -1
- package/dist/adapters/cli/copilot.d.ts.map +1 -1
- package/dist/adapters/cli/copilot.js +23 -5
- package/dist/adapters/cli/copilot.js.map +1 -1
- package/dist/adapters/cli/cursor.d.ts.map +1 -1
- package/dist/adapters/cli/cursor.js +13 -3
- package/dist/adapters/cli/cursor.js.map +1 -1
- package/dist/adapters/cli/grok.d.ts.map +1 -1
- package/dist/adapters/cli/grok.js +24 -5
- package/dist/adapters/cli/grok.js.map +1 -1
- package/dist/adapters/cli/kimi.d.ts.map +1 -1
- package/dist/adapters/cli/kimi.js +19 -1
- package/dist/adapters/cli/kimi.js.map +1 -1
- package/dist/adapters/cli/mojo.d.ts +28 -0
- package/dist/adapters/cli/mojo.d.ts.map +1 -0
- package/dist/adapters/cli/mojo.js +58 -0
- package/dist/adapters/cli/mojo.js.map +1 -0
- package/dist/adapters/cli/pi.d.ts +6 -4
- package/dist/adapters/cli/pi.d.ts.map +1 -1
- package/dist/adapters/cli/pi.js +6 -4
- package/dist/adapters/cli/pi.js.map +1 -1
- package/dist/adapters/cli/read-isolation.d.ts +251 -1
- package/dist/adapters/cli/read-isolation.d.ts.map +1 -1
- package/dist/adapters/cli/read-isolation.js +254 -1
- package/dist/adapters/cli/read-isolation.js.map +1 -1
- package/dist/adapters/cli/registry.d.ts +13 -1
- package/dist/adapters/cli/registry.d.ts.map +1 -1
- package/dist/adapters/cli/registry.js +19 -1
- package/dist/adapters/cli/registry.js.map +1 -1
- package/dist/adapters/cli/runner-input.d.ts +3 -3
- package/dist/adapters/cli/runner-input.d.ts.map +1 -1
- package/dist/adapters/cli/runner-input.js +4 -3
- package/dist/adapters/cli/runner-input.js.map +1 -1
- package/dist/adapters/cli/shared-hints.d.ts +3 -1
- package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
- package/dist/adapters/cli/shared-hints.js +22 -6
- package/dist/adapters/cli/shared-hints.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +29 -4
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/bot-registry.d.ts +20 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +30 -4
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli/arg-utils.d.ts +5 -0
- package/dist/cli/arg-utils.d.ts.map +1 -1
- package/dist/cli/arg-utils.js +7 -0
- package/dist/cli/arg-utils.js.map +1 -1
- package/dist/cli/bots-list-output.d.ts +130 -2
- package/dist/cli/bots-list-output.d.ts.map +1 -1
- package/dist/cli/bots-list-output.js +173 -27
- package/dist/cli/bots-list-output.js.map +1 -1
- package/dist/cli/daemon-lifecycle-env.d.ts +10 -3
- package/dist/cli/daemon-lifecycle-env.d.ts.map +1 -1
- package/dist/cli/daemon-lifecycle-env.js +38 -11
- package/dist/cli/daemon-lifecycle-env.js.map +1 -1
- package/dist/cli/dashboard-command.d.ts +15 -0
- package/dist/cli/dashboard-command.d.ts.map +1 -1
- package/dist/cli/dashboard-command.js +22 -0
- package/dist/cli/dashboard-command.js.map +1 -1
- package/dist/cli/log-tail.d.ts +54 -0
- package/dist/cli/log-tail.d.ts.map +1 -0
- package/dist/cli/log-tail.js +167 -0
- package/dist/cli/log-tail.js.map +1 -0
- package/dist/cli/pm2-env.d.ts +16 -0
- package/dist/cli/pm2-env.d.ts.map +1 -0
- package/dist/cli/pm2-env.js +63 -0
- package/dist/cli/pm2-env.js.map +1 -0
- package/dist/cli/pm2-fleet-lock.d.ts +26 -0
- package/dist/cli/pm2-fleet-lock.d.ts.map +1 -0
- package/dist/cli/pm2-fleet-lock.js +50 -0
- package/dist/cli/pm2-fleet-lock.js.map +1 -0
- package/dist/cli/pm2-god-retirement.d.ts +59 -0
- package/dist/cli/pm2-god-retirement.d.ts.map +1 -0
- package/dist/cli/pm2-god-retirement.js +95 -0
- package/dist/cli/pm2-god-retirement.js.map +1 -0
- package/dist/cli/session-list-wake.d.ts +41 -0
- package/dist/cli/session-list-wake.d.ts.map +1 -0
- package/dist/cli/session-list-wake.js +102 -0
- package/dist/cli/session-list-wake.js.map +1 -0
- package/dist/cli/workflow.d.ts.map +1 -1
- package/dist/cli/workflow.js +26 -0
- package/dist/cli/workflow.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +933 -115
- package/dist/cli.js.map +1 -1
- package/dist/codex-app-runner.js +261 -49
- package/dist/codex-app-runner.js.map +1 -1
- package/dist/core/cli-runtime-update.d.ts +8 -0
- package/dist/core/cli-runtime-update.d.ts.map +1 -1
- package/dist/core/cli-runtime-update.js +127 -8
- package/dist/core/cli-runtime-update.js.map +1 -1
- package/dist/core/close-residual.d.ts +64 -0
- package/dist/core/close-residual.d.ts.map +1 -0
- package/dist/core/close-residual.js +93 -0
- package/dist/core/close-residual.js.map +1 -0
- package/dist/core/closed-session-card.d.ts.map +1 -1
- package/dist/core/closed-session-card.js +11 -3
- package/dist/core/closed-session-card.js.map +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +129 -27
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/cost-calculator.d.ts +3 -1
- package/dist/core/cost-calculator.d.ts.map +1 -1
- package/dist/core/cost-calculator.js +230 -1
- package/dist/core/cost-calculator.js.map +1 -1
- package/dist/core/daemon-background-close.d.ts +19 -0
- package/dist/core/daemon-background-close.d.ts.map +1 -0
- package/dist/core/daemon-background-close.js +39 -0
- package/dist/core/daemon-background-close.js.map +1 -0
- package/dist/core/dashboard-command/overview.d.ts +4 -0
- package/dist/core/dashboard-command/overview.d.ts.map +1 -1
- package/dist/core/dashboard-command/overview.js +8 -1
- package/dist/core/dashboard-command/overview.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +373 -41
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/dashboard-rows.d.ts +4 -0
- package/dist/core/dashboard-rows.d.ts.map +1 -1
- package/dist/core/dashboard-rows.js +3 -0
- package/dist/core/dashboard-rows.js.map +1 -1
- package/dist/core/dashboard-url.d.ts +37 -0
- package/dist/core/dashboard-url.d.ts.map +1 -1
- package/dist/core/dashboard-url.js +71 -0
- package/dist/core/dashboard-url.js.map +1 -1
- package/dist/core/deferred-schedule-settlement.d.ts +15 -1
- package/dist/core/deferred-schedule-settlement.d.ts.map +1 -1
- package/dist/core/deferred-schedule-settlement.js +4 -1
- package/dist/core/deferred-schedule-settlement.js.map +1 -1
- package/dist/core/device-isolation-daemon.d.ts +91 -1
- package/dist/core/device-isolation-daemon.d.ts.map +1 -1
- package/dist/core/device-isolation-daemon.js +346 -2
- package/dist/core/device-isolation-daemon.js.map +1 -1
- package/dist/core/local-terminal-opener.d.ts.map +1 -1
- package/dist/core/local-terminal-opener.js +5 -1
- package/dist/core/local-terminal-opener.js.map +1 -1
- package/dist/core/loopback-target.d.ts +14 -0
- package/dist/core/loopback-target.d.ts.map +1 -0
- package/dist/core/loopback-target.js +20 -0
- package/dist/core/loopback-target.js.map +1 -0
- package/dist/core/maintenance.d.ts.map +1 -1
- package/dist/core/maintenance.js +16 -2
- package/dist/core/maintenance.js.map +1 -1
- package/dist/core/mojo-containment-command.d.ts +20 -0
- package/dist/core/mojo-containment-command.d.ts.map +1 -0
- package/dist/core/mojo-containment-command.js +266 -0
- package/dist/core/mojo-containment-command.js.map +1 -0
- package/dist/core/mojo-containment.d.ts +575 -0
- package/dist/core/mojo-containment.d.ts.map +1 -0
- package/dist/core/mojo-containment.js +1158 -0
- package/dist/core/mojo-containment.js.map +1 -0
- package/dist/core/mojo-launcher-env-quarantine.d.ts +30 -0
- package/dist/core/mojo-launcher-env-quarantine.d.ts.map +1 -0
- package/dist/core/mojo-launcher-env-quarantine.js +175 -0
- package/dist/core/mojo-launcher-env-quarantine.js.map +1 -0
- package/dist/core/mojo-session-identity.d.ts +3 -0
- package/dist/core/mojo-session-identity.d.ts.map +1 -0
- package/dist/core/mojo-session-identity.js +126 -0
- package/dist/core/mojo-session-identity.js.map +1 -0
- package/dist/core/persistent-backend.d.ts +32 -8
- package/dist/core/persistent-backend.d.ts.map +1 -1
- package/dist/core/persistent-backend.js +55 -17
- package/dist/core/persistent-backend.js.map +1 -1
- package/dist/core/plugins/mcp/gateway.d.ts +14 -1
- package/dist/core/plugins/mcp/gateway.d.ts.map +1 -1
- package/dist/core/plugins/mcp/gateway.js +70 -3
- package/dist/core/plugins/mcp/gateway.js.map +1 -1
- package/dist/core/plugins/mcp/host.d.ts +2 -0
- package/dist/core/plugins/mcp/host.d.ts.map +1 -1
- package/dist/core/plugins/mcp/host.js +2 -0
- package/dist/core/plugins/mcp/host.js.map +1 -1
- package/dist/core/plugins/pm2.d.ts.map +1 -1
- package/dist/core/plugins/pm2.js +16 -3
- package/dist/core/plugins/pm2.js.map +1 -1
- package/dist/core/plugins/service-manager.d.ts +9 -0
- package/dist/core/plugins/service-manager.d.ts.map +1 -1
- package/dist/core/plugins/service-manager.js +33 -16
- package/dist/core/plugins/service-manager.js.map +1 -1
- package/dist/core/preview-port-owner.d.ts +109 -0
- package/dist/core/preview-port-owner.d.ts.map +1 -0
- package/dist/core/preview-port-owner.js +320 -0
- package/dist/core/preview-port-owner.js.map +1 -0
- package/dist/core/remote-cli-ids.d.ts +5 -0
- package/dist/core/remote-cli-ids.d.ts.map +1 -0
- package/dist/core/remote-cli-ids.js +24 -0
- package/dist/core/remote-cli-ids.js.map +1 -0
- package/dist/core/{riff-shutdown-detach.d.ts → remote-shutdown-detach.d.ts} +40 -39
- package/dist/core/remote-shutdown-detach.d.ts.map +1 -0
- package/dist/core/{riff-shutdown-detach.js → remote-shutdown-detach.js} +85 -83
- package/dist/core/remote-shutdown-detach.js.map +1 -0
- package/dist/core/{riff-worker-shutdown-readiness.d.ts → remote-worker-shutdown-readiness.d.ts} +5 -4
- package/dist/core/remote-worker-shutdown-readiness.d.ts.map +1 -0
- package/dist/core/{riff-worker-shutdown-readiness.js → remote-worker-shutdown-readiness.js} +4 -3
- package/dist/core/remote-worker-shutdown-readiness.js.map +1 -0
- package/dist/core/reply-target.d.ts +16 -1
- package/dist/core/reply-target.d.ts.map +1 -1
- package/dist/core/reply-target.js +61 -0
- package/dist/core/reply-target.js.map +1 -1
- package/dist/core/session-group-birth.d.ts +26 -0
- package/dist/core/session-group-birth.d.ts.map +1 -1
- package/dist/core/session-group-birth.js +81 -4
- package/dist/core/session-group-birth.js.map +1 -1
- package/dist/core/session-manager.d.ts +15 -2
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +259 -5
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-model.d.ts +46 -0
- package/dist/core/session-model.d.ts.map +1 -0
- package/dist/core/session-model.js +42 -0
- package/dist/core/session-model.js.map +1 -0
- package/dist/core/session-owner-reminder.d.ts +63 -0
- package/dist/core/session-owner-reminder.d.ts.map +1 -0
- package/dist/core/session-owner-reminder.js +196 -0
- package/dist/core/session-owner-reminder.js.map +1 -0
- package/dist/core/session-preview-registry.d.ts +53 -0
- package/dist/core/session-preview-registry.d.ts.map +1 -0
- package/dist/core/session-preview-registry.js +82 -0
- package/dist/core/session-preview-registry.js.map +1 -0
- package/dist/core/session-preview.d.ts +114 -0
- package/dist/core/session-preview.d.ts.map +1 -0
- package/dist/core/session-preview.js +184 -0
- package/dist/core/session-preview.js.map +1 -0
- package/dist/core/session-rename.d.ts.map +1 -1
- package/dist/core/session-rename.js +5 -3
- package/dist/core/session-rename.js.map +1 -1
- package/dist/core/session-wake-deadline.d.ts +5 -0
- package/dist/core/session-wake-deadline.d.ts.map +1 -0
- package/dist/core/session-wake-deadline.js +13 -0
- package/dist/core/session-wake-deadline.js.map +1 -0
- package/dist/core/shutdown-budgets.d.ts +7 -6
- package/dist/core/shutdown-budgets.d.ts.map +1 -1
- package/dist/core/shutdown-budgets.js +11 -10
- package/dist/core/shutdown-budgets.js.map +1 -1
- package/dist/core/terminal-control-grant.d.ts +74 -0
- package/dist/core/terminal-control-grant.d.ts.map +1 -0
- package/dist/core/terminal-control-grant.js +183 -0
- package/dist/core/terminal-control-grant.js.map +1 -0
- package/dist/core/terminal-proxy.d.ts.map +1 -1
- package/dist/core/terminal-proxy.js +4 -5
- package/dist/core/terminal-proxy.js.map +1 -1
- package/dist/core/terminal-write-auth.d.ts +4 -11
- package/dist/core/terminal-write-auth.d.ts.map +1 -1
- package/dist/core/terminal-write-auth.js +19 -16
- package/dist/core/terminal-write-auth.js.map +1 -1
- package/dist/core/trigger-session.d.ts.map +1 -1
- package/dist/core/trigger-session.js +36 -29
- package/dist/core/trigger-session.js.map +1 -1
- package/dist/core/types.d.ts +88 -10
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +9 -7
- package/dist/core/types.js.map +1 -1
- package/dist/core/workbench-link.d.ts +81 -0
- package/dist/core/workbench-link.d.ts.map +1 -0
- package/dist/core/workbench-link.js +109 -0
- package/dist/core/workbench-link.js.map +1 -0
- package/dist/core/worker-pool.d.ts +312 -11
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +2494 -209
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts +6 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +443 -106
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/aggregator.d.ts +33 -3
- package/dist/dashboard/aggregator.d.ts.map +1 -1
- package/dist/dashboard/aggregator.js +93 -17
- package/dist/dashboard/aggregator.js.map +1 -1
- package/dist/dashboard/auth-session-connections.d.ts +59 -0
- package/dist/dashboard/auth-session-connections.d.ts.map +1 -0
- package/dist/dashboard/auth-session-connections.js +109 -0
- package/dist/dashboard/auth-session-connections.js.map +1 -0
- package/dist/dashboard/auth.d.ts +99 -1
- package/dist/dashboard/auth.d.ts.map +1 -1
- package/dist/dashboard/auth.js +176 -64
- package/dist/dashboard/auth.js.map +1 -1
- package/dist/dashboard/bot-payload.d.ts +1 -0
- package/dist/dashboard/bot-payload.d.ts.map +1 -1
- package/dist/dashboard/bot-payload.js +3 -0
- package/dist/dashboard/bot-payload.js.map +1 -1
- package/dist/dashboard/control-audit.d.ts +112 -0
- package/dist/dashboard/control-audit.d.ts.map +1 -0
- package/dist/dashboard/control-audit.js +195 -0
- package/dist/dashboard/control-audit.js.map +1 -0
- package/dist/dashboard/control-csrf.d.ts +123 -0
- package/dist/dashboard/control-csrf.d.ts.map +1 -0
- package/dist/dashboard/control-csrf.js +392 -0
- package/dist/dashboard/control-csrf.js.map +1 -0
- package/dist/dashboard/daemon-reconcile.d.ts +26 -0
- package/dist/dashboard/daemon-reconcile.d.ts.map +1 -0
- package/dist/dashboard/daemon-reconcile.js +46 -0
- package/dist/dashboard/daemon-reconcile.js.map +1 -0
- package/dist/dashboard/debug-terminal.d.ts.map +1 -1
- package/dist/dashboard/debug-terminal.js +18 -4
- package/dist/dashboard/debug-terminal.js.map +1 -1
- package/dist/dashboard/events-sse.d.ts +60 -0
- package/dist/dashboard/events-sse.d.ts.map +1 -0
- package/dist/dashboard/events-sse.js +63 -0
- package/dist/dashboard/events-sse.js.map +1 -0
- package/dist/dashboard/h5-auth.d.ts +256 -0
- package/dist/dashboard/h5-auth.d.ts.map +1 -0
- package/dist/dashboard/h5-auth.js +825 -0
- package/dist/dashboard/h5-auth.js.map +1 -0
- package/dist/dashboard/managed-spawn.d.ts +36 -0
- package/dist/dashboard/managed-spawn.d.ts.map +1 -0
- package/dist/dashboard/managed-spawn.js +169 -0
- package/dist/dashboard/managed-spawn.js.map +1 -0
- package/dist/dashboard/preview-content-capability.d.ts +41 -0
- package/dist/dashboard/preview-content-capability.d.ts.map +1 -0
- package/dist/dashboard/preview-content-capability.js +127 -0
- package/dist/dashboard/preview-content-capability.js.map +1 -0
- package/dist/dashboard/preview-contract.d.ts +85 -0
- package/dist/dashboard/preview-contract.d.ts.map +1 -0
- package/dist/dashboard/preview-contract.js +198 -0
- package/dist/dashboard/preview-contract.js.map +1 -0
- package/dist/dashboard/preview-guard-page.d.ts +52 -0
- package/dist/dashboard/preview-guard-page.d.ts.map +1 -0
- package/dist/dashboard/preview-guard-page.js +162 -0
- package/dist/dashboard/preview-guard-page.js.map +1 -0
- package/dist/dashboard/preview-interaction.d.ts +57 -0
- package/dist/dashboard/preview-interaction.d.ts.map +1 -0
- package/dist/dashboard/preview-interaction.js +198 -0
- package/dist/dashboard/preview-interaction.js.map +1 -0
- package/dist/dashboard/preview-proxy.d.ts +113 -0
- package/dist/dashboard/preview-proxy.d.ts.map +1 -0
- package/dist/dashboard/preview-proxy.js +543 -0
- package/dist/dashboard/preview-proxy.js.map +1 -0
- package/dist/dashboard/public-redact.d.ts +23 -0
- package/dist/dashboard/public-redact.d.ts.map +1 -1
- package/dist/dashboard/public-redact.js +90 -21
- package/dist/dashboard/public-redact.js.map +1 -1
- package/dist/dashboard/request-identity.d.ts +77 -0
- package/dist/dashboard/request-identity.d.ts.map +1 -0
- package/dist/dashboard/request-identity.js +72 -0
- package/dist/dashboard/request-identity.js.map +1 -0
- package/dist/dashboard/session-cleanup.d.ts +5 -0
- package/dist/dashboard/session-cleanup.d.ts.map +1 -1
- package/dist/dashboard/session-cleanup.js +4 -0
- package/dist/dashboard/session-cleanup.js.map +1 -1
- package/dist/dashboard/settings-write-applier.d.ts +5 -1
- package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
- package/dist/dashboard/settings-write-applier.js +14 -0
- package/dist/dashboard/settings-write-applier.js.map +1 -1
- package/dist/dashboard/standing-link.d.ts +76 -0
- package/dist/dashboard/standing-link.d.ts.map +1 -0
- package/dist/dashboard/standing-link.js +111 -0
- package/dist/dashboard/standing-link.js.map +1 -0
- package/dist/dashboard/terminal-control.d.ts +105 -0
- package/dist/dashboard/terminal-control.d.ts.map +1 -0
- package/dist/dashboard/terminal-control.js +296 -0
- package/dist/dashboard/terminal-control.js.map +1 -0
- package/dist/dashboard/terminal-front-proxy.d.ts +55 -0
- package/dist/dashboard/terminal-front-proxy.d.ts.map +1 -0
- package/dist/dashboard/terminal-front-proxy.js +397 -0
- package/dist/dashboard/terminal-front-proxy.js.map +1 -0
- package/dist/dashboard/terminal-view-capability.d.ts +97 -0
- package/dist/dashboard/terminal-view-capability.d.ts.map +1 -0
- package/dist/dashboard/terminal-view-capability.js +145 -0
- package/dist/dashboard/terminal-view-capability.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-api.d.ts +59 -0
- package/dist/dashboard/web/agent-workbench-api.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-api.js +227 -0
- package/dist/dashboard/web/agent-workbench-api.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-appearance-menu.d.ts +59 -0
- package/dist/dashboard/web/agent-workbench-appearance-menu.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-appearance-menu.js +195 -0
- package/dist/dashboard/web/agent-workbench-appearance-menu.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-appearance.d.ts +222 -0
- package/dist/dashboard/web/agent-workbench-appearance.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-appearance.js +499 -0
- package/dist/dashboard/web/agent-workbench-appearance.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-capabilities.d.ts +13 -0
- package/dist/dashboard/web/agent-workbench-capabilities.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-capabilities.js +24 -0
- package/dist/dashboard/web/agent-workbench-capabilities.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-chat.d.ts +90 -0
- package/dist/dashboard/web/agent-workbench-chat.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-chat.js +168 -0
- package/dist/dashboard/web/agent-workbench-chat.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-dock-page.d.ts +3 -0
- package/dist/dashboard/web/agent-workbench-dock-page.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-dock-page.js +23 -0
- package/dist/dashboard/web/agent-workbench-dock-page.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-dock-view.d.ts +22 -0
- package/dist/dashboard/web/agent-workbench-dock-view.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-dock-view.js +76 -0
- package/dist/dashboard/web/agent-workbench-dock-view.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-model.d.ts +196 -0
- package/dist/dashboard/web/agent-workbench-model.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-model.js +529 -0
- package/dist/dashboard/web/agent-workbench-model.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-page.d.ts +3 -0
- package/dist/dashboard/web/agent-workbench-page.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-page.js +27 -0
- package/dist/dashboard/web/agent-workbench-page.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-panes.d.ts +49 -0
- package/dist/dashboard/web/agent-workbench-panes.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-panes.js +468 -0
- package/dist/dashboard/web/agent-workbench-panes.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-session-list.d.ts +42 -0
- package/dist/dashboard/web/agent-workbench-session-list.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-session-list.js +353 -0
- package/dist/dashboard/web/agent-workbench-session-list.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-storage.d.ts +49 -0
- package/dist/dashboard/web/agent-workbench-storage.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-storage.js +197 -0
- package/dist/dashboard/web/agent-workbench-storage.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-touch.d.ts +46 -0
- package/dist/dashboard/web/agent-workbench-touch.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-touch.js +96 -0
- package/dist/dashboard/web/agent-workbench-touch.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-view.d.ts +34 -0
- package/dist/dashboard/web/agent-workbench-view.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-view.js +439 -0
- package/dist/dashboard/web/agent-workbench-view.js.map +1 -0
- package/dist/dashboard/web/app.d.ts.map +1 -1
- package/dist/dashboard/web/app.js +63 -2
- package/dist/dashboard/web/app.js.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.d.ts +11 -0
- package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.js +267 -36
- package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts +6 -0
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/control-csrf.d.ts +4 -0
- package/dist/dashboard/web/control-csrf.d.ts.map +1 -0
- package/dist/dashboard/web/control-csrf.js +26 -0
- package/dist/dashboard/web/control-csrf.js.map +1 -0
- package/dist/dashboard/web/dashboard-routes.d.ts.map +1 -1
- package/dist/dashboard/web/dashboard-routes.js +2 -0
- package/dist/dashboard/web/dashboard-routes.js.map +1 -1
- package/dist/dashboard/web/groups-page.d.ts.map +1 -1
- package/dist/dashboard/web/groups-page.js +11 -2
- package/dist/dashboard/web/groups-page.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +132 -6
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/overview-page.d.ts.map +1 -1
- package/dist/dashboard/web/overview-page.js +3 -2
- package/dist/dashboard/web/overview-page.js.map +1 -1
- package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
- package/dist/dashboard/web/sessions-page.js +41 -1
- package/dist/dashboard/web/sessions-page.js.map +1 -1
- package/dist/dashboard/web/sessions.d.ts.map +1 -1
- package/dist/dashboard/web/sessions.js +5 -1
- package/dist/dashboard/web/sessions.js.map +1 -1
- package/dist/dashboard/web/settings-page.d.ts +3 -0
- package/dist/dashboard/web/settings-page.d.ts.map +1 -1
- package/dist/dashboard/web/settings-page.js +56 -10
- package/dist/dashboard/web/settings-page.js.map +1 -1
- package/dist/dashboard/web/store.d.ts +27 -2
- package/dist/dashboard/web/store.d.ts.map +1 -1
- package/dist/dashboard/web/store.js +43 -4
- package/dist/dashboard/web/store.js.map +1 -1
- package/dist/dashboard/web/ui.d.ts +3 -0
- package/dist/dashboard/web/ui.d.ts.map +1 -1
- package/dist/dashboard/web/ui.js +11 -0
- package/dist/dashboard/web/ui.js.map +1 -1
- package/dist/dashboard/web/update-action.d.ts +12 -0
- package/dist/dashboard/web/update-action.d.ts.map +1 -1
- package/dist/dashboard/web/update-action.js +11 -0
- package/dist/dashboard/web/update-action.js.map +1 -1
- package/dist/dashboard/workbench-doctor.d.ts +114 -0
- package/dist/dashboard/workbench-doctor.d.ts.map +1 -0
- package/dist/dashboard/workbench-doctor.js +620 -0
- package/dist/dashboard/workbench-doctor.js.map +1 -0
- package/dist/dashboard/workbench-ticket.d.ts +165 -0
- package/dist/dashboard/workbench-ticket.d.ts.map +1 -0
- package/dist/dashboard/workbench-ticket.js +495 -0
- package/dist/dashboard/workbench-ticket.js.map +1 -0
- package/dist/dashboard-web/app.js +1 -1
- package/dist/dashboard-web/chunks/agent-workbench-dock-page-IKAC5CFL.js +1 -0
- package/dist/dashboard-web/chunks/agent-workbench-page-M53UN5EB.js +1 -0
- package/dist/dashboard-web/chunks/bot-defaults-page-YKE5SDZF.js +10 -0
- package/dist/dashboard-web/chunks/chunk-22WIADUT.js +2 -0
- package/dist/dashboard-web/chunks/chunk-24ONYQMR.js +1 -0
- package/dist/dashboard-web/chunks/{chunk-Z7B3E3US.js → chunk-2W6SCN3M.js} +1 -1
- package/dist/dashboard-web/chunks/chunk-4XNJX6B3.js +1 -0
- package/dist/dashboard-web/chunks/{chunk-VMWNR3SV.js → chunk-F6KNPF4R.js} +11 -9
- package/dist/dashboard-web/chunks/chunk-FXW3V4JR.js +1 -0
- package/dist/dashboard-web/chunks/chunk-G5ZS2YUS.js +1 -0
- package/dist/dashboard-web/chunks/chunk-KTQ7B6ZR.js +1 -0
- package/dist/dashboard-web/chunks/{chunk-Z7DO42GJ.js → chunk-NACP7BMR.js} +1 -1
- package/dist/dashboard-web/chunks/{connectors-page-CPH6VVFY.js → connectors-page-VOISXXII.js} +1 -1
- package/dist/dashboard-web/chunks/{feedback-page-J2FKQ3LK.js → feedback-page-RN6HRZMS.js} +1 -1
- package/dist/dashboard-web/chunks/groups-page-UQT6DCB7.js +8 -0
- package/dist/dashboard-web/chunks/{insights-page-A2LWTKJD.js → insights-page-HJFDYR5N.js} +3 -3
- package/dist/dashboard-web/chunks/{monitor-room-2YR752E3.js → monitor-room-V7PCP47V.js} +1 -1
- package/dist/dashboard-web/chunks/{monitoring-page-BGYSKZGM.js → monitoring-page-NKEKVQAP.js} +1 -1
- package/dist/dashboard-web/chunks/{office-page-ZZHTEAE4.js → office-page-GVJF73XW.js} +1 -1
- package/dist/dashboard-web/chunks/overview-page-KHLV6VTI.js +1 -0
- package/dist/dashboard-web/chunks/{plugin-page-VEGTCEAS.js → plugin-page-J3OF5IY4.js} +1 -1
- package/dist/dashboard-web/chunks/{roles-page-YUC4YLJG.js → roles-page-WQIRSOLZ.js} +1 -1
- package/dist/dashboard-web/chunks/{schedules-page-2L2NKTDJ.js → schedules-page-J2DEY2X6.js} +1 -1
- package/dist/dashboard-web/chunks/sessions-page-7BQFEA5P.js +8 -0
- package/dist/dashboard-web/chunks/settings-page-BJEIOFJH.js +2 -0
- package/dist/dashboard-web/chunks/{skills-page-DQPBBI3T.js → skills-page-HGIFR3IU.js} +1 -1
- package/dist/dashboard-web/chunks/{team-federation-page-LQKLDV4K.js → team-federation-page-MR5ZPL4B.js} +1 -1
- package/dist/dashboard-web/chunks/{v3-page-RVKQIMAP.js → v3-page-GBR5JWPE.js} +2 -2
- package/dist/dashboard-web/chunks/{whiteboards-page-S5K55HRZ.js → whiteboards-page-QAZM4BN3.js} +1 -1
- package/dist/dashboard-web/index.html +8 -1
- package/dist/dashboard-web/style.css +2006 -10
- package/dist/dashboard.js +1006 -263
- package/dist/dashboard.js.map +1 -1
- package/dist/global-config.d.ts +30 -0
- package/dist/global-config.d.ts.map +1 -1
- package/dist/global-config.js +33 -0
- package/dist/global-config.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +31 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +28 -1
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts +1 -1
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +22 -6
- package/dist/im/lark/card-builder.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +118 -24
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/client.d.ts +15 -0
- package/dist/im/lark/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +30 -0
- package/dist/im/lark/client.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts +48 -0
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +133 -22
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/identity-cache.d.ts +14 -6
- package/dist/im/lark/identity-cache.d.ts.map +1 -1
- package/dist/im/lark/identity-cache.js +69 -26
- package/dist/im/lark/identity-cache.js.map +1 -1
- package/dist/im/lark/lark-hosts.d.ts +13 -0
- package/dist/im/lark/lark-hosts.d.ts.map +1 -1
- package/dist/im/lark/lark-hosts.js +16 -0
- package/dist/im/lark/lark-hosts.js.map +1 -1
- package/dist/im/lark/overview-card.d.ts +22 -0
- package/dist/im/lark/overview-card.d.ts.map +1 -1
- package/dist/im/lark/overview-card.js +53 -3
- package/dist/im/lark/overview-card.js.map +1 -1
- package/dist/im/lark/sessions-card.d.ts.map +1 -1
- package/dist/im/lark/sessions-card.js +26 -3
- package/dist/im/lark/sessions-card.js.map +1 -1
- package/dist/im/lark/workflow-slash-command.d.ts +6 -0
- package/dist/im/lark/workflow-slash-command.d.ts.map +1 -1
- package/dist/im/lark/workflow-slash-command.js +7 -0
- package/dist/im/lark/workflow-slash-command.js.map +1 -1
- package/dist/index-daemon.js +27 -4
- package/dist/index-daemon.js.map +1 -1
- package/dist/index-dashboard.d.ts +3 -0
- package/dist/index-dashboard.d.ts.map +1 -0
- package/dist/index-dashboard.js +94 -0
- package/dist/index-dashboard.js.map +1 -0
- package/dist/platform/team-agents-client.d.ts +199 -0
- package/dist/platform/team-agents-client.d.ts.map +1 -0
- package/dist/platform/team-agents-client.js +263 -0
- package/dist/platform/team-agents-client.js.map +1 -0
- package/dist/platform/tunnel-client.d.ts +6 -0
- package/dist/platform/tunnel-client.d.ts.map +1 -1
- package/dist/platform/tunnel-client.js.map +1 -1
- package/dist/services/async-trigger-state.d.ts +7 -3
- package/dist/services/async-trigger-state.d.ts.map +1 -1
- package/dist/services/async-trigger-state.js +28 -0
- package/dist/services/async-trigger-state.js.map +1 -1
- package/dist/services/async-trigger-store.d.ts +13 -4
- package/dist/services/async-trigger-store.d.ts.map +1 -1
- package/dist/services/async-trigger-store.js +55 -8
- package/dist/services/async-trigger-store.js.map +1 -1
- package/dist/services/bot-config-store.d.ts +1 -11
- package/dist/services/bot-config-store.d.ts.map +1 -1
- package/dist/services/bot-config-store.js +27 -3
- package/dist/services/bot-config-store.js.map +1 -1
- package/dist/services/bot-profile-store.d.ts +17 -2
- package/dist/services/bot-profile-store.d.ts.map +1 -1
- package/dist/services/bot-profile-store.js +93 -12
- package/dist/services/bot-profile-store.js.map +1 -1
- package/dist/services/bridge-turn-queue.d.ts +10 -1
- package/dist/services/bridge-turn-queue.d.ts.map +1 -1
- package/dist/services/bridge-turn-queue.js +29 -9
- package/dist/services/bridge-turn-queue.js.map +1 -1
- package/dist/services/claude-transcript.d.ts +19 -0
- package/dist/services/claude-transcript.d.ts.map +1 -1
- package/dist/services/claude-transcript.js +57 -5
- package/dist/services/claude-transcript.js.map +1 -1
- package/dist/services/codex-app-threads.d.ts +2 -0
- package/dist/services/codex-app-threads.d.ts.map +1 -1
- package/dist/services/codex-app-threads.js +1 -1
- package/dist/services/codex-app-threads.js.map +1 -1
- package/dist/services/codex-reasoning-effort.d.ts +8 -0
- package/dist/services/codex-reasoning-effort.d.ts.map +1 -1
- package/dist/services/codex-reasoning-effort.js +24 -0
- package/dist/services/codex-reasoning-effort.js.map +1 -1
- package/dist/services/cursor-resume-policy.d.ts.map +1 -1
- package/dist/services/cursor-resume-policy.js +15 -3
- package/dist/services/cursor-resume-policy.js.map +1 -1
- package/dist/services/open-platform-rename.d.ts.map +1 -1
- package/dist/services/open-platform-rename.js +3 -2
- package/dist/services/open-platform-rename.js.map +1 -1
- package/dist/services/ordinary-turn-recovery.d.ts +76 -0
- package/dist/services/ordinary-turn-recovery.d.ts.map +1 -0
- package/dist/services/ordinary-turn-recovery.js +349 -0
- package/dist/services/ordinary-turn-recovery.js.map +1 -0
- package/dist/services/resume-fresh-policy.d.ts +17 -0
- package/dist/services/resume-fresh-policy.d.ts.map +1 -0
- package/dist/services/resume-fresh-policy.js +24 -0
- package/dist/services/resume-fresh-policy.js.map +1 -0
- package/dist/services/session-group-title.d.ts +1 -0
- package/dist/services/session-group-title.d.ts.map +1 -1
- package/dist/services/session-group-title.js +16 -1
- package/dist/services/session-group-title.js.map +1 -1
- package/dist/services/session-groups-store.d.ts +30 -0
- package/dist/services/session-groups-store.d.ts.map +1 -1
- package/dist/services/session-groups-store.js +26 -4
- package/dist/services/session-groups-store.js.map +1 -1
- package/dist/services/session-owner-notification.d.ts +9 -0
- package/dist/services/session-owner-notification.d.ts.map +1 -0
- package/dist/services/session-owner-notification.js +10 -0
- package/dist/services/session-owner-notification.js.map +1 -0
- package/dist/services/session-owner-reminder-config-store.d.ts +9 -0
- package/dist/services/session-owner-reminder-config-store.d.ts.map +1 -0
- package/dist/services/session-owner-reminder-config-store.js +24 -0
- package/dist/services/session-owner-reminder-config-store.js.map +1 -0
- package/dist/services/session-owner-reminder-store.d.ts +5 -0
- package/dist/services/session-owner-reminder-store.d.ts.map +1 -0
- package/dist/services/session-owner-reminder-store.js +54 -0
- package/dist/services/session-owner-reminder-store.js.map +1 -0
- package/dist/services/session-store.d.ts +98 -21
- package/dist/services/session-store.d.ts.map +1 -1
- package/dist/services/session-store.js +406 -48
- package/dist/services/session-store.js.map +1 -1
- package/dist/services/structured-bridge-clis.d.ts +9 -5
- package/dist/services/structured-bridge-clis.d.ts.map +1 -1
- package/dist/services/structured-bridge-clis.js +9 -4
- package/dist/services/structured-bridge-clis.js.map +1 -1
- package/dist/services/transcript-resolver.d.ts +1 -1
- package/dist/services/transcript-resolver.d.ts.map +1 -1
- package/dist/services/transcript-resolver.js +6 -1
- package/dist/services/transcript-resolver.js.map +1 -1
- package/dist/setup/bot-config-editor.d.ts.map +1 -1
- package/dist/setup/bot-config-editor.js +7 -2
- package/dist/setup/bot-config-editor.js.map +1 -1
- package/dist/setup/ensure-herdr-integrations.d.ts +13 -0
- package/dist/setup/ensure-herdr-integrations.d.ts.map +1 -1
- package/dist/setup/ensure-herdr-integrations.js +21 -3
- package/dist/setup/ensure-herdr-integrations.js.map +1 -1
- package/dist/setup/open-platform-automation.d.ts +1 -0
- package/dist/setup/open-platform-automation.d.ts.map +1 -1
- package/dist/setup/open-platform-automation.js +107 -7
- package/dist/setup/open-platform-automation.js.map +1 -1
- package/dist/skills/definitions.d.ts +17 -0
- package/dist/skills/definitions.d.ts.map +1 -1
- package/dist/skills/definitions.js +78 -8
- package/dist/skills/definitions.js.map +1 -1
- package/dist/skills/injection-mode.d.ts +24 -3
- package/dist/skills/injection-mode.d.ts.map +1 -1
- package/dist/skills/injection-mode.js +64 -15
- package/dist/skills/injection-mode.js.map +1 -1
- package/dist/skills/installer.d.ts +18 -0
- package/dist/skills/installer.d.ts.map +1 -1
- package/dist/skills/installer.js +45 -1
- package/dist/skills/installer.js.map +1 -1
- package/dist/types.d.ts +221 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/child-env.d.ts +92 -2
- package/dist/utils/child-env.d.ts.map +1 -1
- package/dist/utils/child-env.js +195 -1
- package/dist/utils/child-env.js.map +1 -1
- package/dist/utils/cli-usage-limit.d.ts +25 -14
- package/dist/utils/cli-usage-limit.d.ts.map +1 -1
- package/dist/utils/cli-usage-limit.js +25 -15
- package/dist/utils/cli-usage-limit.js.map +1 -1
- package/dist/utils/daemon-rejection-guard.d.ts +35 -0
- package/dist/utils/daemon-rejection-guard.d.ts.map +1 -0
- package/dist/utils/daemon-rejection-guard.js +101 -0
- package/dist/utils/daemon-rejection-guard.js.map +1 -0
- package/dist/utils/dashboard-env.d.ts +53 -0
- package/dist/utils/dashboard-env.d.ts.map +1 -0
- package/dist/utils/dashboard-env.js +116 -0
- package/dist/utils/dashboard-env.js.map +1 -0
- package/dist/utils/global-install.d.ts.map +1 -1
- package/dist/utils/global-install.js +61 -5
- package/dist/utils/global-install.js.map +1 -1
- package/dist/utils/install-diagnostics.d.ts +14 -7
- package/dist/utils/install-diagnostics.d.ts.map +1 -1
- package/dist/utils/install-diagnostics.js +20 -11
- package/dist/utils/install-diagnostics.js.map +1 -1
- package/dist/utils/local-dev-update.d.ts +132 -0
- package/dist/utils/local-dev-update.d.ts.map +1 -0
- package/dist/utils/local-dev-update.js +176 -0
- package/dist/utils/local-dev-update.js.map +1 -0
- package/dist/utils/pending-input-queue.d.ts +12 -1
- package/dist/utils/pending-input-queue.d.ts.map +1 -1
- package/dist/utils/pending-input-queue.js.map +1 -1
- package/dist/worker.js +1284 -225
- package/dist/worker.js.map +1 -1
- package/dist/workflows/definition.d.ts +68 -68
- package/dist/workflows/events/payloads.d.ts +14 -14
- package/dist/workflows/events/schema.d.ts +312 -312
- package/dist/workflows/migration/v2-run-archive-schema.d.ts +40 -40
- package/dist/workflows/v3/cli-run.d.ts.map +1 -1
- package/dist/workflows/v3/cli-run.js +7 -0
- package/dist/workflows/v3/cli-run.js.map +1 -1
- package/dist/workflows/v3/goal-cli.d.ts +3 -0
- package/dist/workflows/v3/goal-cli.d.ts.map +1 -1
- package/dist/workflows/v3/goal-cli.js +11 -0
- package/dist/workflows/v3/goal-cli.js.map +1 -1
- package/dist/workflows/v3/run-envelope.d.ts +144 -144
- package/package.json +3 -2
- package/dist/cli/pm2-god-admission.d.ts +0 -8
- package/dist/cli/pm2-god-admission.d.ts.map +0 -1
- package/dist/cli/pm2-god-admission.js +0 -25
- package/dist/cli/pm2-god-admission.js.map +0 -1
- package/dist/core/riff-shutdown-detach.d.ts.map +0 -1
- package/dist/core/riff-shutdown-detach.js.map +0 -1
- package/dist/core/riff-worker-shutdown-readiness.d.ts.map +0 -1
- package/dist/core/riff-worker-shutdown-readiness.js.map +0 -1
- package/dist/dashboard-web/chunks/bot-defaults-page-65SMZ4QY.js +0 -10
- package/dist/dashboard-web/chunks/chunk-G5XGBCM6.js +0 -1
- package/dist/dashboard-web/chunks/chunk-IKN6WURK.js +0 -1
- package/dist/dashboard-web/chunks/chunk-WLIVBPJC.js +0 -1
- package/dist/dashboard-web/chunks/groups-page-VDOOLS4M.js +0 -8
- package/dist/dashboard-web/chunks/overview-page-7NYPKQSB.js +0 -1
- package/dist/dashboard-web/chunks/sessions-page-DYZZMKFL.js +0 -8
- package/dist/dashboard-web/chunks/settings-page-LIULUSMH.js +0 -2
|
@@ -0,0 +1,1158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A containment handle: the thing that still identifies a mojo turn's process
|
|
3
|
+
* subtree AFTER the backend object that spawned it is gone.
|
|
4
|
+
*
|
|
5
|
+
* Why this module exists
|
|
6
|
+
* ---------------------
|
|
7
|
+
* mojo-process-tree.ts can enumerate a subtree, but only while someone still
|
|
8
|
+
* remembers two facts: the root pid, and the env nonce that was injected into it.
|
|
9
|
+
* Both used to live in per-instance MojoBackend fields (`lastTurnPid`, a
|
|
10
|
+
* `readonly treeNonce` freshly randomised in the field initialiser). Every worker
|
|
11
|
+
* generation therefore got a BRAND NEW nonce and a null root pid, which means a
|
|
12
|
+
* subtree left behind by the previous generation became permanently
|
|
13
|
+
* unenumerable — and `terminateChildProven()` reads "no root pid" as "no subtree
|
|
14
|
+
* exists" and returns true. A credentialed survivor thus turned into a
|
|
15
|
+
* successful close, the row was published `closed`, and the device-isolation
|
|
16
|
+
* blocker vanished (mergePersistedDeviceIsolationSessions filters closed rows).
|
|
17
|
+
*
|
|
18
|
+
* So the handle has to outlive the backend, the worker generation and the daemon
|
|
19
|
+
* process. That is what this module provides, plus the ONE rule that makes it
|
|
20
|
+
* safe:
|
|
21
|
+
*
|
|
22
|
+
* a handle may only be released when quiescence is PROVEN.
|
|
23
|
+
*
|
|
24
|
+
* Two strengths of handle
|
|
25
|
+
* -----------------------
|
|
26
|
+
* STRONG (`cgroup`) — a per-session cgroup v2 directory. Membership survives
|
|
27
|
+
* setsid and is the right tool to KILL a whole turn subtree (cgroup.kill) and
|
|
28
|
+
* to ENUMERATE it. But `cgroup.procs` being empty is NOT a boundary proof:
|
|
29
|
+
* mojo runs at the daemon's UID, and cgroup v2 lets a same-UID process migrate
|
|
30
|
+
* ITSELF out of the leaf (to the parent slice or a sibling) within the
|
|
31
|
+
* delegation domain — a move the leaf-down read cannot see. So a cgroup handle
|
|
32
|
+
* is treated like a weak one for RELEASE purposes (emptiness stops signalling
|
|
33
|
+
* and lets the row close, but the blocker is retained); only a per-session UID
|
|
34
|
+
* or namespace the process cannot leave would make emptiness a real proof.
|
|
35
|
+
*
|
|
36
|
+
* WEAK (`tree-identity`) — a persisted (rootPid, bootId, startTime, nonce)
|
|
37
|
+
* record for hosts with no usable cgroup v2 (this includes cgroup-v1-only
|
|
38
|
+
* hosts and Darwin). It is EVIDENCE THAT A TREE MAY STILL EXIST, never proof
|
|
39
|
+
* that one is gone. `bootId` + `startTime` exist so that pid REUSE cannot be
|
|
40
|
+
* mistaken for the original tree (and, in the one genuinely provable
|
|
41
|
+
* direction, so that a reboot can be recognised as having killed it).
|
|
42
|
+
*
|
|
43
|
+
* Fail-closed, in the same direction as the rest of the isolation path
|
|
44
|
+
* -------------------------------------------------------------------
|
|
45
|
+
* Unreadable state, an unsupported platform, a timeout, or any "cannot tell"
|
|
46
|
+
* answer resolves to NOT PROVEN, so the caller keeps the blocker. This matches
|
|
47
|
+
* the scanner's fail-closed contract (a read error fails the whole scan) and the
|
|
48
|
+
* destroy contract's `local-unproven` staying fenced. `mojo-launcher-env-quarantine`
|
|
49
|
+
* deliberately shipped with no clearing API because "proving that needs
|
|
50
|
+
* trustworthy termination of the whole mojo process group ... which does not
|
|
51
|
+
* exist yet"; `proveContainmentQuiescent` is that missing mechanism, which is why
|
|
52
|
+
* releasing is allowed here and only against a proven verdict.
|
|
53
|
+
*/
|
|
54
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
55
|
+
import { randomBytes } from 'node:crypto';
|
|
56
|
+
import { dirname, join } from 'node:path';
|
|
57
|
+
import { config } from '../config.js';
|
|
58
|
+
import { logger } from '../utils/logger.js';
|
|
59
|
+
import { withFileLockSync } from '../utils/file-lock.js';
|
|
60
|
+
const FILE_NAME = 'mojo-containment-handles.json';
|
|
61
|
+
/** Where per-session cgroups are created when the host supports cgroup v2. */
|
|
62
|
+
const CGROUP_ROOT = '/sys/fs/cgroup';
|
|
63
|
+
const CGROUP_SLICE = 'botmux.slice';
|
|
64
|
+
/**
|
|
65
|
+
* Every pid enrolled ANYWHERE in a cgroup subtree — the leaf plus every
|
|
66
|
+
* descendant cgroup, read recursively.
|
|
67
|
+
*
|
|
68
|
+
* A single-level `cgroup.procs` read is not a containment proof: a same-uid
|
|
69
|
+
* process can `mkdir` a child cgroup under a leaf it owns and migrate itself
|
|
70
|
+
* into it, after which the leaf's own `cgroup.procs` reads empty while the
|
|
71
|
+
* process runs on in the nested group. Reading the whole subtree closes that
|
|
72
|
+
* escape. `{ok:false}` on any unreadable level is the fail-closed signal —
|
|
73
|
+
* "cannot enumerate" must never collapse into "nobody there".
|
|
74
|
+
*
|
|
75
|
+
* NOTE ON SCOPE: this proves quiescence of the subtree we OWN. It does not, and
|
|
76
|
+
* cannot, cover a same-uid adversary that migrates a process OUT to a sibling or
|
|
77
|
+
* ancestor cgroup — cgroups are not a security boundary against a peer uid, and
|
|
78
|
+
* only per-session uid isolation would close that. `prepareContainmentBoundary`
|
|
79
|
+
* nests the boundary under the daemon's own (delegated) cgroup so a non-root
|
|
80
|
+
* delegated daemon at least blocks UP-migration; the residual sibling case is
|
|
81
|
+
* held by device-isolation staying fail-closed (the blocker is dropped only on
|
|
82
|
+
* a proof, never inferred).
|
|
83
|
+
*/
|
|
84
|
+
function cgroupSubtreePids(dir) {
|
|
85
|
+
let pids;
|
|
86
|
+
try {
|
|
87
|
+
pids = readFileSync(`${dir}/cgroup.procs`, 'utf8')
|
|
88
|
+
.split('\n').map(l => Number(l.trim())).filter(n => Number.isInteger(n) && n > 0);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
// ENOENT at a subtree root means that cgroup is genuinely gone.
|
|
92
|
+
if (err.code === 'ENOENT')
|
|
93
|
+
return { ok: true, pids: [] };
|
|
94
|
+
return { ok: false, reason: `cannot read ${dir}/cgroup.procs (${err.code ?? 'unknown'})` };
|
|
95
|
+
}
|
|
96
|
+
let entries;
|
|
97
|
+
try {
|
|
98
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
if (err.code === 'ENOENT')
|
|
102
|
+
return { ok: true, pids };
|
|
103
|
+
return { ok: false, reason: `cannot enumerate ${dir} (${err.code ?? 'unknown'})` };
|
|
104
|
+
}
|
|
105
|
+
for (const entry of entries) {
|
|
106
|
+
if (!entry.isDirectory())
|
|
107
|
+
continue;
|
|
108
|
+
const child = cgroupSubtreePids(join(dir, entry.name));
|
|
109
|
+
if (!child.ok)
|
|
110
|
+
return child;
|
|
111
|
+
pids.push(...child.pids);
|
|
112
|
+
}
|
|
113
|
+
return { ok: true, pids };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The cgroup the daemon itself lives in (from /proc/self/cgroup), used as the
|
|
117
|
+
* PARENT of per-session boundaries.
|
|
118
|
+
*
|
|
119
|
+
* Nesting under the daemon's own cgroup keeps the per-session boundaries tidy
|
|
120
|
+
* and co-located with the daemon. It does NOT make the cgroup a security
|
|
121
|
+
* boundary: within a single delegation domain a same-UID process can still
|
|
122
|
+
* migrate itself between cgroups (up to a writable ancestor, or into a sibling),
|
|
123
|
+
* which is exactly why cgroup emptiness is no longer a release proof (see
|
|
124
|
+
* containmentReleaseDecision). Falls back to the flat `botmux.slice` at the root
|
|
125
|
+
* when /proc/self/cgroup is unreadable or names the root — the caller's `mkdir`
|
|
126
|
+
* still degrades to a weak handle if neither works.
|
|
127
|
+
*/
|
|
128
|
+
function resolveSliceParent(cgroupRoot, procRoot = '/proc') {
|
|
129
|
+
try {
|
|
130
|
+
const rel = readFileSync(`${procRoot}/self/cgroup`, 'utf8')
|
|
131
|
+
.split('\n').find(l => l.startsWith('0::'))?.slice(3).trim();
|
|
132
|
+
if (rel && rel !== '/' && rel.startsWith('/')) {
|
|
133
|
+
const candidate = join(cgroupRoot, rel, CGROUP_SLICE);
|
|
134
|
+
// The daemon's cgroup dir must exist and be a real cgroup for nesting
|
|
135
|
+
// to mean anything; otherwise fall back.
|
|
136
|
+
if (existsSync(join(cgroupRoot, rel, 'cgroup.procs')))
|
|
137
|
+
return candidate;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch { /* fall through to the flat root slice */ }
|
|
141
|
+
return join(cgroupRoot, CGROUP_SLICE);
|
|
142
|
+
}
|
|
143
|
+
/** Thrown instead of degrading to an empty (fail-open) handle store. */
|
|
144
|
+
export class MojoContainmentUnavailableError extends Error {
|
|
145
|
+
cause;
|
|
146
|
+
constructor(message, cause) {
|
|
147
|
+
super(message);
|
|
148
|
+
this.cause = cause;
|
|
149
|
+
this.name = 'MojoContainmentUnavailableError';
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// ── host facts ───────────────────────────────────────────────────────────────
|
|
153
|
+
/**
|
|
154
|
+
* Boot identity of the running kernel.
|
|
155
|
+
*
|
|
156
|
+
* Returns null when it cannot be read (non-Linux, or a locked-down /proc).
|
|
157
|
+
* Callers must treat null as "cannot mint a trustworthy weak handle", NOT as a
|
|
158
|
+
* blank value to store: a handle whose bootId is empty would compare equal
|
|
159
|
+
* across reboots and across hosts, resurrecting exactly the pid-reuse confusion
|
|
160
|
+
* the field exists to prevent.
|
|
161
|
+
*/
|
|
162
|
+
export function readBootId(opts = {}) {
|
|
163
|
+
const procRoot = opts.procRoot ?? '/proc';
|
|
164
|
+
try {
|
|
165
|
+
const id = readFileSync(`${procRoot}/sys/kernel/random/boot_id`, 'utf8').trim();
|
|
166
|
+
return id.length > 0 ? id : null;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* `/proc/<pid>/stat` field 22 (starttime, in clock ticks since boot).
|
|
174
|
+
*
|
|
175
|
+
* null means the pid is not currently live (or is unreadable), which is why the
|
|
176
|
+
* caller may never read null as "the tree is gone": the ROOT exiting says
|
|
177
|
+
* nothing about a descendant that called setsid().
|
|
178
|
+
*
|
|
179
|
+
* Field indexing has the same comm hazard as the scanner: field 2 is
|
|
180
|
+
* parenthesised and may contain spaces or ')', so the split starts after the LAST
|
|
181
|
+
* ')'. From there, index 0 is state (field 3), hence starttime (field 22) sits at
|
|
182
|
+
* index 19.
|
|
183
|
+
*/
|
|
184
|
+
export function readProcStartTime(pid, opts = {}) {
|
|
185
|
+
const procRoot = opts.procRoot ?? '/proc';
|
|
186
|
+
let text;
|
|
187
|
+
try {
|
|
188
|
+
text = readFileSync(`${procRoot}/${pid}/stat`, 'utf8');
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
const close = text.lastIndexOf(')');
|
|
194
|
+
if (close < 0)
|
|
195
|
+
return null;
|
|
196
|
+
const rest = text.slice(close + 1).trim().split(/\s+/);
|
|
197
|
+
const started = Number(rest[19]);
|
|
198
|
+
return Number.isInteger(started) ? started : null;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Is a usable cgroup v2 hierarchy mounted?
|
|
202
|
+
*
|
|
203
|
+
* `cgroup.controllers` exists only on the v2 unified hierarchy, so its presence
|
|
204
|
+
* is the cheap discriminator against a v1-only host (where per-session
|
|
205
|
+
* containment via this module is not available).
|
|
206
|
+
*/
|
|
207
|
+
export function cgroupV2Available(opts = {}) {
|
|
208
|
+
const root = opts.cgroupRoot ?? CGROUP_ROOT;
|
|
209
|
+
try {
|
|
210
|
+
readFileSync(`${root}/cgroup.controllers`, 'utf8');
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
export function readProcLiveness(pid, opts = {}) {
|
|
218
|
+
const procRoot = opts.procRoot ?? '/proc';
|
|
219
|
+
let text;
|
|
220
|
+
try {
|
|
221
|
+
text = readFileSync(`${procRoot}/${pid}/stat`, 'utf8');
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
return err.code === 'ENOENT' ? 'gone' : 'unreadable';
|
|
225
|
+
}
|
|
226
|
+
// Same comm hazard as everywhere else: field 2 is parenthesised and may
|
|
227
|
+
// contain spaces or ')', so cut at the LAST ')'. After that, index 0 is
|
|
228
|
+
// field 3 (state).
|
|
229
|
+
const close = text.lastIndexOf(')');
|
|
230
|
+
if (close < 0)
|
|
231
|
+
return 'unreadable';
|
|
232
|
+
const state = text.slice(close + 1).trim().split(/\s+/)[0];
|
|
233
|
+
if (!state)
|
|
234
|
+
return 'unreadable';
|
|
235
|
+
return state === 'Z' ? 'zombie' : 'running';
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Create the cgroup directory for a turn about to be spawned.
|
|
239
|
+
*
|
|
240
|
+
* Returns null when the host cannot support a strong boundary (no cgroup v2, or
|
|
241
|
+
* the slice is not writable) — the caller falls back to the weak post-spawn
|
|
242
|
+
* handle, which never claims boundary proof.
|
|
243
|
+
*/
|
|
244
|
+
export function prepareContainmentBoundary(input, opts = {}) {
|
|
245
|
+
const cgroupRoot = opts.cgroupRoot ?? CGROUP_ROOT;
|
|
246
|
+
if (!cgroupV2Available({ cgroupRoot }))
|
|
247
|
+
return null;
|
|
248
|
+
// Nest under the daemon's own (delegated) cgroup so a non-root delegated
|
|
249
|
+
// daemon's boundaries have a root-owned parent — blocking a same-uid child
|
|
250
|
+
// from migrating UP and out. Falls back to the flat root slice.
|
|
251
|
+
const sliceParent = resolveSliceParent(cgroupRoot, opts.procRoot);
|
|
252
|
+
// A stable, collision-free name: two generations of the same session must
|
|
253
|
+
// not share a directory, or releasing one would release the other's proof.
|
|
254
|
+
const dir = join(sliceParent, `mojo-${sanitizeForPath(input.sessionId)}-g${input.generation}-${randomBytes(4).toString('hex')}`);
|
|
255
|
+
// Two attempts: transient resource errno (system-wide file-table pressure —
|
|
256
|
+
// ENFILE/EMFILE — or an interrupted syscall) must not silently cost this turn
|
|
257
|
+
// its STRONG boundary when the very next open would succeed. Anything
|
|
258
|
+
// persistent (undelegated slice, read-only /sys) fails both attempts
|
|
259
|
+
// identically and degrades below.
|
|
260
|
+
let lastErr;
|
|
261
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
262
|
+
try {
|
|
263
|
+
mkdirSync(dir, { recursive: true });
|
|
264
|
+
// Enrolment is the CHILD's job (pre-exec), but it must be possible at
|
|
265
|
+
// all: an unwritable cgroup.procs would make every spawn die on the shim
|
|
266
|
+
// handshake. Probe writability now — an empty write is a no-op for the
|
|
267
|
+
// kernel — so an undelegated slice degrades to the weak handle here
|
|
268
|
+
// instead of failing every turn later.
|
|
269
|
+
writeFileSync(`${dir}/cgroup.procs`, '');
|
|
270
|
+
return { sessionId: input.sessionId, generation: input.generation, cgroupPath: dir, nonce: input.nonce };
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
lastErr = err;
|
|
274
|
+
try {
|
|
275
|
+
rmdirSync(dir);
|
|
276
|
+
}
|
|
277
|
+
catch { /* best-effort */ }
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// Delegation not granted / read-only /sys / persistent resource exhaustion —
|
|
281
|
+
// the caller falls back to the weak handle rather than pretending containment
|
|
282
|
+
// was established. Loudly: this downgrades the session's containment evidence
|
|
283
|
+
// for the whole turn (its close can only ever be `closed_with_residual`), and
|
|
284
|
+
// a silent downgrade here is indistinguishable from "host has no cgroup v2"
|
|
285
|
+
// when someone later asks why a strong-capable host produced weak handles.
|
|
286
|
+
logger.warn(`[mojo] session ${input.sessionId}: could not prepare a strong cgroup boundary under `
|
|
287
|
+
+ `${sliceParent} (${lastErr?.code ?? String(lastErr)}); `
|
|
288
|
+
+ 'this turn degrades to the weak tree-identity handle');
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
/** Mint the strong handle for a boundary the child has enrolled itself into. */
|
|
292
|
+
export function strongHandleFromPreparedBoundary(prepared, opts = {}) {
|
|
293
|
+
const bootId = readBootId({ procRoot: opts.procRoot });
|
|
294
|
+
return {
|
|
295
|
+
kind: 'cgroup',
|
|
296
|
+
sessionId: prepared.sessionId,
|
|
297
|
+
generation: prepared.generation,
|
|
298
|
+
cgroupPath: prepared.cgroupPath,
|
|
299
|
+
nonce: prepared.nonce,
|
|
300
|
+
// Stamp the boot id so a later reboot can prove the tree gone (see the
|
|
301
|
+
// cgroup branch of proveContainmentQuiescent). Omitted only if /proc is
|
|
302
|
+
// unreadable, in which case the handle behaves like a legacy one.
|
|
303
|
+
...(bootId !== null ? { bootId } : {}),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Kill every process currently enrolled in a prepared boundary (cgroup.kill),
|
|
308
|
+
* then remove the directory if that emptied it.
|
|
309
|
+
*
|
|
310
|
+
* For the caller who spawned a child through the enrolment shim and then FAILED
|
|
311
|
+
* to record the handle durably: the subtree must not be left running behind a
|
|
312
|
+
* blocker nobody recorded. Returns true only when the boundary is provably
|
|
313
|
+
* empty (or already gone) afterwards — false means the caller must keep its
|
|
314
|
+
* own fence up.
|
|
315
|
+
*/
|
|
316
|
+
export async function killPreparedBoundary(prepared) {
|
|
317
|
+
// The ONLY accepted proof of emptiness is a successful rmdir: the kernel
|
|
318
|
+
// refuses to remove a cgroup with members, and once removed no late enroller
|
|
319
|
+
// can enter (its `cgroup.procs` write fails, so the shim exits without
|
|
320
|
+
// exec'ing). Reading `cgroup.procs` and seeing it empty is NOT enough — a
|
|
321
|
+
// shim mid-handshake could enrol between the read and the rmdir, and a
|
|
322
|
+
// freshly SIGKILLed member lingers as a listed zombie until reaped.
|
|
323
|
+
//
|
|
324
|
+
// The attempts are SPACED (~50ms): `cgroup.kill` SIGKILLs the members, but a
|
|
325
|
+
// setsid descendant that reparents to init lingers as a listed zombie until
|
|
326
|
+
// init reaps it, so a zero-backoff loop would usually exhaust its tries
|
|
327
|
+
// against a corpse and latch the wedge. Descendant cgroups are killed too —
|
|
328
|
+
// cgroup.kill is recursive, but a nested cgroup dir must still be rmdir'd
|
|
329
|
+
// before the parent, which the depth-first sweep below handles.
|
|
330
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
331
|
+
if (attempt > 0)
|
|
332
|
+
await new Promise(r => { setTimeout(r, 50).unref?.(); });
|
|
333
|
+
try {
|
|
334
|
+
writeFileSync(`${prepared.cgroupPath}/cgroup.kill`, '1\n');
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
if (err.code === 'ENOENT')
|
|
338
|
+
return true;
|
|
339
|
+
// Unwritable kill knob (or a synthetic boundary in tests): fall
|
|
340
|
+
// through — the rmdir below is still the authoritative proof.
|
|
341
|
+
}
|
|
342
|
+
if (rmdirCgroupTree(prepared.cgroupPath))
|
|
343
|
+
return true;
|
|
344
|
+
// EBUSY / ENOTEMPTY: members still live (or racing in) — kill again.
|
|
345
|
+
}
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Depth-first rmdir of a cgroup subtree: child cgroups must be removed before
|
|
350
|
+
* their parent. Returns true when the whole tree is gone.
|
|
351
|
+
*
|
|
352
|
+
* On a SYNTHETIC (plain-directory) boundary the knob writes leave `cgroup.procs`
|
|
353
|
+
* / `cgroup.kill` files behind, which a plain rmdir refuses; unlinking them is a
|
|
354
|
+
* no-op on real cgroupfs (EPERM, swallowed) and restores rmdir's meaning in
|
|
355
|
+
* tests. Only the two known knob names are unlinked — never arbitrary content —
|
|
356
|
+
* so a real member file can't be spirited away.
|
|
357
|
+
*/
|
|
358
|
+
function rmdirCgroupTree(dir) {
|
|
359
|
+
let entries;
|
|
360
|
+
try {
|
|
361
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
362
|
+
}
|
|
363
|
+
catch (err) {
|
|
364
|
+
return err.code === 'ENOENT';
|
|
365
|
+
}
|
|
366
|
+
for (const entry of entries) {
|
|
367
|
+
if (entry.isDirectory() && !rmdirCgroupTree(join(dir, entry.name)))
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
for (const knob of ['cgroup.procs', 'cgroup.kill']) {
|
|
371
|
+
try {
|
|
372
|
+
unlinkSync(`${dir}/${knob}`);
|
|
373
|
+
}
|
|
374
|
+
catch { /* cgroupfs refuses; fine */ }
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
rmdirSync(dir);
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
return err.code === 'ENOENT';
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Mint the strongest POST-SPAWN handle this host can describe a turn with.
|
|
386
|
+
*
|
|
387
|
+
* Deliberately NEVER a cgroup handle: migrating an already-running root into a
|
|
388
|
+
* cgroup does not capture descendants it forked before the write, so a strong
|
|
389
|
+
* handle minted here would claim a boundary that provably has a hole (the P0
|
|
390
|
+
* this module was rewritten for). Strong handles exist only via
|
|
391
|
+
* prepareContainmentBoundary + the pre-exec enrolment shim. This function is
|
|
392
|
+
* the fallback for hosts (or spawn paths) where that was not possible, and the
|
|
393
|
+
* handles it mints never carry boundary proof.
|
|
394
|
+
*/
|
|
395
|
+
export function acquireContainmentHandle(input, opts = {}) {
|
|
396
|
+
const bootId = readBootId({ procRoot: opts.procRoot });
|
|
397
|
+
const startTime = bootId === null
|
|
398
|
+
? null
|
|
399
|
+
: readProcStartTime(input.rootPid, { procRoot: opts.procRoot });
|
|
400
|
+
if (bootId === null || startTime === null) {
|
|
401
|
+
// NOT null. Returning null here meant the caller recorded nothing and
|
|
402
|
+
// `hasUnprovenContainment()` answered false, so the blocker was dropped on
|
|
403
|
+
// precisely the hosts that can never prove anything (see
|
|
404
|
+
// UnprovableContainmentHandle).
|
|
405
|
+
return {
|
|
406
|
+
kind: 'unprovable',
|
|
407
|
+
sessionId: input.sessionId,
|
|
408
|
+
generation: input.generation,
|
|
409
|
+
nonce: input.nonce,
|
|
410
|
+
platform: opts.platform ?? process.platform,
|
|
411
|
+
reason: bootId === null
|
|
412
|
+
? 'no cgroup v2 delegation and no readable boot id'
|
|
413
|
+
: `no cgroup v2 delegation and pid ${input.rootPid} has no readable starttime`,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
kind: 'tree-identity',
|
|
418
|
+
sessionId: input.sessionId,
|
|
419
|
+
generation: input.generation,
|
|
420
|
+
rootPid: input.rootPid,
|
|
421
|
+
bootId,
|
|
422
|
+
startTime,
|
|
423
|
+
nonce: input.nonce,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
function sanitizeForPath(value) {
|
|
427
|
+
return value.replace(/[^A-Za-z0-9_.-]/g, '_');
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Can we prove that nothing this handle owns is still executing?
|
|
431
|
+
*
|
|
432
|
+
* STRONG handle: `cgroup.procs` is authoritative. An absent directory also counts
|
|
433
|
+
* as proof, because the kernel refuses `rmdir` on a non-empty cgroup and this
|
|
434
|
+
* module only removes one after a proven verdict — so "gone" can only mean
|
|
435
|
+
* "was empty when it went". Any OTHER read error is a non-proof.
|
|
436
|
+
*
|
|
437
|
+
* WEAK handle:
|
|
438
|
+
* - a bootId mismatch is genuine, cheap proof: the recorded tree cannot have
|
|
439
|
+
* survived the reboot that changed the id;
|
|
440
|
+
* - otherwise the only available evidence is a /proc scan, which the caller
|
|
441
|
+
* must supply. No scan, or a failed scan, or any surviving pid → not proven.
|
|
442
|
+
* - the root pid being gone is explicitly NOT accepted on its own: a descendant
|
|
443
|
+
* that called setsid() outlives its parent, which is the whole reason the
|
|
444
|
+
* scanner unions three signals.
|
|
445
|
+
*
|
|
446
|
+
* A clean weak verdict is the best this host can do, not an unforgeable
|
|
447
|
+
* boundary — see the trust-domain note in mojo-process-tree. Callers that need
|
|
448
|
+
* certainty need a strong handle.
|
|
449
|
+
*/
|
|
450
|
+
export function proveContainmentQuiescent(handle, opts = {}) {
|
|
451
|
+
if (handle.kind === 'cgroup') {
|
|
452
|
+
// A REBOOT is the one thing that provably kills the whole tree, including
|
|
453
|
+
// the same-UID sibling/parent migrant that cgroup emptiness cannot see.
|
|
454
|
+
// Check it FIRST: after a reboot the cgroup directory is gone, so the
|
|
455
|
+
// subtree read below would report `cgroup-empty` — which is NOT a boundary
|
|
456
|
+
// proof (round-8) and would leave the handle unreleasable forever. A
|
|
457
|
+
// changed boot id turns that into the real `boot-id-changed` proof, so a
|
|
458
|
+
// reboot actually clears a cgroup handle (round-9 P2-1). A handle minted
|
|
459
|
+
// before this field existed has no bootId and simply falls through.
|
|
460
|
+
if (handle.bootId) {
|
|
461
|
+
const currentBootId = readBootId({ procRoot: opts.procRoot });
|
|
462
|
+
if (currentBootId !== null && currentBootId !== handle.bootId) {
|
|
463
|
+
return { proven: true, handle, evidence: 'boot-id-changed' };
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
// Read the WHOLE subtree, not just the leaf: a same-uid process can mkdir
|
|
467
|
+
// a child cgroup and migrate into it, which a single-level read would miss
|
|
468
|
+
// (it would see the leaf empty and call the tree gone).
|
|
469
|
+
const subtree = cgroupSubtreePids(handle.cgroupPath);
|
|
470
|
+
if (!subtree.ok) {
|
|
471
|
+
return { proven: false, handle, reason: subtree.reason };
|
|
472
|
+
}
|
|
473
|
+
const pids = subtree.pids;
|
|
474
|
+
if (pids.length === 0)
|
|
475
|
+
return { proven: true, handle, evidence: 'cgroup-empty' };
|
|
476
|
+
// A non-empty cgroup is NOT automatically a live tree. `cgroup.procs`
|
|
477
|
+
// keeps listing a zombie until its parent reaps it, and a zombie executes
|
|
478
|
+
// nothing and cannot use the injected credential. Treating it as alive
|
|
479
|
+
// would wedge the session permanently: the close is already allowed to
|
|
480
|
+
// succeed (the scanner reaches the same verdict), yet the blocker could
|
|
481
|
+
// never clear and `rmdir` on the cgroup would fail forever, leaving an
|
|
482
|
+
// unexplainable stuck blocker and a leaked cgroup directory.
|
|
483
|
+
//
|
|
484
|
+
// Fail-closed is preserved in the direction that matters: only state 'Z'
|
|
485
|
+
// is discounted, an ENOENT means the pid genuinely left, and a state we
|
|
486
|
+
// cannot read counts as EXECUTING.
|
|
487
|
+
const executing = [];
|
|
488
|
+
const zombies = [];
|
|
489
|
+
for (const pid of pids) {
|
|
490
|
+
const liveness = readProcLiveness(pid, { procRoot: opts.procRoot });
|
|
491
|
+
if (liveness === 'gone')
|
|
492
|
+
continue; // raced us; not a member
|
|
493
|
+
if (liveness === 'zombie') {
|
|
494
|
+
zombies.push(pid);
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
executing.push(pid); // running OR unreadable
|
|
498
|
+
}
|
|
499
|
+
if (executing.length > 0) {
|
|
500
|
+
return { proven: false, handle, reason: 'cgroup still has executing members', residualPids: executing };
|
|
501
|
+
}
|
|
502
|
+
// Zombie-only (or everything raced away): nothing can execute, so this is
|
|
503
|
+
// genuine quiescence. Reported in the reason so an operator can see why a
|
|
504
|
+
// still-populated cgroup was accepted.
|
|
505
|
+
return zombies.length > 0
|
|
506
|
+
? {
|
|
507
|
+
proven: true,
|
|
508
|
+
handle,
|
|
509
|
+
reason: `zombie-only cgroup members (${zombies.join(',')})`,
|
|
510
|
+
evidence: 'cgroup-zombie-only',
|
|
511
|
+
}
|
|
512
|
+
: { proven: true, handle, evidence: 'cgroup-empty' };
|
|
513
|
+
}
|
|
514
|
+
if (handle.kind === 'unprovable') {
|
|
515
|
+
// By construction there is no evidence that could settle this. Returning
|
|
516
|
+
// proven:false unconditionally is what keeps the blocker, and because
|
|
517
|
+
// releaseContainmentHandle only accepts a proven verdict, such a handle can
|
|
518
|
+
// never be released — which is the intended terminal state.
|
|
519
|
+
return {
|
|
520
|
+
proven: false,
|
|
521
|
+
handle,
|
|
522
|
+
reason: `containment is unprovable on ${handle.platform}: ${handle.reason}`,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
const currentBootId = readBootId({ procRoot: opts.procRoot });
|
|
526
|
+
if (currentBootId === null) {
|
|
527
|
+
// We cannot even establish which boot we are on, so we cannot rule out
|
|
528
|
+
// that this pid is the original tree. Fail closed.
|
|
529
|
+
return { proven: false, handle, reason: 'boot id unreadable; cannot age out the recorded tree' };
|
|
530
|
+
}
|
|
531
|
+
if (currentBootId !== handle.bootId) {
|
|
532
|
+
return { proven: true, handle, evidence: 'boot-id-changed' };
|
|
533
|
+
}
|
|
534
|
+
if (!opts.scan) {
|
|
535
|
+
return {
|
|
536
|
+
proven: false,
|
|
537
|
+
handle,
|
|
538
|
+
reason: 'weak containment handle cannot prove quiescence without a subtree scan',
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
const evidence = opts.scan(handle);
|
|
542
|
+
if (!evidence.scanned) {
|
|
543
|
+
return {
|
|
544
|
+
proven: false,
|
|
545
|
+
handle,
|
|
546
|
+
reason: `subtree scan failed: ${evidence.reason ?? 'unknown'}`,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
if (evidence.pids.length > 0) {
|
|
550
|
+
return {
|
|
551
|
+
proven: false,
|
|
552
|
+
handle,
|
|
553
|
+
reason: 'subtree still has live members',
|
|
554
|
+
residualPids: [...evidence.pids],
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
// Clean scan. Labelled `scan-clean` on purpose, NOT a boundary proof: this is
|
|
558
|
+
// exactly the shape the reviewer reproduced with a setsid + scrubbed-environ
|
|
559
|
+
// + reparented descendant, which stays alive while the scan reports nothing.
|
|
560
|
+
return { proven: true, handle, evidence: 'scan-clean' };
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Is the recorded root pid still the ORIGINAL process?
|
|
564
|
+
*
|
|
565
|
+
* Used before signalling: a weak handle names a pid, and pids are reused. Sending
|
|
566
|
+
* SIGKILL to a recycled pid would kill an unrelated process, so a caller must
|
|
567
|
+
* confirm identity first. False therefore means "do not signal this pid", NOT
|
|
568
|
+
* "the tree is gone".
|
|
569
|
+
*/
|
|
570
|
+
export function weakHandleRootStillOriginal(handle, opts = {}) {
|
|
571
|
+
const bootId = readBootId({ procRoot: opts.procRoot });
|
|
572
|
+
if (bootId === null || bootId !== handle.bootId)
|
|
573
|
+
return false;
|
|
574
|
+
const startTime = readProcStartTime(handle.rootPid, { procRoot: opts.procRoot });
|
|
575
|
+
return startTime !== null && startTime === handle.startTime;
|
|
576
|
+
}
|
|
577
|
+
function filePath(dataDir) {
|
|
578
|
+
return join(dataDir ?? config.session.dataDir, FILE_NAME);
|
|
579
|
+
}
|
|
580
|
+
/** Stable identity of a handle, so union/removal cannot double-count or mis-hit. */
|
|
581
|
+
export function containmentHandleKey(handle) {
|
|
582
|
+
if (handle.kind === 'cgroup')
|
|
583
|
+
return `cgroup:${handle.cgroupPath}`;
|
|
584
|
+
if (handle.kind === 'unprovable')
|
|
585
|
+
return `unprovable:${handle.sessionId}:${handle.generation}`;
|
|
586
|
+
return `tree:${handle.bootId}:${handle.rootPid}:${handle.startTime}`;
|
|
587
|
+
}
|
|
588
|
+
function parseHandle(value, path, sessionId) {
|
|
589
|
+
const bad = (why) => {
|
|
590
|
+
throw new MojoContainmentUnavailableError(`mojo containment store at ${path} has ${why} for ${sessionId}; `
|
|
591
|
+
+ 'refusing to treat the session as contained');
|
|
592
|
+
};
|
|
593
|
+
if (!value || typeof value !== 'object')
|
|
594
|
+
return bad('a non-object handle');
|
|
595
|
+
const h = value;
|
|
596
|
+
const common = typeof h.sessionId === 'string' && h.sessionId.length > 0
|
|
597
|
+
&& typeof h.generation === 'number' && Number.isInteger(h.generation)
|
|
598
|
+
&& typeof h.nonce === 'string' && h.nonce.length > 0;
|
|
599
|
+
if (!common)
|
|
600
|
+
return bad('a handle with missing common fields');
|
|
601
|
+
if (h.kind === 'cgroup') {
|
|
602
|
+
if (typeof h.cgroupPath !== 'string' || h.cgroupPath.length === 0)
|
|
603
|
+
return bad('a cgroup handle with no path');
|
|
604
|
+
// bootId is OPTIONAL (legacy handles predate it), but if present it must be
|
|
605
|
+
// a non-empty string — a blank one would compare equal across reboots and
|
|
606
|
+
// resurrect the very pid/boot confusion the field exists to prevent.
|
|
607
|
+
if (h.bootId !== undefined && (typeof h.bootId !== 'string' || h.bootId.length === 0)) {
|
|
608
|
+
return bad('a cgroup handle with a malformed bootId');
|
|
609
|
+
}
|
|
610
|
+
return h;
|
|
611
|
+
}
|
|
612
|
+
if (h.kind === 'unprovable') {
|
|
613
|
+
if (typeof h.platform !== 'string' || h.platform.length === 0
|
|
614
|
+
|| typeof h.reason !== 'string' || h.reason.length === 0) {
|
|
615
|
+
return bad('a malformed unprovable handle');
|
|
616
|
+
}
|
|
617
|
+
return h;
|
|
618
|
+
}
|
|
619
|
+
if (h.kind === 'tree-identity') {
|
|
620
|
+
const ok = typeof h.rootPid === 'number' && Number.isInteger(h.rootPid) && h.rootPid > 0
|
|
621
|
+
&& typeof h.bootId === 'string' && h.bootId.length > 0
|
|
622
|
+
&& typeof h.startTime === 'number' && Number.isInteger(h.startTime);
|
|
623
|
+
if (!ok)
|
|
624
|
+
return bad('a malformed tree-identity handle');
|
|
625
|
+
return h;
|
|
626
|
+
}
|
|
627
|
+
return bad(`an unknown handle kind ${JSON.stringify(h.kind)}`);
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Read the store. No caching: a cached snapshot would hide another daemon's
|
|
631
|
+
* writes and could lose a recorded, still-unproven tree.
|
|
632
|
+
*
|
|
633
|
+
* A genuinely absent file is an empty store; anything else (EACCES, corrupt JSON,
|
|
634
|
+
* unknown version, malformed entry) THROWS, because "cannot read" must never
|
|
635
|
+
* become "nothing is contained" — that is the fail-open direction, and it is
|
|
636
|
+
* trivially arranged by the same-user process being policed.
|
|
637
|
+
*/
|
|
638
|
+
function readStrict(dataDir) {
|
|
639
|
+
const path = filePath(dataDir);
|
|
640
|
+
let raw;
|
|
641
|
+
try {
|
|
642
|
+
raw = readFileSync(path, 'utf8');
|
|
643
|
+
}
|
|
644
|
+
catch (err) {
|
|
645
|
+
if (err.code === 'ENOENT') {
|
|
646
|
+
return { version: 1, sessions: {} };
|
|
647
|
+
}
|
|
648
|
+
throw new MojoContainmentUnavailableError(`cannot read mojo containment store at ${path} `
|
|
649
|
+
+ `(${err.code ?? 'unknown'}); `
|
|
650
|
+
+ 'refusing to treat sessions as contained', err);
|
|
651
|
+
}
|
|
652
|
+
let parsed;
|
|
653
|
+
try {
|
|
654
|
+
parsed = JSON.parse(raw);
|
|
655
|
+
}
|
|
656
|
+
catch (err) {
|
|
657
|
+
throw new MojoContainmentUnavailableError(`mojo containment store at ${path} is corrupt; refusing to treat sessions as contained`, err);
|
|
658
|
+
}
|
|
659
|
+
const version = parsed?.version;
|
|
660
|
+
if (version !== 1) {
|
|
661
|
+
throw new MojoContainmentUnavailableError(`mojo containment store at ${path} has unsupported version ${JSON.stringify(version)}`);
|
|
662
|
+
}
|
|
663
|
+
const sessionsRaw = parsed?.sessions;
|
|
664
|
+
if (!parsed || typeof parsed !== 'object' || !sessionsRaw || typeof sessionsRaw !== 'object') {
|
|
665
|
+
throw new MojoContainmentUnavailableError(`mojo containment store at ${path} has an unexpected shape; refusing to treat sessions as contained`);
|
|
666
|
+
}
|
|
667
|
+
const sessions = {};
|
|
668
|
+
for (const [sessionId, list] of Object.entries(sessionsRaw)) {
|
|
669
|
+
if (!Array.isArray(list)) {
|
|
670
|
+
throw new MojoContainmentUnavailableError(`mojo containment store at ${path} has a non-array entry for ${sessionId}`);
|
|
671
|
+
}
|
|
672
|
+
// Rejecting the whole file rather than filtering: silently dropping one
|
|
673
|
+
// junk element would EMPTY that session's containment and unblock it.
|
|
674
|
+
sessions[sessionId] = list.map(item => parseHandle(item, path, sessionId));
|
|
675
|
+
}
|
|
676
|
+
return { version: 1, sessions };
|
|
677
|
+
}
|
|
678
|
+
/** Atomic replace via a UNIQUE temp file — a shared `.tmp` races between daemons. */
|
|
679
|
+
function writeStrict(data, dataDir) {
|
|
680
|
+
const path = filePath(dataDir);
|
|
681
|
+
const tmp = `${path}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
|
|
682
|
+
try {
|
|
683
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
684
|
+
writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600 });
|
|
685
|
+
renameSync(tmp, path);
|
|
686
|
+
}
|
|
687
|
+
catch (err) {
|
|
688
|
+
try {
|
|
689
|
+
if (existsSync(tmp))
|
|
690
|
+
unlinkSync(tmp);
|
|
691
|
+
}
|
|
692
|
+
catch { /* best-effort cleanup */ }
|
|
693
|
+
throw new MojoContainmentUnavailableError(`cannot persist mojo containment store at ${path}; an unproven subtree would be lost`, err);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Record a handle as OWNED AND UNPROVEN.
|
|
698
|
+
*
|
|
699
|
+
* Call this at spawn time, before the child can do anything: a crash between
|
|
700
|
+
* spawn and record is exactly the window that used to lose the tree entirely.
|
|
701
|
+
* Monotonic union by handle identity, so re-recording is idempotent and a later
|
|
702
|
+
* call can never retract an earlier one.
|
|
703
|
+
*
|
|
704
|
+
* THROWS on any read/write failure — the caller must not proceed believing the
|
|
705
|
+
* tree was recorded.
|
|
706
|
+
*/
|
|
707
|
+
export function recordContainmentHandle(handle, dataDir) {
|
|
708
|
+
const path = filePath(dataDir);
|
|
709
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
710
|
+
withFileLockSync(path, () => {
|
|
711
|
+
const data = readStrict(dataDir);
|
|
712
|
+
const before = data.sessions[handle.sessionId] ?? [];
|
|
713
|
+
const key = containmentHandleKey(handle);
|
|
714
|
+
if (before.some(h => containmentHandleKey(h) === key))
|
|
715
|
+
return;
|
|
716
|
+
data.sessions[handle.sessionId] = [...before, handle];
|
|
717
|
+
writeStrict(data, dataDir);
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Handles this session still owns. Empty ONLY when nothing is outstanding.
|
|
722
|
+
*
|
|
723
|
+
* THROWS when the store cannot be read, so isolation callers fail closed instead
|
|
724
|
+
* of reading an error as "clean".
|
|
725
|
+
*/
|
|
726
|
+
export function containmentHandles(sessionId, dataDir) {
|
|
727
|
+
return readStrict(dataDir).sessions[sessionId] ?? [];
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Session ids with an outstanding handle, INCLUDING sessions whose row is gone.
|
|
731
|
+
*
|
|
732
|
+
* The residual path needs this: an explicit `/close` deletes the row, so without
|
|
733
|
+
* it the inventory would lose every trace of an unproven credentialed subtree.
|
|
734
|
+
*/
|
|
735
|
+
export function containmentSessionIds(dataDir) {
|
|
736
|
+
return Object.keys(readStrict(dataDir).sessions);
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Does this session still have a tree we cannot prove is gone?
|
|
740
|
+
*
|
|
741
|
+
* This is the predicate the device-isolation blocker hangs off. It THROWS on an
|
|
742
|
+
* unreadable store rather than answering false.
|
|
743
|
+
*/
|
|
744
|
+
export function hasUnprovenContainment(sessionId, dataDir) {
|
|
745
|
+
return containmentHandles(sessionId, dataDir).length > 0;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Release every durable handle a REBOOT has provably killed, and report what
|
|
749
|
+
* stayed. Run once per daemon boot, BEFORE the device-isolation activation
|
|
750
|
+
* inventory is built.
|
|
751
|
+
*
|
|
752
|
+
* Why this exists (round-11 P1-1): `proveContainmentQuiescent` gained a
|
|
753
|
+
* boot-id-changed branch for cgroup handles, but its only production callers are
|
|
754
|
+
* live-worker teardown and workerless close — NEITHER runs for a session that is
|
|
755
|
+
* already `closed_with_residual` and exists only as a durable handle. So after a
|
|
756
|
+
* host reboot (which truly kills the whole tree, migrant included) that handle
|
|
757
|
+
* lingered forever and device isolation kept synthesising an `activation_blocked`
|
|
758
|
+
* from it — the new bootId proof had no consumer. This is that consumer: it
|
|
759
|
+
* enumerates the store and RELEASES only handles whose verdict is
|
|
760
|
+
* `boot-id-changed`. A same-boot handle, a legacy handle with no bootId, an
|
|
761
|
+
* unprovable handle, or a still-live cgroup are all RETAINED.
|
|
762
|
+
*
|
|
763
|
+
* Fail-CLOSED throughout: an unreadable store, an unreadable /proc, or a handle
|
|
764
|
+
* whose proof throws leaves that handle in place. Releasing on a "cannot tell"
|
|
765
|
+
* is exactly the unblock this whole module refuses.
|
|
766
|
+
*/
|
|
767
|
+
export function reconcileContainmentHandlesOnBoot(opts = {}) {
|
|
768
|
+
let ids;
|
|
769
|
+
try {
|
|
770
|
+
ids = containmentSessionIds(opts.dataDir);
|
|
771
|
+
}
|
|
772
|
+
catch (err) {
|
|
773
|
+
logger.error(`[mojo] boot reconciliation skipped: containment store unreadable (${String(err)}); `
|
|
774
|
+
+ 'every blocker is retained (fail-closed)');
|
|
775
|
+
return { released: 0, retained: 0, storeUnreadable: true };
|
|
776
|
+
}
|
|
777
|
+
let released = 0;
|
|
778
|
+
let retained = 0;
|
|
779
|
+
for (const sessionId of ids) {
|
|
780
|
+
let handles;
|
|
781
|
+
try {
|
|
782
|
+
handles = containmentHandles(sessionId, opts.dataDir);
|
|
783
|
+
}
|
|
784
|
+
catch {
|
|
785
|
+
retained++;
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
for (const handle of handles) {
|
|
789
|
+
let verdict;
|
|
790
|
+
try {
|
|
791
|
+
// No scan callback: we only ever act on `boot-id-changed`, which is
|
|
792
|
+
// decided from the stamped boot id alone. A weak scan-clean or a
|
|
793
|
+
// cgroup-empty verdict is deliberately NOT released here.
|
|
794
|
+
verdict = proveContainmentQuiescent(handle, opts.procRoot ? { procRoot: opts.procRoot } : {});
|
|
795
|
+
}
|
|
796
|
+
catch {
|
|
797
|
+
retained++;
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (verdict.proven && verdict.evidence === 'boot-id-changed') {
|
|
801
|
+
try {
|
|
802
|
+
releaseContainmentHandle(verdict, opts.dataDir);
|
|
803
|
+
released++;
|
|
804
|
+
}
|
|
805
|
+
catch {
|
|
806
|
+
retained++;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
retained++;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (released > 0) {
|
|
815
|
+
logger.info(`[mojo] boot reconciliation: released ${released} containment handle(s) a reboot proved gone, `
|
|
816
|
+
+ `retained ${retained}`);
|
|
817
|
+
}
|
|
818
|
+
return { released, retained, storeUnreadable: false };
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Is this verdict strong enough to FORGET the tree, and how much stays behind?
|
|
822
|
+
*
|
|
823
|
+
* This is the production gate on `boundaryProof`. It exists because a
|
|
824
|
+
* `proven: true` verdict is NOT one thing: on a cgroup host it is kernel state,
|
|
825
|
+
* on a plain Linux host it can be nothing more than "the scan saw nobody", and
|
|
826
|
+
* those two must not share a code path.
|
|
827
|
+
*
|
|
828
|
+
* Truth table, all of it pinned by unit tests:
|
|
829
|
+
*
|
|
830
|
+
* verdict evidence boundaryProof release residual
|
|
831
|
+
* ---------------- -------------------- ------------- ------- --------
|
|
832
|
+
* proven, weak boot-id-changed true yes null
|
|
833
|
+
* proven, cgroup boot-id-changed true yes null
|
|
834
|
+
* proven, cgroup cgroup-empty FALSE NO deviceIsolation
|
|
835
|
+
* proven, cgroup cgroup-zombie-only FALSE NO deviceIsolation
|
|
836
|
+
* proven, weak scan-clean FALSE NO deviceIsolation
|
|
837
|
+
* not proven n/a false NO deviceIsolation
|
|
838
|
+
*
|
|
839
|
+
* Only a reboot (boot-id-changed) authorises forgetting a tree — for BOTH handle
|
|
840
|
+
* kinds since the strong handle gained a stamped bootId (a reboot kills the
|
|
841
|
+
* whole tree, sibling-migrant included, and resets cgroupfs). A cgroup being
|
|
842
|
+
* empty is NOT a boundary proof — a same-UID process can migrate itself out of
|
|
843
|
+
* the leaf, invisible to the leaf-down read — so an emptiness verdict behaves
|
|
844
|
+
* exactly like a weak scan-clean: it lets the caller stop re-signalling
|
|
845
|
+
* (`signalsStopped: true`) and lets the SESSION close, but authorises nothing
|
|
846
|
+
* else. The handle stays in the durable store, so `hasUnprovenContainment` keeps
|
|
847
|
+
* answering true and the device-isolation blocker survives the close.
|
|
848
|
+
*/
|
|
849
|
+
export function containmentReleaseDecision(verdict) {
|
|
850
|
+
const handle = verdict.handle;
|
|
851
|
+
if (!verdict.proven) {
|
|
852
|
+
return {
|
|
853
|
+
boundaryProof: false,
|
|
854
|
+
releaseAuthorised: false,
|
|
855
|
+
evidence: 'not-proven',
|
|
856
|
+
residual: {
|
|
857
|
+
deviceIsolation: true,
|
|
858
|
+
pids: verdict.residualPids ? [...verdict.residualPids] : undefined,
|
|
859
|
+
reason: verdict.reason,
|
|
860
|
+
},
|
|
861
|
+
signalsStopped: false,
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
// An absent evidence field is filled in from the handle kind, never upgraded
|
|
865
|
+
// past it: a weak handle defaults to `scan-clean`, the weakest option.
|
|
866
|
+
const evidence = verdict.evidence
|
|
867
|
+
?? (handle.kind === 'cgroup' ? 'cgroup-empty' : 'scan-clean');
|
|
868
|
+
// EXACTLY ONE unforgeable fact authorises forgetting a tree: a boot id change.
|
|
869
|
+
// A reboot provably killed every process; nothing else does.
|
|
870
|
+
//
|
|
871
|
+
// A cgroup being empty is DELIBERATELY NOT on this list, reversing an earlier
|
|
872
|
+
// revision. mojo runs at the daemon's UID, and cgroup v2 lets a same-UID
|
|
873
|
+
// process migrate ITSELF between cgroups within the delegation domain — up to
|
|
874
|
+
// the parent slice, or sideways into a sibling. The close proof recurses the
|
|
875
|
+
// leaf DOWNWARD, so a process that migrated OUT is invisible and the leaf reads
|
|
876
|
+
// empty. Treating that as proof cleared the device-isolation blocker while a
|
|
877
|
+
// credentialed process ran on — and "device-isolation fails closed" cannot
|
|
878
|
+
// back it up, because the blocker is the very thing the false proof clears
|
|
879
|
+
// (circular). Until a per-session UID or namespace makes the cgroup a boundary
|
|
880
|
+
// the process cannot leave, a cgroup handle is only a KILL + DIAGNOSTIC tool:
|
|
881
|
+
// like a weak scan-clean, it may stop signalling and let the row close, but the
|
|
882
|
+
// close carries a residual and the blocker is RETAINED. `cgroup.kill` + reboot
|
|
883
|
+
// remain the only things that genuinely remove the tree.
|
|
884
|
+
const boundaryProof = evidence === 'boot-id-changed';
|
|
885
|
+
if (boundaryProof) {
|
|
886
|
+
return {
|
|
887
|
+
boundaryProof: true,
|
|
888
|
+
releaseAuthorised: true,
|
|
889
|
+
evidence,
|
|
890
|
+
residual: null,
|
|
891
|
+
signalsStopped: true,
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
const reason = evidence === 'cgroup-empty' || evidence === 'cgroup-zombie-only'
|
|
895
|
+
? `${evidence} is a KILL/diagnostic signal, not a boundary proof: a same-UID process can `
|
|
896
|
+
+ 'migrate itself out of the cgroup (to the parent slice or a sibling), which a leaf-down '
|
|
897
|
+
+ 'read cannot see. The blocker is retained until a reboot or an operator revoke.'
|
|
898
|
+
: `${evidence} is a diagnostic signal, not a boundary proof: a descendant that `
|
|
899
|
+
+ 'calls setsid, scrubs its environ and reparents to init is invisible to the scan';
|
|
900
|
+
return {
|
|
901
|
+
boundaryProof: false,
|
|
902
|
+
releaseAuthorised: false,
|
|
903
|
+
evidence,
|
|
904
|
+
residual: { deviceIsolation: true, reason },
|
|
905
|
+
signalsStopped: true,
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Release a handle — the ONLY removal path, and it demands the proof.
|
|
910
|
+
*
|
|
911
|
+
* Taking the verdict (rather than a boolean, or nothing at all) is deliberate: it
|
|
912
|
+
* makes "clear the blocker without proving quiescence" unrepresentable at the type
|
|
913
|
+
* level, which is the invariant this whole module exists to enforce. A caller
|
|
914
|
+
* holding a `proven: false` verdict has no way to spend it here.
|
|
915
|
+
*
|
|
916
|
+
* The ONE evidence that authorises release is `boot-id-changed`, for either
|
|
917
|
+
* handle kind (a strong handle carries a stamped bootId since round 10).
|
|
918
|
+
* Emptiness verdicts (cgroup-empty / cgroup-zombie-only / scan-clean) resolve to
|
|
919
|
+
* `releaseAuthorised: false` and return below with the handle and the
|
|
920
|
+
* device-isolation blocker retained — cgroup emptiness is not a boundary proof
|
|
921
|
+
* (a same-UID process can migrate out of the leaf; see
|
|
922
|
+
* containmentReleaseDecision). No cgroup directory is reclaimed on the release
|
|
923
|
+
* path: `boot-id-changed` means the host rebooted and cgroupfs came back empty,
|
|
924
|
+
* so there is nothing to remove. Within a boot the tree is killed via
|
|
925
|
+
* `cgroup.kill` during teardown and the handle waits for reboot or operator
|
|
926
|
+
* revoke.
|
|
927
|
+
*/
|
|
928
|
+
export function releaseContainmentHandle(verdict, dataDir) {
|
|
929
|
+
if (!verdict.proven) {
|
|
930
|
+
throw new MojoContainmentUnavailableError(`refusing to release containment for session ${verdict.handle.sessionId}: `
|
|
931
|
+
+ `quiescence was not proven (${verdict.reason})`);
|
|
932
|
+
}
|
|
933
|
+
const handle = verdict.handle;
|
|
934
|
+
const decision = containmentReleaseDecision(verdict);
|
|
935
|
+
if (!decision.releaseAuthorised) {
|
|
936
|
+
// Proven, but only diagnostically (a weak scan-clean, or ANY cgroup verdict
|
|
937
|
+
// now). Do NOT throw: the caller is allowed to stop signalling and to close
|
|
938
|
+
// the session. Just refuse to forget the tree, which is what keeps the
|
|
939
|
+
// device-isolation blocker after the close.
|
|
940
|
+
logger.warn(`[mojo] session ${handle.sessionId}: containment looks quiescent by `
|
|
941
|
+
+ `${decision.evidence} but that is not a boundary proof; keeping the handle and the `
|
|
942
|
+
+ 'device-isolation residual. Signals stopped, blocker retained.');
|
|
943
|
+
return decision;
|
|
944
|
+
}
|
|
945
|
+
// Only a boot-id-changed proof reaches here (either handle kind). No cgroup
|
|
946
|
+
// directory needs reclaiming: the reboot that changed the boot id also reset
|
|
947
|
+
// cgroupfs, so a strong handle's directory is already gone.
|
|
948
|
+
const path = filePath(dataDir);
|
|
949
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
950
|
+
withFileLockSync(path, () => {
|
|
951
|
+
const data = readStrict(dataDir);
|
|
952
|
+
const before = data.sessions[handle.sessionId] ?? [];
|
|
953
|
+
const key = containmentHandleKey(handle);
|
|
954
|
+
const after = before.filter(h => containmentHandleKey(h) !== key);
|
|
955
|
+
if (after.length === before.length)
|
|
956
|
+
return;
|
|
957
|
+
if (after.length === 0)
|
|
958
|
+
delete data.sessions[handle.sessionId];
|
|
959
|
+
else
|
|
960
|
+
data.sessions[handle.sessionId] = after;
|
|
961
|
+
writeStrict(data, dataDir);
|
|
962
|
+
});
|
|
963
|
+
return decision;
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* OPERATOR OVERRIDE: drop handles without quiescence proof.
|
|
967
|
+
*
|
|
968
|
+
* releaseContainmentHandle is deliberately unreachable without a proven verdict,
|
|
969
|
+
* and on a non-cgroup host a weak handle's scan-clean is never a boundary proof
|
|
970
|
+
* — so after one mojo session runs and closes there, its handle (and with it the
|
|
971
|
+
* whole-machine device-isolation `activation_blocked` 409) persists FOREVER,
|
|
972
|
+
* with no operational path out short of hand-editing the ledger JSON. An
|
|
973
|
+
* `unprovable` handle is like that by design. Both are correct fail-closed
|
|
974
|
+
* defaults and both still need an explicit, auditable exit.
|
|
975
|
+
*
|
|
976
|
+
* This is that exit, and only for a human operator (the `botmux mojo-containment
|
|
977
|
+
* revoke` command): it removes the named handles while logging exactly what was
|
|
978
|
+
* dropped, so the decision that "these trees are acceptable to forget" is a
|
|
979
|
+
* recorded human judgement, never something the runtime can reach on its own.
|
|
980
|
+
* Nothing in daemon/worker code may call this.
|
|
981
|
+
*/
|
|
982
|
+
export function revokeContainmentHandles(sessionId, opts = {}) {
|
|
983
|
+
const path = filePath(opts.dataDir);
|
|
984
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
985
|
+
let removed = [];
|
|
986
|
+
let remaining = [];
|
|
987
|
+
withFileLockSync(path, () => {
|
|
988
|
+
const data = readStrict(opts.dataDir);
|
|
989
|
+
const before = data.sessions[sessionId] ?? [];
|
|
990
|
+
removed = opts.handleKey === undefined
|
|
991
|
+
? before
|
|
992
|
+
: before.filter(h => containmentHandleKey(h) === opts.handleKey);
|
|
993
|
+
remaining = opts.handleKey === undefined
|
|
994
|
+
? []
|
|
995
|
+
: before.filter(h => containmentHandleKey(h) !== opts.handleKey);
|
|
996
|
+
if (removed.length === 0)
|
|
997
|
+
return;
|
|
998
|
+
if (remaining.length === 0)
|
|
999
|
+
delete data.sessions[sessionId];
|
|
1000
|
+
else
|
|
1001
|
+
data.sessions[sessionId] = remaining;
|
|
1002
|
+
writeStrict(data, opts.dataDir);
|
|
1003
|
+
});
|
|
1004
|
+
for (const handle of removed) {
|
|
1005
|
+
logger.warn(`[mojo] OPERATOR REVOCATION: containment handle ${containmentHandleKey(handle)} for `
|
|
1006
|
+
+ `session ${sessionId} was dropped WITHOUT quiescence proof. Any surviving subtree `
|
|
1007
|
+
+ 'of that turn is no longer tracked; its device-isolation blocker is gone.'
|
|
1008
|
+
+ (opts.auditNote ? ` [${opts.auditNote}]` : ''));
|
|
1009
|
+
}
|
|
1010
|
+
// The caller (the operator CLI) is responsible for cgroup.kill + reclaiming the
|
|
1011
|
+
// directory of any removed cgroup handle, and for LOGGING that kill's result.
|
|
1012
|
+
// A cgroup handle IS released by proof — but only on `boot-id-changed`, i.e. a
|
|
1013
|
+
// reboot, which already wiped cgroupfs, so there is nothing to reclaim on that
|
|
1014
|
+
// path. Revoke is therefore the only place a cgroup handle is dropped while its
|
|
1015
|
+
// directory may still exist (a same-boot operator override), so it is where the
|
|
1016
|
+
// reclaim lives. Doing it here fire-and-forget would drop the blocker before a
|
|
1017
|
+
// possibly-live tree was actually killed, invisibly; the CLI awaits it instead.
|
|
1018
|
+
return { removed, remaining };
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Executing (non-zombie) pids anywhere in a cgroup handle's subtree, for the
|
|
1022
|
+
* operator revoke safety gate. `unreadable` is the fail-closed signal — the gate
|
|
1023
|
+
* must surface it rather than read "cannot enumerate" as "nobody there".
|
|
1024
|
+
*/
|
|
1025
|
+
export function cgroupHandleLiveMembers(handle, opts = {}) {
|
|
1026
|
+
const subtree = cgroupSubtreePids(handle.cgroupPath);
|
|
1027
|
+
if (!subtree.ok)
|
|
1028
|
+
return { live: [], unreadable: true };
|
|
1029
|
+
const live = subtree.pids.filter(pid => readProcLiveness(pid, opts) !== 'zombie'
|
|
1030
|
+
&& readProcLiveness(pid, opts) !== 'gone');
|
|
1031
|
+
return { live, unreadable: false };
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Hand every outstanding handle of a session to a new worker generation.
|
|
1035
|
+
*
|
|
1036
|
+
* Inheritance is a UNION and it is unconditional: replacement does not prove
|
|
1037
|
+
* anything about the old tree, so the new generation becomes responsible for
|
|
1038
|
+
* proving it later. The generation stamp is refreshed for logging while the
|
|
1039
|
+
* IDENTITY fields (cgroup path, pid/boot/starttime, nonce) are preserved
|
|
1040
|
+
* verbatim — rewriting those would invent a handle that proves nothing about the
|
|
1041
|
+
* tree actually left behind.
|
|
1042
|
+
*
|
|
1043
|
+
* Nothing is removed here, so a crash mid-inheritance is safe: the handles are
|
|
1044
|
+
* still recorded under the same session id.
|
|
1045
|
+
*/
|
|
1046
|
+
export function inheritContainmentHandles(sessionId, nextGeneration, dataDir) {
|
|
1047
|
+
const path = filePath(dataDir);
|
|
1048
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1049
|
+
let inherited = [];
|
|
1050
|
+
withFileLockSync(path, () => {
|
|
1051
|
+
const data = readStrict(dataDir);
|
|
1052
|
+
const before = data.sessions[sessionId] ?? [];
|
|
1053
|
+
if (before.length === 0)
|
|
1054
|
+
return;
|
|
1055
|
+
inherited = before.map(h => ({ ...h, generation: nextGeneration }));
|
|
1056
|
+
data.sessions[sessionId] = inherited;
|
|
1057
|
+
writeStrict(data, dataDir);
|
|
1058
|
+
});
|
|
1059
|
+
return inherited;
|
|
1060
|
+
}
|
|
1061
|
+
// ── the ONLY source of boundaryProof: true ───────────────────────────────────
|
|
1062
|
+
/**
|
|
1063
|
+
* Map a containment verdict onto ProcTree's `TurnQuiescence`.
|
|
1064
|
+
*
|
|
1065
|
+
* This function is the single place in the codebase allowed to mint
|
|
1066
|
+
* `{ kind: 'contained-proven', boundaryProof: true }`, and it does so for exactly
|
|
1067
|
+
* ONE evidence — `boot-id-changed`. That is the whole point of the contracts
|
|
1068
|
+
* meeting here:
|
|
1069
|
+
*
|
|
1070
|
+
* - `quiescenceFromScan()` can never produce `boundaryProof: true`; a clean
|
|
1071
|
+
* /proc scan is a diagnostic signal, because a descendant that both setsids
|
|
1072
|
+
* AND scrubs its own environ evades enumeration entirely.
|
|
1073
|
+
* - An empty (or zombie-only) `cgroup.procs` is ALSO only a diagnostic signal,
|
|
1074
|
+
* NOT a boundary proof: mojo runs at the daemon's UID and cgroup v2 lets a
|
|
1075
|
+
* same-UID process migrate itself out of the leaf, invisible to the leaf-down
|
|
1076
|
+
* read. cgroup emptiness is good for stopping signals and for killing
|
|
1077
|
+
* (cgroup.kill), never for dropping device isolation.
|
|
1078
|
+
* - `boot-id-changed` is the only unforgeable release: a reboot kills the whole
|
|
1079
|
+
* tree, migrant included. It applies to a weak handle and to a cgroup handle
|
|
1080
|
+
* once its bootId is stamped.
|
|
1081
|
+
*
|
|
1082
|
+
* So a STRONG (cgroup) proven-empty verdict maps to `diagnostic-clean`, exactly
|
|
1083
|
+
* like a weak `scan-clean`, and `containmentReleaseDecision` refuses to authorise
|
|
1084
|
+
* removal for either — the handle stays in the durable store and the blocker
|
|
1085
|
+
* survives the close. The gate lives in `containmentReleaseDecision`, a real
|
|
1086
|
+
* production consumer.
|
|
1087
|
+
*/
|
|
1088
|
+
/**
|
|
1089
|
+
* The ONE place a `boundaryProof: true` TurnQuiescence is constructed.
|
|
1090
|
+
*
|
|
1091
|
+
* Round 4 claimed this property in a comment while three separate sites minted
|
|
1092
|
+
* the value, which is why the claim was rejected as unverifiable. It is now
|
|
1093
|
+
* structural: `boundaryProof: true` is minted here and in the release-decision
|
|
1094
|
+
* gate (`containmentReleaseDecision`), and nowhere else — `git grep -n
|
|
1095
|
+
* "boundaryProof: true" -- src/` returns those two sites plus type definitions.
|
|
1096
|
+
* It is exported for the backend, which decides WHETHER a proven boundary applies
|
|
1097
|
+
* but must not decide what one looks like.
|
|
1098
|
+
*/
|
|
1099
|
+
export function containedProvenQuiescence() {
|
|
1100
|
+
return { kind: 'contained-proven', boundaryProof: true };
|
|
1101
|
+
}
|
|
1102
|
+
export function containmentQuiescence(verdict) {
|
|
1103
|
+
if (!verdict.proven) {
|
|
1104
|
+
if (verdict.handle.kind === 'unprovable') {
|
|
1105
|
+
return { kind: 'unsupported-platform', boundaryProof: false, platform: verdict.handle.platform };
|
|
1106
|
+
}
|
|
1107
|
+
return verdict.residualPids && verdict.residualPids.length > 0
|
|
1108
|
+
? { kind: 'alive', boundaryProof: false, pids: [...verdict.residualPids] }
|
|
1109
|
+
: { kind: 'unscannable', boundaryProof: false, reason: verdict.reason };
|
|
1110
|
+
}
|
|
1111
|
+
if (containmentReleaseDecision(verdict).boundaryProof) {
|
|
1112
|
+
return containedProvenQuiescence();
|
|
1113
|
+
}
|
|
1114
|
+
// No `unprovable` case here on purpose. proveContainmentQuiescent NEVER returns
|
|
1115
|
+
// proven:true for that kind, so a branch for it would be unreachable — an
|
|
1116
|
+
// unfalsifiable claim that no mutation can kill. A hand-constructed impossible
|
|
1117
|
+
// verdict falls through to the line below, which is still boundaryProof:false,
|
|
1118
|
+
// so deleting the branch costs no safety.
|
|
1119
|
+
//
|
|
1120
|
+
// Proven as far as a weak handle can prove anything — diagnostic only.
|
|
1121
|
+
return { kind: 'diagnostic-clean', boundaryProof: false };
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Strongest quiescence statement available for a session, across ALL of its
|
|
1125
|
+
* outstanding handles.
|
|
1126
|
+
*
|
|
1127
|
+
* Semantics are intentionally pessimistic, because a session is only as contained
|
|
1128
|
+
* as its WEAKEST outstanding tree:
|
|
1129
|
+
* - any handle that is not proven → that handle's non-proof is the answer
|
|
1130
|
+
* - no handles at all → `diagnostic-clean`; nothing is recorded,
|
|
1131
|
+
* but "nothing recorded" is not a kernel-level boundary proof either
|
|
1132
|
+
* - every handle proven, at least one only diagnostically → `diagnostic-clean`
|
|
1133
|
+
* - every handle proven WITH a boundary proof → `contained-proven`
|
|
1134
|
+
*
|
|
1135
|
+
* A store that cannot be read THROWS (via `containmentHandles`), so an
|
|
1136
|
+
* unreadable store can never present itself as a clean session.
|
|
1137
|
+
*/
|
|
1138
|
+
export function sessionContainmentQuiescence(sessionId, prove, dataDir) {
|
|
1139
|
+
const handles = containmentHandles(sessionId, dataDir);
|
|
1140
|
+
if (handles.length === 0)
|
|
1141
|
+
return { kind: 'diagnostic-clean', boundaryProof: false };
|
|
1142
|
+
// Same authority as the single-handle path on purpose: a session is only as
|
|
1143
|
+
// contained as its weakest tree, and "weakest" is decided by boundary proof,
|
|
1144
|
+
// not by handle kind, so a weak handle aged out by a reboot counts while a
|
|
1145
|
+
// weak handle that merely scanned clean does not.
|
|
1146
|
+
let allProvenBoundary = true;
|
|
1147
|
+
for (const handle of handles) {
|
|
1148
|
+
const verdict = prove(handle);
|
|
1149
|
+
if (!verdict.proven)
|
|
1150
|
+
return containmentQuiescence(verdict);
|
|
1151
|
+
if (!containmentReleaseDecision(verdict).boundaryProof)
|
|
1152
|
+
allProvenBoundary = false;
|
|
1153
|
+
}
|
|
1154
|
+
return allProvenBoundary
|
|
1155
|
+
? containedProvenQuiescence()
|
|
1156
|
+
: { kind: 'diagnostic-clean', boundaryProof: false };
|
|
1157
|
+
}
|
|
1158
|
+
//# sourceMappingURL=mojo-containment.js.map
|