apex-code 0.0.1-alpha.0 → 0.0.1-alpha.10
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/CHANGELOG.md +471 -0
- package/README.md +74 -680
- package/dist/cli/args.d.ts +21 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +119 -15
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/auth-command.d.ts.map +1 -1
- package/dist/cli/auth-command.js +3 -3
- package/dist/cli/auth-command.js.map +1 -1
- package/dist/cli/concurrent-session.d.ts +3 -0
- package/dist/cli/concurrent-session.d.ts.map +1 -0
- package/dist/cli/concurrent-session.js +13 -0
- package/dist/cli/concurrent-session.js.map +1 -0
- package/dist/cli/cost-command.d.ts +19 -0
- package/dist/cli/cost-command.d.ts.map +1 -0
- package/dist/cli/cost-command.js +107 -0
- package/dist/cli/cost-command.js.map +1 -0
- package/dist/cli/file-processor.d.ts.map +1 -1
- package/dist/cli/file-processor.js +2 -1
- package/dist/cli/file-processor.js.map +1 -1
- package/dist/cli/startup-ui.d.ts +1 -1
- package/dist/cli/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +1 -2
- package/dist/cli/startup-ui.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +99 -16
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +5 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +35 -20
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +2 -0
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +28 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +140 -4
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +121 -9
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +364 -123
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts +4 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +17 -14
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +11 -10
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +3 -0
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +4 -3
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +41 -32
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/context/deferred-schemas.d.ts +92 -0
- package/dist/core/context/deferred-schemas.d.ts.map +1 -0
- package/dist/core/context/deferred-schemas.js +74 -0
- package/dist/core/context/deferred-schemas.js.map +1 -0
- package/dist/core/context/eviction.d.ts +47 -0
- package/dist/core/context/eviction.d.ts.map +1 -0
- package/dist/core/context/eviction.js +83 -0
- package/dist/core/context/eviction.js.map +1 -0
- package/dist/core/context/pipeline.d.ts +90 -0
- package/dist/core/context/pipeline.d.ts.map +1 -0
- package/dist/core/context/pipeline.js +109 -0
- package/dist/core/context/pipeline.js.map +1 -0
- package/dist/core/credential-failover.d.ts +30 -0
- package/dist/core/credential-failover.d.ts.map +1 -0
- package/dist/core/credential-failover.js +46 -0
- package/dist/core/credential-failover.js.map +1 -0
- package/dist/core/credential-pool.d.ts +60 -0
- package/dist/core/credential-pool.d.ts.map +1 -0
- package/dist/core/credential-pool.js +84 -0
- package/dist/core/credential-pool.js.map +1 -0
- package/dist/core/defaults.d.ts +1 -0
- package/dist/core/defaults.d.ts.map +1 -1
- package/dist/core/defaults.js +9 -0
- package/dist/core/defaults.js.map +1 -1
- package/dist/core/delegation/agents.d.ts +15 -0
- package/dist/core/delegation/agents.d.ts.map +1 -0
- package/dist/core/delegation/agents.js +55 -0
- package/dist/core/delegation/agents.js.map +1 -0
- package/dist/core/delegation/ceiling.d.ts +29 -0
- package/dist/core/delegation/ceiling.d.ts.map +1 -0
- package/dist/core/delegation/ceiling.js +34 -0
- package/dist/core/delegation/ceiling.js.map +1 -0
- package/dist/core/delegation/runtime.d.ts +94 -0
- package/dist/core/delegation/runtime.d.ts.map +1 -0
- package/dist/core/delegation/runtime.js +117 -0
- package/dist/core/delegation/runtime.js.map +1 -0
- package/dist/core/durable-state/daemon.d.ts +37 -0
- package/dist/core/durable-state/daemon.d.ts.map +1 -0
- package/dist/core/durable-state/daemon.js +62 -0
- package/dist/core/durable-state/daemon.js.map +1 -0
- package/dist/core/durable-state/provenance.d.ts +7 -0
- package/dist/core/durable-state/provenance.d.ts.map +1 -0
- package/dist/core/durable-state/provenance.js +24 -0
- package/dist/core/durable-state/provenance.js.map +1 -0
- package/dist/core/durable-state/sqlite.d.ts +84 -0
- package/dist/core/durable-state/sqlite.d.ts.map +1 -0
- package/dist/core/durable-state/sqlite.js +355 -0
- package/dist/core/durable-state/sqlite.js.map +1 -0
- package/dist/core/environment.d.ts +12 -0
- package/dist/core/environment.d.ts.map +1 -0
- package/dist/core/environment.js +69 -0
- package/dist/core/environment.js.map +1 -0
- package/dist/core/evidence-policy.d.ts +17 -0
- package/dist/core/evidence-policy.d.ts.map +1 -0
- package/dist/core/evidence-policy.js +25 -0
- package/dist/core/evidence-policy.js.map +1 -0
- package/dist/core/evidence.d.ts +16 -0
- package/dist/core/evidence.d.ts.map +1 -0
- package/dist/core/evidence.js +51 -0
- package/dist/core/evidence.js.map +1 -0
- package/dist/core/experimental.d.ts +4 -0
- package/dist/core/experimental.d.ts.map +1 -1
- package/dist/core/experimental.js +6 -1
- package/dist/core/experimental.js.map +1 -1
- package/dist/core/extensions/index.d.ts +2 -2
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +115 -48
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/types.d.ts +44 -9
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js +3 -0
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +1 -1
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +1 -1
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/http-dispatcher.d.ts +1 -2
- package/dist/core/http-dispatcher.d.ts.map +1 -1
- package/dist/core/http-dispatcher.js +2 -17
- package/dist/core/http-dispatcher.js.map +1 -1
- package/dist/core/http-idle-timeout.d.ts +12 -0
- package/dist/core/http-idle-timeout.d.ts.map +1 -0
- package/dist/core/http-idle-timeout.js +27 -0
- package/dist/core/http-idle-timeout.js.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/keybindings.d.ts +38 -13
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +26 -5
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/lsp/client.d.ts +78 -0
- package/dist/core/lsp/client.d.ts.map +1 -0
- package/dist/core/lsp/client.js +494 -0
- package/dist/core/lsp/client.js.map +1 -0
- package/dist/core/lsp/diagnostics.d.ts +47 -0
- package/dist/core/lsp/diagnostics.d.ts.map +1 -0
- package/dist/core/lsp/diagnostics.js +224 -0
- package/dist/core/lsp/diagnostics.js.map +1 -0
- package/dist/core/lsp/pool.d.ts +48 -0
- package/dist/core/lsp/pool.d.ts.map +1 -0
- package/dist/core/lsp/pool.js +150 -0
- package/dist/core/lsp/pool.js.map +1 -0
- package/dist/core/lsp/registry.d.ts +49 -0
- package/dist/core/lsp/registry.d.ts.map +1 -0
- package/dist/core/lsp/registry.js +215 -0
- package/dist/core/lsp/registry.js.map +1 -0
- package/dist/core/lsp/status.d.ts +23 -0
- package/dist/core/lsp/status.d.ts.map +1 -0
- package/dist/core/lsp/status.js +35 -0
- package/dist/core/lsp/status.js.map +1 -0
- package/dist/core/lsp/sync.d.ts +13 -0
- package/dist/core/lsp/sync.d.ts.map +1 -0
- package/dist/core/lsp/sync.js +71 -0
- package/dist/core/lsp/sync.js.map +1 -0
- package/dist/core/model-config.d.ts +18 -0
- package/dist/core/model-config.d.ts.map +1 -1
- package/dist/core/model-config.js +25 -6
- package/dist/core/model-config.js.map +1 -1
- package/dist/core/model-resolver.d.ts +21 -0
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +73 -8
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/model-runtime.d.ts +73 -0
- package/dist/core/model-runtime.d.ts.map +1 -1
- package/dist/core/model-runtime.js +225 -8
- package/dist/core/model-runtime.js.map +1 -1
- package/dist/core/models-store.d.ts.map +1 -1
- package/dist/core/models-store.js +2 -1
- package/dist/core/models-store.js.map +1 -1
- package/dist/core/observability/aggregate.d.ts +21 -0
- package/dist/core/observability/aggregate.d.ts.map +1 -0
- package/dist/core/observability/aggregate.js +36 -0
- package/dist/core/observability/aggregate.js.map +1 -0
- package/dist/core/observability/otlp.d.ts +66 -0
- package/dist/core/observability/otlp.d.ts.map +1 -0
- package/dist/core/observability/otlp.js +95 -0
- package/dist/core/observability/otlp.js.map +1 -0
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +40 -16
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/permissions/gate.d.ts +42 -0
- package/dist/core/permissions/gate.d.ts.map +1 -0
- package/dist/core/permissions/gate.js +72 -0
- package/dist/core/permissions/gate.js.map +1 -0
- package/dist/core/permissions/modes.d.ts +18 -0
- package/dist/core/permissions/modes.d.ts.map +1 -0
- package/dist/core/permissions/modes.js +57 -0
- package/dist/core/permissions/modes.js.map +1 -0
- package/dist/core/permissions/responder.d.ts +30 -0
- package/dist/core/permissions/responder.d.ts.map +1 -0
- package/dist/core/permissions/responder.js +32 -0
- package/dist/core/permissions/responder.js.map +1 -0
- package/dist/core/permissions/rules.d.ts +34 -0
- package/dist/core/permissions/rules.d.ts.map +1 -0
- package/dist/core/permissions/rules.js +74 -0
- package/dist/core/permissions/rules.js.map +1 -0
- package/dist/core/permissions/startup.d.ts +45 -0
- package/dist/core/permissions/startup.d.ts.map +1 -0
- package/dist/core/permissions/startup.js +63 -0
- package/dist/core/permissions/startup.js.map +1 -0
- package/dist/core/permissions/store.d.ts +114 -0
- package/dist/core/permissions/store.d.ts.map +1 -0
- package/dist/core/permissions/store.js +229 -0
- package/dist/core/permissions/store.js.map +1 -0
- package/dist/core/pi-manifest.d.ts.map +1 -1
- package/dist/core/pi-manifest.js +2 -1
- package/dist/core/pi-manifest.js.map +1 -1
- package/dist/core/project-trust.d.ts.map +1 -1
- package/dist/core/project-trust.js +1 -1
- package/dist/core/project-trust.js.map +1 -1
- package/dist/core/provider-attribution.d.ts.map +1 -1
- package/dist/core/provider-attribution.js +6 -7
- package/dist/core/provider-attribution.js.map +1 -1
- package/dist/core/remote-catalog-provider.d.ts +2 -2
- package/dist/core/remote-catalog-provider.d.ts.map +1 -1
- package/dist/core/remote-catalog-provider.js +22 -17
- package/dist/core/remote-catalog-provider.js.map +1 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +3 -2
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sandbox/bwrap-arguments.d.ts +52 -0
- package/dist/core/sandbox/bwrap-arguments.d.ts.map +1 -0
- package/dist/core/sandbox/bwrap-arguments.js +86 -0
- package/dist/core/sandbox/bwrap-arguments.js.map +1 -0
- package/dist/core/sandbox/child-entry.d.ts +2 -0
- package/dist/core/sandbox/child-entry.d.ts.map +1 -0
- package/dist/core/sandbox/child-entry.js +22 -0
- package/dist/core/sandbox/child-entry.js.map +1 -0
- package/dist/core/sandbox/cli-launch.d.ts +88 -0
- package/dist/core/sandbox/cli-launch.d.ts.map +1 -0
- package/dist/core/sandbox/cli-launch.js +272 -0
- package/dist/core/sandbox/cli-launch.js.map +1 -0
- package/dist/core/sandbox/cli-supervisor.d.ts +30 -0
- package/dist/core/sandbox/cli-supervisor.d.ts.map +1 -0
- package/dist/core/sandbox/cli-supervisor.js +123 -0
- package/dist/core/sandbox/cli-supervisor.js.map +1 -0
- package/dist/core/sandbox/default-hosts.d.ts +28 -0
- package/dist/core/sandbox/default-hosts.d.ts.map +1 -0
- package/dist/core/sandbox/default-hosts.js +69 -0
- package/dist/core/sandbox/default-hosts.js.map +1 -0
- package/dist/core/sandbox/full-access.d.ts +28 -0
- package/dist/core/sandbox/full-access.d.ts.map +1 -0
- package/dist/core/sandbox/full-access.js +63 -0
- package/dist/core/sandbox/full-access.js.map +1 -0
- package/dist/core/sandbox/git-identity.d.ts +57 -0
- package/dist/core/sandbox/git-identity.d.ts.map +1 -0
- package/dist/core/sandbox/git-identity.js +78 -0
- package/dist/core/sandbox/git-identity.js.map +1 -0
- package/dist/core/sandbox/host-approval.d.ts +42 -0
- package/dist/core/sandbox/host-approval.d.ts.map +1 -0
- package/dist/core/sandbox/host-approval.js +99 -0
- package/dist/core/sandbox/host-approval.js.map +1 -0
- package/dist/core/sandbox/linux-backend.d.ts +46 -0
- package/dist/core/sandbox/linux-backend.d.ts.map +1 -0
- package/dist/core/sandbox/linux-backend.js +304 -0
- package/dist/core/sandbox/linux-backend.js.map +1 -0
- package/dist/core/sandbox/macos-backend.d.ts +28 -0
- package/dist/core/sandbox/macos-backend.d.ts.map +1 -0
- package/dist/core/sandbox/macos-backend.js +313 -0
- package/dist/core/sandbox/macos-backend.js.map +1 -0
- package/dist/core/sandbox/network-proxy.d.ts +38 -0
- package/dist/core/sandbox/network-proxy.d.ts.map +1 -0
- package/dist/core/sandbox/network-proxy.js +153 -0
- package/dist/core/sandbox/network-proxy.js.map +1 -0
- package/dist/core/sandbox/network-refusal.d.ts +24 -0
- package/dist/core/sandbox/network-refusal.d.ts.map +1 -0
- package/dist/core/sandbox/network-refusal.js +72 -0
- package/dist/core/sandbox/network-refusal.js.map +1 -0
- package/dist/core/sandbox/policy.d.ts +40 -0
- package/dist/core/sandbox/policy.d.ts.map +1 -0
- package/dist/core/sandbox/policy.js +55 -0
- package/dist/core/sandbox/policy.js.map +1 -0
- package/dist/core/sandbox/profiles.d.ts +25 -0
- package/dist/core/sandbox/profiles.d.ts.map +1 -0
- package/dist/core/sandbox/profiles.js +23 -0
- package/dist/core/sandbox/profiles.js.map +1 -0
- package/dist/core/sandbox/rpc/command-client.d.ts +28 -0
- package/dist/core/sandbox/rpc/command-client.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/command-client.js +86 -0
- package/dist/core/sandbox/rpc/command-client.js.map +1 -0
- package/dist/core/sandbox/rpc/command-proxy.d.ts +48 -0
- package/dist/core/sandbox/rpc/command-proxy.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/command-proxy.js +120 -0
- package/dist/core/sandbox/rpc/command-proxy.js.map +1 -0
- package/dist/core/sandbox/rpc/credential-client.d.ts +20 -0
- package/dist/core/sandbox/rpc/credential-client.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/credential-client.js +170 -0
- package/dist/core/sandbox/rpc/credential-client.js.map +1 -0
- package/dist/core/sandbox/rpc/credential-proxy.d.ts +44 -0
- package/dist/core/sandbox/rpc/credential-proxy.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/credential-proxy.js +287 -0
- package/dist/core/sandbox/rpc/credential-proxy.js.map +1 -0
- package/dist/core/sandbox/rpc/framing.d.ts +35 -0
- package/dist/core/sandbox/rpc/framing.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/framing.js +114 -0
- package/dist/core/sandbox/rpc/framing.js.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-helper.d.ts +40 -0
- package/dist/core/sandbox/rpc/git-credential-helper.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-helper.js +152 -0
- package/dist/core/sandbox/rpc/git-credential-helper.js.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.d.ts +48 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.js +142 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.js.map +1 -0
- package/dist/core/sandbox/supervisor-temp.d.ts +18 -0
- package/dist/core/sandbox/supervisor-temp.d.ts.map +1 -0
- package/dist/core/sandbox/supervisor-temp.js +21 -0
- package/dist/core/sandbox/supervisor-temp.js.map +1 -0
- package/dist/core/sandbox/supervisor.d.ts +51 -0
- package/dist/core/sandbox/supervisor.d.ts.map +1 -0
- package/dist/core/sandbox/supervisor.js +18 -0
- package/dist/core/sandbox/supervisor.js.map +1 -0
- package/dist/core/sandbox/terminal-handoff.d.ts +45 -0
- package/dist/core/sandbox/terminal-handoff.d.ts.map +1 -0
- package/dist/core/sandbox/terminal-handoff.js +176 -0
- package/dist/core/sandbox/terminal-handoff.js.map +1 -0
- package/dist/core/sandbox/terminal-size.d.ts +40 -0
- package/dist/core/sandbox/terminal-size.d.ts.map +1 -0
- package/dist/core/sandbox/terminal-size.js +105 -0
- package/dist/core/sandbox/terminal-size.js.map +1 -0
- package/dist/core/sandbox/violations.d.ts +23 -0
- package/dist/core/sandbox/violations.d.ts.map +1 -0
- package/dist/core/sandbox/violations.js +30 -0
- package/dist/core/sandbox/violations.js.map +1 -0
- package/dist/core/sdk.d.ts +32 -41
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +161 -6
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-export.d.ts +4 -0
- package/dist/core/session-export.d.ts.map +1 -0
- package/dist/core/session-export.js +32 -0
- package/dist/core/session-export.js.map +1 -0
- package/dist/core/session-lease.d.ts +31 -0
- package/dist/core/session-lease.d.ts.map +1 -0
- package/dist/core/session-lease.js +138 -0
- package/dist/core/session-lease.js.map +1 -0
- package/dist/core/session-manager.d.ts +46 -2
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +62 -4
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-share.d.ts +31 -0
- package/dist/core/session-share.d.ts.map +1 -0
- package/dist/core/session-share.js +45 -0
- package/dist/core/session-share.js.map +1 -0
- package/dist/core/settings-diagnostics.d.ts +9 -0
- package/dist/core/settings-diagnostics.d.ts.map +1 -0
- package/dist/core/settings-diagnostics.js +21 -0
- package/dist/core/settings-diagnostics.js.map +1 -0
- package/dist/core/settings-manager.d.ts +92 -9
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +138 -36
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts +19 -7
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +128 -52
- package/dist/core/skills.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +5 -2
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts +2 -2
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +59 -68
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/timings.d.ts +1 -1
- package/dist/core/timings.d.ts.map +1 -1
- package/dist/core/timings.js +3 -2
- package/dist/core/timings.js.map +1 -1
- package/dist/core/tools/ask-user.d.ts +25 -0
- package/dist/core/tools/ask-user.d.ts.map +1 -0
- package/dist/core/tools/ask-user.js +49 -0
- package/dist/core/tools/ask-user.js.map +1 -0
- package/dist/core/tools/bash-command-segments.d.ts +22 -0
- package/dist/core/tools/bash-command-segments.d.ts.map +1 -0
- package/dist/core/tools/bash-command-segments.js +128 -0
- package/dist/core/tools/bash-command-segments.js.map +1 -0
- package/dist/core/tools/bash.d.ts +40 -4
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +183 -32
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/contract.d.ts +183 -0
- package/dist/core/tools/contract.d.ts.map +1 -0
- package/dist/core/tools/contract.js +48 -0
- package/dist/core/tools/contract.js.map +1 -0
- package/dist/core/tools/delegate.d.ts +38 -0
- package/dist/core/tools/delegate.d.ts.map +1 -0
- package/dist/core/tools/delegate.js +90 -0
- package/dist/core/tools/delegate.js.map +1 -0
- package/dist/core/tools/diagnostics.d.ts +39 -0
- package/dist/core/tools/diagnostics.d.ts.map +1 -0
- package/dist/core/tools/diagnostics.js +62 -0
- package/dist/core/tools/diagnostics.js.map +1 -0
- package/dist/core/tools/edit-diff.d.ts +0 -5
- package/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/core/tools/edit-diff.js +2 -5
- package/dist/core/tools/edit-diff.js.map +1 -1
- package/dist/core/tools/edit.d.ts +12 -2
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +61 -10
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts +2 -2
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +13 -1
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts +4 -2
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +47 -10
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +51 -6
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +145 -2
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts +4 -2
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +17 -1
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/lsp.d.ts +62 -0
- package/dist/core/tools/lsp.d.ts.map +1 -0
- package/dist/core/tools/lsp.js +216 -0
- package/dist/core/tools/lsp.js.map +1 -0
- package/dist/core/tools/output-accumulator.d.ts.map +1 -1
- package/dist/core/tools/output-accumulator.js +1 -1
- package/dist/core/tools/output-accumulator.js.map +1 -1
- package/dist/core/tools/path-permission.d.ts +18 -0
- package/dist/core/tools/path-permission.d.ts.map +1 -0
- package/dist/core/tools/path-permission.js +28 -0
- package/dist/core/tools/path-permission.js.map +1 -0
- package/dist/core/tools/plan-present.d.ts +27 -0
- package/dist/core/tools/plan-present.d.ts.map +1 -0
- package/dist/core/tools/plan-present.js +55 -0
- package/dist/core/tools/plan-present.js.map +1 -0
- package/dist/core/tools/powershell.d.ts +16 -0
- package/dist/core/tools/powershell.d.ts.map +1 -0
- package/dist/core/tools/powershell.js +39 -0
- package/dist/core/tools/powershell.js.map +1 -0
- package/dist/core/tools/read.d.ts +2 -2
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +14 -0
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/skill-search.d.ts +32 -0
- package/dist/core/tools/skill-search.d.ts.map +1 -0
- package/dist/core/tools/skill-search.js +56 -0
- package/dist/core/tools/skill-search.js.map +1 -0
- package/dist/core/tools/test.d.ts +34 -0
- package/dist/core/tools/test.d.ts.map +1 -0
- package/dist/core/tools/test.js +75 -0
- package/dist/core/tools/test.js.map +1 -0
- package/dist/core/tools/todo-write.d.ts +31 -0
- package/dist/core/tools/todo-write.d.ts.map +1 -0
- package/dist/core/tools/todo-write.js +51 -0
- package/dist/core/tools/todo-write.js.map +1 -0
- package/dist/core/tools/tool-definition-wrapper.d.ts +13 -2
- package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.js +11 -1
- package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
- package/dist/core/tools/tool-schema.d.ts +22 -0
- package/dist/core/tools/tool-schema.d.ts.map +1 -0
- package/dist/core/tools/tool-schema.js +45 -0
- package/dist/core/tools/tool-schema.js.map +1 -0
- package/dist/core/tools/web-fetch.d.ts +26 -0
- package/dist/core/tools/web-fetch.d.ts.map +1 -0
- package/dist/core/tools/web-fetch.js +62 -0
- package/dist/core/tools/web-fetch.js.map +1 -0
- package/dist/core/tools/web-search-exa.d.ts +33 -0
- package/dist/core/tools/web-search-exa.d.ts.map +1 -0
- package/dist/core/tools/web-search-exa.js +128 -0
- package/dist/core/tools/web-search-exa.js.map +1 -0
- package/dist/core/tools/web-search.d.ts +26 -0
- package/dist/core/tools/web-search.d.ts.map +1 -0
- package/dist/core/tools/web-search.js +53 -0
- package/dist/core/tools/web-search.js.map +1 -0
- package/dist/core/tools/write.d.ts +16 -2
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +48 -2
- package/dist/core/tools/write.js.map +1 -1
- package/dist/core/trust-manager.d.ts.map +1 -1
- package/dist/core/trust-manager.js +2 -1
- package/dist/core/trust-manager.js.map +1 -1
- package/dist/core/usage-performance-store.d.ts +56 -0
- package/dist/core/usage-performance-store.d.ts.map +1 -0
- package/dist/core/usage-performance-store.js +85 -0
- package/dist/core/usage-performance-store.js.map +1 -0
- package/dist/core/web-search-provider.d.ts +64 -0
- package/dist/core/web-search-provider.d.ts.map +1 -0
- package/dist/core/web-search-provider.js +149 -0
- package/dist/core/web-search-provider.js.map +1 -0
- package/dist/extensions/llama/index.d.ts.map +1 -1
- package/dist/extensions/llama/index.js +2 -0
- package/dist/extensions/llama/index.js.map +1 -1
- package/dist/extensions/llama/provider.d.ts.map +1 -1
- package/dist/extensions/llama/provider.js +6 -2
- package/dist/extensions/llama/provider.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +5 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +87 -22
- package/dist/main.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +6 -5
- package/dist/migrations.js.map +1 -1
- package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/config-selector.js +9 -6
- package/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +72 -1
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +338 -1
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/dynamic-border.d.ts +7 -1
- package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
- package/dist/modes/interactive/components/dynamic-border.js +11 -2
- package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts +4 -0
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +24 -6
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/first-time-setup.d.ts +1 -4
- package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
- package/dist/modes/interactive/components/first-time-setup.js +17 -41
- package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
- package/dist/modes/interactive/components/first-use-hints.d.ts +8 -0
- package/dist/modes/interactive/components/first-use-hints.d.ts.map +1 -0
- package/dist/modes/interactive/components/first-use-hints.js +22 -0
- package/dist/modes/interactive/components/first-use-hints.js.map +1 -0
- package/dist/modes/interactive/components/footer.d.ts +23 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +182 -21
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +2 -0
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +2 -0
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +31 -9
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +236 -96
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +25 -5
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +226 -58
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-submenu.d.ts +72 -0
- package/dist/modes/interactive/components/settings-submenu.d.ts.map +1 -0
- package/dist/modes/interactive/components/settings-submenu.js +165 -0
- package/dist/modes/interactive/components/settings-submenu.js.map +1 -0
- package/dist/modes/interactive/components/splash-header.d.ts +66 -0
- package/dist/modes/interactive/components/splash-header.d.ts.map +1 -0
- package/dist/modes/interactive/components/splash-header.js +180 -0
- package/dist/modes/interactive/components/splash-header.js.map +1 -0
- package/dist/modes/interactive/components/thinking-selector.d.ts +15 -3
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.js +74 -17
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +16 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +113 -35
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tool-panel.d.ts +30 -0
- package/dist/modes/interactive/components/tool-panel.d.ts.map +1 -0
- package/dist/modes/interactive/components/tool-panel.js +130 -0
- package/dist/modes/interactive/components/tool-panel.js.map +1 -0
- package/dist/modes/interactive/external-editor.d.ts.map +1 -1
- package/dist/modes/interactive/external-editor.js +60 -10
- package/dist/modes/interactive/external-editor.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +61 -5
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +642 -203
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/model-catalog-refresh.d.ts +7 -0
- package/dist/modes/interactive/model-catalog-refresh.d.ts.map +1 -0
- package/dist/modes/interactive/model-catalog-refresh.js +34 -0
- package/dist/modes/interactive/model-catalog-refresh.js.map +1 -0
- package/dist/modes/interactive/theme/apex.json +93 -0
- package/dist/modes/interactive/theme/dark.json +2 -0
- package/dist/modes/interactive/theme/light.json +3 -1
- package/dist/modes/interactive/theme/theme-controller.d.ts +10 -2
- package/dist/modes/interactive/theme/theme-controller.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme-controller.js +25 -11
- package/dist/modes/interactive/theme/theme-controller.js.map +1 -1
- package/dist/modes/interactive/theme/theme-schema.json +11 -3
- package/dist/modes/interactive/theme/theme.d.ts +14 -3
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +65 -10
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/json-event.d.ts +18 -12
- package/dist/modes/json-event.d.ts.map +1 -1
- package/dist/modes/json-event.js +22 -5
- package/dist/modes/json-event.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +2 -1
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/package-manager-cli.d.ts +1 -0
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +211 -14
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/rpc-entry.d.ts.map +1 -1
- package/dist/rpc-entry.js +2 -1
- package/dist/rpc-entry.js.map +1 -1
- package/dist/server/create-harness.d.ts +1 -1
- package/dist/server/create-harness.d.ts.map +1 -1
- package/dist/server/create-harness.js +8 -5
- package/dist/server/create-harness.js.map +1 -1
- package/dist/testing/replay/metrics.d.ts +24 -0
- package/dist/testing/replay/metrics.d.ts.map +1 -0
- package/dist/testing/replay/metrics.js +35 -0
- package/dist/testing/replay/metrics.js.map +1 -0
- package/dist/testing/replay/recorded-provider.d.ts +10 -0
- package/dist/testing/replay/recorded-provider.d.ts.map +1 -0
- package/dist/testing/replay/recorded-provider.js +108 -0
- package/dist/testing/replay/recorded-provider.js.map +1 -0
- package/dist/testing/replay/runner.d.ts +56 -0
- package/dist/testing/replay/runner.d.ts.map +1 -0
- package/dist/testing/replay/runner.js +406 -0
- package/dist/testing/replay/runner.js.map +1 -0
- package/dist/themes/apex-logo.d.ts +41 -0
- package/dist/themes/apex-logo.d.ts.map +1 -0
- package/dist/themes/apex-logo.js +35 -0
- package/dist/themes/apex-logo.js.map +1 -0
- package/dist/utils/apex-code-user-agent.d.ts +2 -0
- package/dist/utils/apex-code-user-agent.d.ts.map +1 -0
- package/dist/utils/apex-code-user-agent.js +5 -0
- package/dist/utils/apex-code-user-agent.js.map +1 -0
- package/dist/utils/changelog.d.ts +7 -2
- package/dist/utils/changelog.d.ts.map +1 -1
- package/dist/utils/changelog.js +82 -13
- package/dist/utils/changelog.js.map +1 -1
- package/dist/utils/clipboard-image.d.ts.map +1 -1
- package/dist/utils/clipboard-image.js +1 -1
- package/dist/utils/clipboard-image.js.map +1 -1
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +2 -1
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/management-http.d.ts +5 -3
- package/dist/utils/management-http.d.ts.map +1 -1
- package/dist/utils/management-http.js +14 -10
- package/dist/utils/management-http.js.map +1 -1
- package/dist/utils/shell.d.ts +3 -0
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +17 -5
- package/dist/utils/shell.js.map +1 -1
- package/dist/utils/syntax-highlight.d.ts +1 -0
- package/dist/utils/syntax-highlight.d.ts.map +1 -1
- package/dist/utils/syntax-highlight.js +60 -1
- package/dist/utils/syntax-highlight.js.map +1 -1
- package/dist/utils/text.d.ts +8 -0
- package/dist/utils/text.d.ts.map +1 -0
- package/dist/utils/text.js +9 -0
- package/dist/utils/text.js.map +1 -0
- package/dist/utils/tools-manager.d.ts +70 -2
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +280 -124
- package/dist/utils/tools-manager.js.map +1 -1
- package/dist/utils/version-check.d.ts +7 -5
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +25 -14
- package/dist/utils/version-check.js.map +1 -1
- package/dist/utils/windows-self-update.d.ts.map +1 -1
- package/dist/utils/windows-self-update.js +1 -1
- package/dist/utils/windows-self-update.js.map +1 -1
- package/docs/compaction.md +39 -24
- package/docs/containerization.md +24 -21
- package/docs/custom-provider.md +15 -12
- package/docs/development.md +11 -11
- package/docs/environment-variables.md +37 -77
- package/docs/extensions.md +68 -54
- package/docs/index.md +12 -24
- package/docs/json.md +18 -11
- package/docs/keybindings.md +18 -12
- package/docs/llama-cpp.md +9 -7
- package/docs/models.md +17 -11
- package/docs/packages.md +47 -43
- package/docs/prompt-templates.md +3 -3
- package/docs/providers.md +10 -10
- package/docs/quickstart.md +49 -32
- package/docs/rpc.md +35 -18
- package/docs/sdk.md +20 -6
- package/docs/security.md +18 -18
- package/docs/session-format.md +9 -9
- package/docs/sessions.md +13 -13
- package/docs/settings.md +83 -16
- package/docs/shell-aliases.md +1 -1
- package/docs/skills.md +66 -15
- package/docs/terminal-setup.md +44 -19
- package/docs/termux.md +4 -4
- package/docs/themes.md +32 -11
- package/docs/tmux.md +4 -4
- package/docs/tui.md +5 -5
- package/docs/usage.md +61 -53
- package/docs/windows.md +24 -2
- package/examples/extensions/border-status-editor.ts +1 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +2 -2
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/git-checkpoint.ts +2 -2
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/notify.ts +3 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/subagent/README.md +3 -1
- package/examples/extensions/subagent/agents.ts +40 -9
- package/examples/extensions/subagent/index.ts +27 -4
- package/examples/extensions/titlebar-spinner.ts +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/rpc-extension-ui.ts +1 -1
- package/examples/sdk/06-extensions.ts +1 -1
- package/examples/sdk/README.md +1 -1
- package/npm-shrinkwrap.json +280 -445
- package/package.json +9 -12
- package/dist/core/telemetry.d.ts +0 -3
- package/dist/core/telemetry.d.ts.map +0 -1
- package/dist/core/telemetry.js +0 -9
- package/dist/core/telemetry.js.map +0 -1
- package/dist/utils/pi-user-agent.d.ts +0 -2
- package/dist/utils/pi-user-agent.d.ts.map +0 -1
- package/dist/utils/pi-user-agent.js +0 -5
- package/dist/utils/pi-user-agent.js.map +0 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { chmodSync, existsSync, unlinkSync } from "node:fs";
|
|
2
|
+
import * as net from "node:net";
|
|
3
|
+
import { FrameReader, isRequestObject, writeFrame } from "./framing.js";
|
|
4
|
+
const MAX_CONNECTIONS = 32;
|
|
5
|
+
/**
|
|
6
|
+
* Serve git credentials to the sandboxed child without ever putting one inside it.
|
|
7
|
+
*
|
|
8
|
+
* ADR 0015 keeps credentials host-owned and hands the child a read-only projection. A git
|
|
9
|
+
* credential cannot be projected the same way: it is not one file with a stable shape, it
|
|
10
|
+
* is whatever the host's own helper produces per host. So the credential stays on the host
|
|
11
|
+
* and only the answer to one question crosses, for one host, once it has been released.
|
|
12
|
+
*
|
|
13
|
+
* Two gates, and the first is the one that matters most.
|
|
14
|
+
*
|
|
15
|
+
* The host must be reachable by this session. git only asks for a credential after a
|
|
16
|
+
* server challenged it, so in the ordinary flow the host was already allowed or approved
|
|
17
|
+
* at the network layer. A request for a host the session cannot reach is therefore not git
|
|
18
|
+
* doing its job; it is something that went looking for a token. Refusing it costs nothing
|
|
19
|
+
* real and removes the case where this channel is more useful to an attacker than to git.
|
|
20
|
+
*
|
|
21
|
+
* The human must release it. Per ADR 0023 that decision is the supervisor's, because this
|
|
22
|
+
* socket has no peer authentication -- every descendant in the child's namespace can reach
|
|
23
|
+
* it, which is precisely the code the boundary exists to contain. Without a releaser the
|
|
24
|
+
* channel refuses outright, so a headless session cannot hand out a credential at all.
|
|
25
|
+
*
|
|
26
|
+
* A release covers one host for the session. It is never persisted and never widened to a
|
|
27
|
+
* second host, which is what keeps an approval for a push from also releasing a token to
|
|
28
|
+
* whatever else the session later contacts.
|
|
29
|
+
*/
|
|
30
|
+
export function createGitCredentialProxy(options) {
|
|
31
|
+
/** Hosts released this session. Never persisted, never widened. */
|
|
32
|
+
const released = new Set();
|
|
33
|
+
const pending = new Map();
|
|
34
|
+
function audit(host, detail) {
|
|
35
|
+
options.violationStore?.add({
|
|
36
|
+
kind: "network",
|
|
37
|
+
command: `git credential ${host}`,
|
|
38
|
+
detail,
|
|
39
|
+
timestamp: new Date(),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
async function release(host) {
|
|
43
|
+
if (released.has(host))
|
|
44
|
+
return true;
|
|
45
|
+
if (!options.requestRelease)
|
|
46
|
+
return false;
|
|
47
|
+
const inFlight = pending.get(host);
|
|
48
|
+
if (inFlight)
|
|
49
|
+
return inFlight;
|
|
50
|
+
const question = (async () => {
|
|
51
|
+
try {
|
|
52
|
+
const approved = await options.requestRelease(host);
|
|
53
|
+
if (approved)
|
|
54
|
+
released.add(host);
|
|
55
|
+
return approved;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
pending.delete(host);
|
|
62
|
+
}
|
|
63
|
+
})();
|
|
64
|
+
pending.set(host, question);
|
|
65
|
+
return question;
|
|
66
|
+
}
|
|
67
|
+
async function answer(request) {
|
|
68
|
+
if (!isRequestObject(request))
|
|
69
|
+
return { ok: false, error: "Malformed request." };
|
|
70
|
+
if (request.op !== "get") {
|
|
71
|
+
// Only `get` crosses. `store` and `erase` would let the child rewrite the host's
|
|
72
|
+
// credential store, which ADR 0015 keeps as an explicit host operation.
|
|
73
|
+
return { ok: false, error: "Only credential reads are served over this channel." };
|
|
74
|
+
}
|
|
75
|
+
const host = typeof request.host === "string" ? request.host : "";
|
|
76
|
+
const protocol = typeof request.protocol === "string" ? request.protocol : "https";
|
|
77
|
+
if (!host)
|
|
78
|
+
return { ok: false, error: "Request named no host." };
|
|
79
|
+
if (!options.isHostAllowed(host)) {
|
|
80
|
+
audit(host, `Refused a credential for ${host}, which this session may not reach.`);
|
|
81
|
+
return { ok: false, error: "Host is not reachable by this session." };
|
|
82
|
+
}
|
|
83
|
+
if (!(await release(host))) {
|
|
84
|
+
audit(host, `Credential for ${host} was not released.`);
|
|
85
|
+
return { ok: false, error: "Credential was not released." };
|
|
86
|
+
}
|
|
87
|
+
const credential = await options.fillCredential({ host, protocol });
|
|
88
|
+
if (!credential) {
|
|
89
|
+
audit(host, `The host credential store had nothing for ${host}.`);
|
|
90
|
+
return { ok: false, error: "No credential is configured for that host." };
|
|
91
|
+
}
|
|
92
|
+
return { ok: true, username: credential.username, password: credential.password };
|
|
93
|
+
}
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
// A prior launch that never closed leaves a stale socket file that listen() refuses
|
|
96
|
+
// to bind over, exactly as the network proxy handles.
|
|
97
|
+
if (existsSync(options.socketPath))
|
|
98
|
+
unlinkSync(options.socketPath);
|
|
99
|
+
const sockets = new Set();
|
|
100
|
+
const server = net.createServer((socket) => {
|
|
101
|
+
if (sockets.size >= MAX_CONNECTIONS) {
|
|
102
|
+
socket.destroy();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
sockets.add(socket);
|
|
106
|
+
socket.on("close", () => sockets.delete(socket));
|
|
107
|
+
socket.on("error", () => socket.destroy());
|
|
108
|
+
const reader = new FrameReader(socket, "Git credential channel", (detail) => audit("(protocol)", `Refused: ${detail}`));
|
|
109
|
+
void (async () => {
|
|
110
|
+
try {
|
|
111
|
+
const response = await answer(await reader.next());
|
|
112
|
+
writeFrame(socket, response);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// A disconnected or malformed client is the reader's business; there is
|
|
116
|
+
// nothing to answer and nothing to record beyond what it already did.
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
socket.end();
|
|
120
|
+
}
|
|
121
|
+
})();
|
|
122
|
+
});
|
|
123
|
+
server.on("error", reject);
|
|
124
|
+
server.listen(options.socketPath, () => {
|
|
125
|
+
server.off("error", reject);
|
|
126
|
+
try {
|
|
127
|
+
chmodSync(options.socketPath, 0o600);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// The containing directory is already 0700; the mode here is defence in depth.
|
|
131
|
+
}
|
|
132
|
+
resolve({
|
|
133
|
+
close: () => new Promise((closed) => {
|
|
134
|
+
for (const socket of sockets)
|
|
135
|
+
socket.destroy();
|
|
136
|
+
server.close(() => closed());
|
|
137
|
+
}),
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=git-credential-proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-credential-proxy.js","sourceRoot":"","sources":["../../../../src/core/sandbox/rpc/git-credential-proxy.ts"],"names":[],"mappings":"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","sourcesContent":["import { chmodSync, existsSync, unlinkSync } from \"node:fs\";\nimport * as net from \"node:net\";\nimport type { SandboxViolationStore } from \"../violations.ts\";\nimport { FrameReader, isRequestObject, writeFrame } from \"./framing.ts\";\n\nconst MAX_CONNECTIONS = 32;\n\nexport interface GitCredentialRequest {\n\treadonly protocol: string;\n\treadonly host: string;\n}\n\nexport interface GitCredential {\n\treadonly username: string;\n\treadonly password: string;\n}\n\nexport interface GitCredentialProxy {\n\tclose(): Promise<void>;\n}\n\n/**\n * Serve git credentials to the sandboxed child without ever putting one inside it.\n *\n * ADR 0015 keeps credentials host-owned and hands the child a read-only projection. A git\n * credential cannot be projected the same way: it is not one file with a stable shape, it\n * is whatever the host's own helper produces per host. So the credential stays on the host\n * and only the answer to one question crosses, for one host, once it has been released.\n *\n * Two gates, and the first is the one that matters most.\n *\n * The host must be reachable by this session. git only asks for a credential after a\n * server challenged it, so in the ordinary flow the host was already allowed or approved\n * at the network layer. A request for a host the session cannot reach is therefore not git\n * doing its job; it is something that went looking for a token. Refusing it costs nothing\n * real and removes the case where this channel is more useful to an attacker than to git.\n *\n * The human must release it. Per ADR 0023 that decision is the supervisor's, because this\n * socket has no peer authentication -- every descendant in the child's namespace can reach\n * it, which is precisely the code the boundary exists to contain. Without a releaser the\n * channel refuses outright, so a headless session cannot hand out a credential at all.\n *\n * A release covers one host for the session. It is never persisted and never widened to a\n * second host, which is what keeps an approval for a push from also releasing a token to\n * whatever else the session later contacts.\n */\nexport function createGitCredentialProxy(options: {\n\tsocketPath: string;\n\t/** Whether this session may reach the host at all. */\n\tisHostAllowed: (host: string) => boolean;\n\t/** Ask a human to release the host's credential. Absent means refuse without asking. */\n\trequestRelease?: (host: string) => Promise<boolean>;\n\t/** Resolve the credential on the host, using the host's own configuration. */\n\tfillCredential: (request: GitCredentialRequest) => Promise<GitCredential | undefined>;\n\tviolationStore?: SandboxViolationStore;\n}): Promise<GitCredentialProxy> {\n\t/** Hosts released this session. Never persisted, never widened. */\n\tconst released = new Set<string>();\n\tconst pending = new Map<string, Promise<boolean>>();\n\n\tfunction audit(host: string, detail: string): void {\n\t\toptions.violationStore?.add({\n\t\t\tkind: \"network\",\n\t\t\tcommand: `git credential ${host}`,\n\t\t\tdetail,\n\t\t\ttimestamp: new Date(),\n\t\t});\n\t}\n\n\tasync function release(host: string): Promise<boolean> {\n\t\tif (released.has(host)) return true;\n\t\tif (!options.requestRelease) return false;\n\t\tconst inFlight = pending.get(host);\n\t\tif (inFlight) return inFlight;\n\t\tconst question = (async () => {\n\t\t\ttry {\n\t\t\t\tconst approved = await (options.requestRelease as (h: string) => Promise<boolean>)(host);\n\t\t\t\tif (approved) released.add(host);\n\t\t\t\treturn approved;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t} finally {\n\t\t\t\tpending.delete(host);\n\t\t\t}\n\t\t})();\n\t\tpending.set(host, question);\n\t\treturn question;\n\t}\n\n\tasync function answer(request: unknown): Promise<object> {\n\t\tif (!isRequestObject(request)) return { ok: false, error: \"Malformed request.\" };\n\t\tif (request.op !== \"get\") {\n\t\t\t// Only `get` crosses. `store` and `erase` would let the child rewrite the host's\n\t\t\t// credential store, which ADR 0015 keeps as an explicit host operation.\n\t\t\treturn { ok: false, error: \"Only credential reads are served over this channel.\" };\n\t\t}\n\t\tconst host = typeof request.host === \"string\" ? request.host : \"\";\n\t\tconst protocol = typeof request.protocol === \"string\" ? request.protocol : \"https\";\n\t\tif (!host) return { ok: false, error: \"Request named no host.\" };\n\n\t\tif (!options.isHostAllowed(host)) {\n\t\t\taudit(host, `Refused a credential for ${host}, which this session may not reach.`);\n\t\t\treturn { ok: false, error: \"Host is not reachable by this session.\" };\n\t\t}\n\t\tif (!(await release(host))) {\n\t\t\taudit(host, `Credential for ${host} was not released.`);\n\t\t\treturn { ok: false, error: \"Credential was not released.\" };\n\t\t}\n\t\tconst credential = await options.fillCredential({ host, protocol });\n\t\tif (!credential) {\n\t\t\taudit(host, `The host credential store had nothing for ${host}.`);\n\t\t\treturn { ok: false, error: \"No credential is configured for that host.\" };\n\t\t}\n\t\treturn { ok: true, username: credential.username, password: credential.password };\n\t}\n\n\treturn new Promise((resolve, reject) => {\n\t\t// A prior launch that never closed leaves a stale socket file that listen() refuses\n\t\t// to bind over, exactly as the network proxy handles.\n\t\tif (existsSync(options.socketPath)) unlinkSync(options.socketPath);\n\n\t\tconst sockets = new Set<net.Socket>();\n\t\tconst server = net.createServer((socket) => {\n\t\t\tif (sockets.size >= MAX_CONNECTIONS) {\n\t\t\t\tsocket.destroy();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsockets.add(socket);\n\t\t\tsocket.on(\"close\", () => sockets.delete(socket));\n\t\t\tsocket.on(\"error\", () => socket.destroy());\n\t\t\tconst reader = new FrameReader(socket, \"Git credential channel\", (detail) =>\n\t\t\t\taudit(\"(protocol)\", `Refused: ${detail}`),\n\t\t\t);\n\t\t\tvoid (async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst response = await answer(await reader.next());\n\t\t\t\t\twriteFrame(socket, response);\n\t\t\t\t} catch {\n\t\t\t\t\t// A disconnected or malformed client is the reader's business; there is\n\t\t\t\t\t// nothing to answer and nothing to record beyond what it already did.\n\t\t\t\t} finally {\n\t\t\t\t\tsocket.end();\n\t\t\t\t}\n\t\t\t})();\n\t\t});\n\n\t\tserver.on(\"error\", reject);\n\t\tserver.listen(options.socketPath, () => {\n\t\t\tserver.off(\"error\", reject);\n\t\t\ttry {\n\t\t\t\tchmodSync(options.socketPath, 0o600);\n\t\t\t} catch {\n\t\t\t\t// The containing directory is already 0700; the mode here is defence in depth.\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tclose: () =>\n\t\t\t\t\tnew Promise((closed) => {\n\t\t\t\t\t\tfor (const socket of sockets) socket.destroy();\n\t\t\t\t\t\tserver.close(() => closed());\n\t\t\t\t\t}),\n\t\t\t});\n\t\t});\n\t});\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The directory the supervisor puts its own private state in.
|
|
3
|
+
*
|
|
4
|
+
* `/tmp` on POSIX rather than `TMPDIR`, deliberately, for two reasons the channel code
|
|
5
|
+
* depends on. `TMPDIR` can point inside the workspace on macOS, which the sandbox may
|
|
6
|
+
* write, and a socket has to fit inside `AF_UNIX`'s 108-byte `sun_path`, which an
|
|
7
|
+
* unusually long `TMPDIR` exhausts.
|
|
8
|
+
*
|
|
9
|
+
* Windows has neither `/tmp` nor Unix sockets. Nothing here is reachable in a Windows
|
|
10
|
+
* session, because ADR 0005 leaves that platform unsupported and both backends report
|
|
11
|
+
* `unavailable` before a launch gets this far. It still has to resolve to something real:
|
|
12
|
+
* these helpers are called before the backend's platform check in one path, and off-platform
|
|
13
|
+
* in the tests that exercise violation attribution without a macOS host. Returning
|
|
14
|
+
* `os.tmpdir()` there turns a confusing `ENOENT: mkdtemp '/tmp/...'` into the accurate
|
|
15
|
+
* "OS sandbox is supported on macOS only."
|
|
16
|
+
*/
|
|
17
|
+
export declare function supervisorTempDirectory(): string;
|
|
18
|
+
//# sourceMappingURL=supervisor-temp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supervisor-temp.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/supervisor-temp.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAEhD","sourcesContent":["import { tmpdir } from \"node:os\";\n\n/**\n * The directory the supervisor puts its own private state in.\n *\n * `/tmp` on POSIX rather than `TMPDIR`, deliberately, for two reasons the channel code\n * depends on. `TMPDIR` can point inside the workspace on macOS, which the sandbox may\n * write, and a socket has to fit inside `AF_UNIX`'s 108-byte `sun_path`, which an\n * unusually long `TMPDIR` exhausts.\n *\n * Windows has neither `/tmp` nor Unix sockets. Nothing here is reachable in a Windows\n * session, because ADR 0005 leaves that platform unsupported and both backends report\n * `unavailable` before a launch gets this far. It still has to resolve to something real:\n * these helpers are called before the backend's platform check in one path, and off-platform\n * in the tests that exercise violation attribution without a macOS host. Returning\n * `os.tmpdir()` there turns a confusing `ENOENT: mkdtemp '/tmp/...'` into the accurate\n * \"OS sandbox is supported on macOS only.\"\n */\nexport function supervisorTempDirectory(): string {\n\treturn process.platform === \"win32\" ? tmpdir() : \"/tmp\";\n}\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { tmpdir } from "node:os";
|
|
2
|
+
/**
|
|
3
|
+
* The directory the supervisor puts its own private state in.
|
|
4
|
+
*
|
|
5
|
+
* `/tmp` on POSIX rather than `TMPDIR`, deliberately, for two reasons the channel code
|
|
6
|
+
* depends on. `TMPDIR` can point inside the workspace on macOS, which the sandbox may
|
|
7
|
+
* write, and a socket has to fit inside `AF_UNIX`'s 108-byte `sun_path`, which an
|
|
8
|
+
* unusually long `TMPDIR` exhausts.
|
|
9
|
+
*
|
|
10
|
+
* Windows has neither `/tmp` nor Unix sockets. Nothing here is reachable in a Windows
|
|
11
|
+
* session, because ADR 0005 leaves that platform unsupported and both backends report
|
|
12
|
+
* `unavailable` before a launch gets this far. It still has to resolve to something real:
|
|
13
|
+
* these helpers are called before the backend's platform check in one path, and off-platform
|
|
14
|
+
* in the tests that exercise violation attribution without a macOS host. Returning
|
|
15
|
+
* `os.tmpdir()` there turns a confusing `ENOENT: mkdtemp '/tmp/...'` into the accurate
|
|
16
|
+
* "OS sandbox is supported on macOS only."
|
|
17
|
+
*/
|
|
18
|
+
export function supervisorTempDirectory() {
|
|
19
|
+
return process.platform === "win32" ? tmpdir() : "/tmp";
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=supervisor-temp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supervisor-temp.js","sourceRoot":"","sources":["../../../src/core/sandbox/supervisor-temp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEjC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,uBAAuB,GAAW;IACjD,OAAO,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;AAAA,CACxD","sourcesContent":["import { tmpdir } from \"node:os\";\n\n/**\n * The directory the supervisor puts its own private state in.\n *\n * `/tmp` on POSIX rather than `TMPDIR`, deliberately, for two reasons the channel code\n * depends on. `TMPDIR` can point inside the workspace on macOS, which the sandbox may\n * write, and a socket has to fit inside `AF_UNIX`'s 108-byte `sun_path`, which an\n * unusually long `TMPDIR` exhausts.\n *\n * Windows has neither `/tmp` nor Unix sockets. Nothing here is reachable in a Windows\n * session, because ADR 0005 leaves that platform unsupported and both backends report\n * `unavailable` before a launch gets this far. It still has to resolve to something real:\n * these helpers are called before the backend's platform check in one path, and off-platform\n * in the tests that exercise violation attribution without a macOS host. Returning\n * `os.tmpdir()` there turns a confusing `ENOENT: mkdtemp '/tmp/...'` into the accurate\n * \"OS sandbox is supported on macOS only.\"\n */\nexport function supervisorTempDirectory(): string {\n\treturn process.platform === \"win32\" ? tmpdir() : \"/tmp\";\n}\n"]}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type SandboxPolicy, type SandboxStatus } from "./policy.ts";
|
|
2
|
+
export interface SandboxLaunch {
|
|
3
|
+
readonly command: string;
|
|
4
|
+
readonly args: readonly string[];
|
|
5
|
+
readonly policy: SandboxPolicy;
|
|
6
|
+
readonly environment?: NodeJS.ProcessEnv;
|
|
7
|
+
/** Application/runtime directories needed by the child but never writable by it. */
|
|
8
|
+
readonly readOnlyPaths?: readonly string[];
|
|
9
|
+
/** Individual read-only files, such as a host-owned credential file. */
|
|
10
|
+
readonly readOnlyFiles?: readonly string[];
|
|
11
|
+
/**
|
|
12
|
+
* Host executables projected read-only at an exact path inside the child. The
|
|
13
|
+
* destination is chosen by the caller because only it knows where the child will
|
|
14
|
+
* look; the backend just places the file there without write access.
|
|
15
|
+
*/
|
|
16
|
+
readonly readOnlyBinaries?: readonly {
|
|
17
|
+
readonly source: string;
|
|
18
|
+
readonly destination: string;
|
|
19
|
+
}[];
|
|
20
|
+
/**
|
|
21
|
+
* The supervisor-owned credential write channel, when one was opened for this
|
|
22
|
+
* launch. `hostSocketPath` is where the supervisor's writer listens; the backend
|
|
23
|
+
* projects it to `childSocketPath`, which is also what the child is told through
|
|
24
|
+
* `APEX_CREDENTIAL_PROXY_PATH`. Absent means the session has no credential write
|
|
25
|
+
* path at all -- the mount stays read-only with no exception.
|
|
26
|
+
*/
|
|
27
|
+
readonly credentialChannel?: {
|
|
28
|
+
readonly hostSocketPath: string;
|
|
29
|
+
readonly childSocketPath: string;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** A platform adapter. Its sole job is to launch a normal Apex child inside an OS boundary. */
|
|
33
|
+
export interface SandboxBackend {
|
|
34
|
+
readonly status: SandboxStatus;
|
|
35
|
+
launch(launch: SandboxLaunch): Promise<number>;
|
|
36
|
+
close(): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
export interface SandboxSupervisor {
|
|
39
|
+
readonly status: SandboxStatus;
|
|
40
|
+
launch(options: Omit<SandboxLaunch, "policy">): Promise<number>;
|
|
41
|
+
close(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Owns fail-closed policy propagation and lifecycle; OS details remain behind the
|
|
45
|
+
* backend so all testable security semantics are Apex-owned.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createSandboxSupervisor(options: {
|
|
48
|
+
backend: SandboxBackend;
|
|
49
|
+
policy: SandboxPolicy;
|
|
50
|
+
}): SandboxSupervisor;
|
|
51
|
+
//# sourceMappingURL=supervisor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/supervisor.ts"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,aAAa,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAEhG,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACzC,oFAAoF;IACpF,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,wEAAwE;IACxE,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACjG;;;;;;OAMG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE;QAC5B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;QAChC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;KACjC,CAAC;CACF;AAED,+FAA+F;AAC/F,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAChE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE;IAChD,OAAO,EAAE,cAAc,CAAC;IACxB,MAAM,EAAE,aAAa,CAAC;CACtB,GAAG,iBAAiB,CAWpB","sourcesContent":["import { requireSandboxEnforcement, type SandboxPolicy, type SandboxStatus } from \"./policy.ts\";\n\nexport interface SandboxLaunch {\n\treadonly command: string;\n\treadonly args: readonly string[];\n\treadonly policy: SandboxPolicy;\n\treadonly environment?: NodeJS.ProcessEnv;\n\t/** Application/runtime directories needed by the child but never writable by it. */\n\treadonly readOnlyPaths?: readonly string[];\n\t/** Individual read-only files, such as a host-owned credential file. */\n\treadonly readOnlyFiles?: readonly string[];\n\t/**\n\t * Host executables projected read-only at an exact path inside the child. The\n\t * destination is chosen by the caller because only it knows where the child will\n\t * look; the backend just places the file there without write access.\n\t */\n\treadonly readOnlyBinaries?: readonly { readonly source: string; readonly destination: string }[];\n\t/**\n\t * The supervisor-owned credential write channel, when one was opened for this\n\t * launch. `hostSocketPath` is where the supervisor's writer listens; the backend\n\t * projects it to `childSocketPath`, which is also what the child is told through\n\t * `APEX_CREDENTIAL_PROXY_PATH`. Absent means the session has no credential write\n\t * path at all -- the mount stays read-only with no exception.\n\t */\n\treadonly credentialChannel?: {\n\t\treadonly hostSocketPath: string;\n\t\treadonly childSocketPath: string;\n\t};\n}\n\n/** A platform adapter. Its sole job is to launch a normal Apex child inside an OS boundary. */\nexport interface SandboxBackend {\n\treadonly status: SandboxStatus;\n\tlaunch(launch: SandboxLaunch): Promise<number>;\n\tclose(): Promise<void>;\n}\n\nexport interface SandboxSupervisor {\n\treadonly status: SandboxStatus;\n\tlaunch(options: Omit<SandboxLaunch, \"policy\">): Promise<number>;\n\tclose(): Promise<void>;\n}\n\n/**\n * Owns fail-closed policy propagation and lifecycle; OS details remain behind the\n * backend so all testable security semantics are Apex-owned.\n */\nexport function createSandboxSupervisor(options: {\n\tbackend: SandboxBackend;\n\tpolicy: SandboxPolicy;\n}): SandboxSupervisor {\n\treturn {\n\t\tstatus: options.backend.status,\n\t\tasync launch(launch) {\n\t\t\trequireSandboxEnforcement(options.backend.status);\n\t\t\treturn options.backend.launch({ ...launch, policy: options.policy });\n\t\t},\n\t\tasync close() {\n\t\t\tawait options.backend.close();\n\t\t},\n\t};\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { requireSandboxEnforcement } from "./policy.js";
|
|
2
|
+
/**
|
|
3
|
+
* Owns fail-closed policy propagation and lifecycle; OS details remain behind the
|
|
4
|
+
* backend so all testable security semantics are Apex-owned.
|
|
5
|
+
*/
|
|
6
|
+
export function createSandboxSupervisor(options) {
|
|
7
|
+
return {
|
|
8
|
+
status: options.backend.status,
|
|
9
|
+
async launch(launch) {
|
|
10
|
+
requireSandboxEnforcement(options.backend.status);
|
|
11
|
+
return options.backend.launch({ ...launch, policy: options.policy });
|
|
12
|
+
},
|
|
13
|
+
async close() {
|
|
14
|
+
await options.backend.close();
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=supervisor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supervisor.js","sourceRoot":"","sources":["../../../src/core/sandbox/supervisor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAA0C,MAAM,aAAa,CAAC;AA2ChG;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAGvC,EAAqB;IACrB,OAAO;QACN,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,MAAM;QAC9B,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE;YACpB,yBAAyB,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YAClD,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QAAA,CACrE;QACD,KAAK,CAAC,KAAK,GAAG;YACb,MAAM,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QAAA,CAC9B;KACD,CAAC;AAAA,CACF","sourcesContent":["import { requireSandboxEnforcement, type SandboxPolicy, type SandboxStatus } from \"./policy.ts\";\n\nexport interface SandboxLaunch {\n\treadonly command: string;\n\treadonly args: readonly string[];\n\treadonly policy: SandboxPolicy;\n\treadonly environment?: NodeJS.ProcessEnv;\n\t/** Application/runtime directories needed by the child but never writable by it. */\n\treadonly readOnlyPaths?: readonly string[];\n\t/** Individual read-only files, such as a host-owned credential file. */\n\treadonly readOnlyFiles?: readonly string[];\n\t/**\n\t * Host executables projected read-only at an exact path inside the child. The\n\t * destination is chosen by the caller because only it knows where the child will\n\t * look; the backend just places the file there without write access.\n\t */\n\treadonly readOnlyBinaries?: readonly { readonly source: string; readonly destination: string }[];\n\t/**\n\t * The supervisor-owned credential write channel, when one was opened for this\n\t * launch. `hostSocketPath` is where the supervisor's writer listens; the backend\n\t * projects it to `childSocketPath`, which is also what the child is told through\n\t * `APEX_CREDENTIAL_PROXY_PATH`. Absent means the session has no credential write\n\t * path at all -- the mount stays read-only with no exception.\n\t */\n\treadonly credentialChannel?: {\n\t\treadonly hostSocketPath: string;\n\t\treadonly childSocketPath: string;\n\t};\n}\n\n/** A platform adapter. Its sole job is to launch a normal Apex child inside an OS boundary. */\nexport interface SandboxBackend {\n\treadonly status: SandboxStatus;\n\tlaunch(launch: SandboxLaunch): Promise<number>;\n\tclose(): Promise<void>;\n}\n\nexport interface SandboxSupervisor {\n\treadonly status: SandboxStatus;\n\tlaunch(options: Omit<SandboxLaunch, \"policy\">): Promise<number>;\n\tclose(): Promise<void>;\n}\n\n/**\n * Owns fail-closed policy propagation and lifecycle; OS details remain behind the\n * backend so all testable security semantics are Apex-owned.\n */\nexport function createSandboxSupervisor(options: {\n\tbackend: SandboxBackend;\n\tpolicy: SandboxPolicy;\n}): SandboxSupervisor {\n\treturn {\n\t\tstatus: options.backend.status,\n\t\tasync launch(launch) {\n\t\t\trequireSandboxEnforcement(options.backend.status);\n\t\t\treturn options.backend.launch({ ...launch, policy: options.policy });\n\t\t},\n\t\tasync close() {\n\t\t\tawait options.backend.close();\n\t\t},\n\t};\n}\n"]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lends the terminal from the sandboxed child back to the supervisor for a prompt.
|
|
3
|
+
*
|
|
4
|
+
* The supervisor has to draw the escalation prompt itself. Per ADR 0023 an approval
|
|
5
|
+
* asserted from inside the boundary is indistinguishable from one forged by the code the
|
|
6
|
+
* boundary exists to contain, so the human's answer must be read by the side that owns the
|
|
7
|
+
* decision. The child cannot read it in any case: it runs under `bwrap --new-session` and
|
|
8
|
+
* has no controlling terminal, which `terminal-size.ts` documents for the same reason.
|
|
9
|
+
*
|
|
10
|
+
* The transport is the one `terminal-size.ts` already established -- a file in the
|
|
11
|
+
* workspace, which is bind-mounted into the child, watched on the other side. Signals are
|
|
12
|
+
* unavailable because bwrap sits between the processes as PID 1 and forwards nothing.
|
|
13
|
+
*
|
|
14
|
+
* Two files rather than one, because the supervisor must know the child has actually let
|
|
15
|
+
* go of stdin before it reads an answer. The child inherits stdin and its TUI holds it in
|
|
16
|
+
* raw mode; prompting while both sides read would lose keystrokes to whichever won.
|
|
17
|
+
*
|
|
18
|
+
* The child is asked, never obeyed. A child that never acknowledges delays the prompt by
|
|
19
|
+
* the timeout and no longer, because a contained process that could withhold
|
|
20
|
+
* acknowledgement indefinitely would be able to veto the human's decision -- which is the
|
|
21
|
+
* authority failure ADR 0023 forbids. An unread prompt is a legibility failure, and that
|
|
22
|
+
* is the direction this is allowed to fail in.
|
|
23
|
+
*/
|
|
24
|
+
/** Env var naming the directory both sides agree on. */
|
|
25
|
+
export declare const TERMINAL_HANDOFF_PATH_VARIABLE = "APEX_TERMINAL_HANDOFF_PATH";
|
|
26
|
+
export interface TerminalHandoff {
|
|
27
|
+
/**
|
|
28
|
+
* Suspend the child, run `prompt` with the terminal to itself, then resume the child.
|
|
29
|
+
* Overlapping calls are serialised, so two prompts never share the terminal.
|
|
30
|
+
*/
|
|
31
|
+
borrowTerminal<T>(prompt: () => Promise<T>): Promise<T>;
|
|
32
|
+
stop(): void;
|
|
33
|
+
}
|
|
34
|
+
/** Supervisor side. */
|
|
35
|
+
export declare function createTerminalHandoff(directory: string, options?: {
|
|
36
|
+
acknowledgementTimeoutMs?: number;
|
|
37
|
+
}): TerminalHandoff;
|
|
38
|
+
/** Child side. Acknowledges a suspend once it has actually stopped drawing and reading. */
|
|
39
|
+
export declare function observeTerminalHandoff(directory: string, hooks: {
|
|
40
|
+
suspend: () => void | Promise<void>;
|
|
41
|
+
resume: () => void | Promise<void>;
|
|
42
|
+
}): {
|
|
43
|
+
stop: () => void;
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=terminal-handoff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal-handoff.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/terminal-handoff.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,wDAAwD;AACxD,eAAO,MAAM,8BAA8B,+BAA+B,CAAC;AAwB3E,MAAM,WAAW,eAAe;IAC/B;;;OAGG;IACH,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACxD,IAAI,IAAI,IAAI,CAAC;CACb;AAED,uBAAuB;AACvB,wBAAgB,qBAAqB,CACpC,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE;IAAE,wBAAwB,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7C,eAAe,CAgFjB;AAED,2FAA2F;AAC3F,wBAAgB,sBAAsB,CACrC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE;IAAE,OAAO,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAAC,MAAM,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,GAChF;IAAE,IAAI,EAAE,MAAM,IAAI,CAAA;CAAE,CAgDtB","sourcesContent":["import { type FSWatcher, readFileSync, rmSync, watch, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/**\n * Lends the terminal from the sandboxed child back to the supervisor for a prompt.\n *\n * The supervisor has to draw the escalation prompt itself. Per ADR 0023 an approval\n * asserted from inside the boundary is indistinguishable from one forged by the code the\n * boundary exists to contain, so the human's answer must be read by the side that owns the\n * decision. The child cannot read it in any case: it runs under `bwrap --new-session` and\n * has no controlling terminal, which `terminal-size.ts` documents for the same reason.\n *\n * The transport is the one `terminal-size.ts` already established -- a file in the\n * workspace, which is bind-mounted into the child, watched on the other side. Signals are\n * unavailable because bwrap sits between the processes as PID 1 and forwards nothing.\n *\n * Two files rather than one, because the supervisor must know the child has actually let\n * go of stdin before it reads an answer. The child inherits stdin and its TUI holds it in\n * raw mode; prompting while both sides read would lose keystrokes to whichever won.\n *\n * The child is asked, never obeyed. A child that never acknowledges delays the prompt by\n * the timeout and no longer, because a contained process that could withhold\n * acknowledgement indefinitely would be able to veto the human's decision -- which is the\n * authority failure ADR 0023 forbids. An unread prompt is a legibility failure, and that\n * is the direction this is allowed to fail in.\n */\n\n/** Env var naming the directory both sides agree on. */\nexport const TERMINAL_HANDOFF_PATH_VARIABLE = \"APEX_TERMINAL_HANDOFF_PATH\";\n\nconst STATE_FILE = \"terminal-handoff\";\nconst ACKNOWLEDGEMENT_FILE = \"terminal-handoff-ack\";\nconst DEFAULT_ACKNOWLEDGEMENT_TIMEOUT_MS = 1_000;\n\n/**\n * How often each side re-reads the state file, independently of the watcher.\n *\n * `fs.watch` is a thin wrapper over whatever the platform provides, and the platforms do\n * not agree. macOS CI delivered a resume more than a second after it was written, and\n * intermittently: the event arrives, late enough that a TUI would sit visibly frozen\n * after the human had already answered. The watcher still does the work in the common\n * case; this only bounds how wrong it can be.\n */\nconst POLL_INTERVAL_MS = 100;\n\ntype HandoffState = \"suspend\" | \"resume\";\n\nfunction parseState(contents: string): HandoffState | undefined {\n\tconst value = contents.trim();\n\treturn value === \"suspend\" || value === \"resume\" ? value : undefined;\n}\n\nexport interface TerminalHandoff {\n\t/**\n\t * Suspend the child, run `prompt` with the terminal to itself, then resume the child.\n\t * Overlapping calls are serialised, so two prompts never share the terminal.\n\t */\n\tborrowTerminal<T>(prompt: () => Promise<T>): Promise<T>;\n\tstop(): void;\n}\n\n/** Supervisor side. */\nexport function createTerminalHandoff(\n\tdirectory: string,\n\toptions?: { acknowledgementTimeoutMs?: number },\n): TerminalHandoff {\n\tconst statePath = join(directory, STATE_FILE);\n\tconst acknowledgementPath = join(directory, ACKNOWLEDGEMENT_FILE);\n\tconst timeout = options?.acknowledgementTimeoutMs ?? DEFAULT_ACKNOWLEDGEMENT_TIMEOUT_MS;\n\t// Every borrow chains onto the last, which is what keeps two escalations that arrive\n\t// together from drawing over each other.\n\tlet queue: Promise<unknown> = Promise.resolve();\n\n\tfunction write(state: HandoffState): void {\n\t\ttry {\n\t\t\twriteFileSync(statePath, `${state}\\n`);\n\t\t} catch {\n\t\t\t// A handoff we cannot request still leaves the prompt readable often enough to\n\t\t\t// be worth attempting; it is never worth failing the escalation over.\n\t\t}\n\t}\n\n\tfunction waitForAcknowledgement(): Promise<void> {\n\t\treturn new Promise((resolve) => {\n\t\t\tlet watcher: FSWatcher | undefined;\n\t\t\tlet timer: NodeJS.Timeout | undefined;\n\t\t\tlet poll: NodeJS.Timeout | undefined;\n\t\t\tconst finish = () => {\n\t\t\t\tif (timer) clearTimeout(timer);\n\t\t\t\tif (poll) clearInterval(poll);\n\t\t\t\twatcher?.close();\n\t\t\t\tresolve();\n\t\t\t};\n\t\t\tconst check = () => {\n\t\t\t\ttry {\n\t\t\t\t\tif (readFileSync(acknowledgementPath, \"utf8\").trim() === \"suspended\") finish();\n\t\t\t\t} catch {\n\t\t\t\t\t// Not written yet.\n\t\t\t\t}\n\t\t\t};\n\t\t\ttimer = setTimeout(finish, timeout);\n\t\t\ttry {\n\t\t\t\twatcher = watch(directory, check);\n\t\t\t} catch {\n\t\t\t\t// Without a watcher the poll below still delivers, just less promptly.\n\t\t\t}\n\t\t\tpoll = setInterval(check, POLL_INTERVAL_MS);\n\t\t\tpoll.unref();\n\t\t\tcheck();\n\t\t});\n\t}\n\n\tasync function run<T>(prompt: () => Promise<T>): Promise<T> {\n\t\ttry {\n\t\t\trmSync(acknowledgementPath, { force: true });\n\t\t} catch {\n\t\t\t// A stale acknowledgement is handled by the content check, not by its absence.\n\t\t}\n\t\twrite(\"suspend\");\n\t\tawait waitForAcknowledgement();\n\t\ttry {\n\t\t\treturn await prompt();\n\t\t} finally {\n\t\t\twrite(\"resume\");\n\t\t}\n\t}\n\n\treturn {\n\t\tborrowTerminal<T>(prompt: () => Promise<T>): Promise<T> {\n\t\t\tconst next = queue.then(\n\t\t\t\t() => run(prompt),\n\t\t\t\t() => run(prompt),\n\t\t\t);\n\t\t\t// Swallowed here only so one rejected borrow does not poison the queue for the\n\t\t\t// next; the caller still receives the rejection through `next`.\n\t\t\tqueue = next.then(\n\t\t\t\t() => undefined,\n\t\t\t\t() => undefined,\n\t\t\t);\n\t\t\treturn next;\n\t\t},\n\t\tstop(): void {\n\t\t\twrite(\"resume\");\n\t\t},\n\t};\n}\n\n/** Child side. Acknowledges a suspend once it has actually stopped drawing and reading. */\nexport function observeTerminalHandoff(\n\tdirectory: string,\n\thooks: { suspend: () => void | Promise<void>; resume: () => void | Promise<void> },\n): { stop: () => void } {\n\tconst statePath = join(directory, STATE_FILE);\n\tconst acknowledgementPath = join(directory, ACKNOWLEDGEMENT_FILE);\n\tlet current: HandoffState = \"resume\";\n\tlet applying: Promise<void> = Promise.resolve();\n\n\tconst apply = () => {\n\t\tlet state: HandoffState | undefined;\n\t\ttry {\n\t\t\tstate = parseState(readFileSync(statePath, \"utf8\"));\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t\tif (!state || state === current) return;\n\t\tcurrent = state;\n\t\tapplying = applying.then(async () => {\n\t\t\tif (state === \"suspend\") {\n\t\t\t\tawait hooks.suspend();\n\t\t\t\ttry {\n\t\t\t\t\twriteFileSync(acknowledgementPath, \"suspended\\n\");\n\t\t\t\t} catch {\n\t\t\t\t\t// The supervisor's timeout covers an acknowledgement we cannot write.\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tawait hooks.resume();\n\t\t\t}\n\t\t});\n\t};\n\n\tapply();\n\n\tlet watcher: FSWatcher | undefined;\n\ttry {\n\t\twatcher = watch(directory, apply);\n\t} catch {\n\t\t// Without a watcher the poll below still delivers, just less promptly.\n\t}\n\tconst poll = setInterval(apply, POLL_INTERVAL_MS);\n\t// Never hold the process open for this. The child exits on its own schedule and a\n\t// handoff that stops being observed at shutdown has nothing left to deliver.\n\tpoll.unref();\n\n\treturn {\n\t\tstop: () => {\n\t\t\twatcher?.close();\n\t\t\tclearInterval(poll);\n\t\t},\n\t};\n}\n"]}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { readFileSync, rmSync, watch, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Lends the terminal from the sandboxed child back to the supervisor for a prompt.
|
|
5
|
+
*
|
|
6
|
+
* The supervisor has to draw the escalation prompt itself. Per ADR 0023 an approval
|
|
7
|
+
* asserted from inside the boundary is indistinguishable from one forged by the code the
|
|
8
|
+
* boundary exists to contain, so the human's answer must be read by the side that owns the
|
|
9
|
+
* decision. The child cannot read it in any case: it runs under `bwrap --new-session` and
|
|
10
|
+
* has no controlling terminal, which `terminal-size.ts` documents for the same reason.
|
|
11
|
+
*
|
|
12
|
+
* The transport is the one `terminal-size.ts` already established -- a file in the
|
|
13
|
+
* workspace, which is bind-mounted into the child, watched on the other side. Signals are
|
|
14
|
+
* unavailable because bwrap sits between the processes as PID 1 and forwards nothing.
|
|
15
|
+
*
|
|
16
|
+
* Two files rather than one, because the supervisor must know the child has actually let
|
|
17
|
+
* go of stdin before it reads an answer. The child inherits stdin and its TUI holds it in
|
|
18
|
+
* raw mode; prompting while both sides read would lose keystrokes to whichever won.
|
|
19
|
+
*
|
|
20
|
+
* The child is asked, never obeyed. A child that never acknowledges delays the prompt by
|
|
21
|
+
* the timeout and no longer, because a contained process that could withhold
|
|
22
|
+
* acknowledgement indefinitely would be able to veto the human's decision -- which is the
|
|
23
|
+
* authority failure ADR 0023 forbids. An unread prompt is a legibility failure, and that
|
|
24
|
+
* is the direction this is allowed to fail in.
|
|
25
|
+
*/
|
|
26
|
+
/** Env var naming the directory both sides agree on. */
|
|
27
|
+
export const TERMINAL_HANDOFF_PATH_VARIABLE = "APEX_TERMINAL_HANDOFF_PATH";
|
|
28
|
+
const STATE_FILE = "terminal-handoff";
|
|
29
|
+
const ACKNOWLEDGEMENT_FILE = "terminal-handoff-ack";
|
|
30
|
+
const DEFAULT_ACKNOWLEDGEMENT_TIMEOUT_MS = 1_000;
|
|
31
|
+
/**
|
|
32
|
+
* How often each side re-reads the state file, independently of the watcher.
|
|
33
|
+
*
|
|
34
|
+
* `fs.watch` is a thin wrapper over whatever the platform provides, and the platforms do
|
|
35
|
+
* not agree. macOS CI delivered a resume more than a second after it was written, and
|
|
36
|
+
* intermittently: the event arrives, late enough that a TUI would sit visibly frozen
|
|
37
|
+
* after the human had already answered. The watcher still does the work in the common
|
|
38
|
+
* case; this only bounds how wrong it can be.
|
|
39
|
+
*/
|
|
40
|
+
const POLL_INTERVAL_MS = 100;
|
|
41
|
+
function parseState(contents) {
|
|
42
|
+
const value = contents.trim();
|
|
43
|
+
return value === "suspend" || value === "resume" ? value : undefined;
|
|
44
|
+
}
|
|
45
|
+
/** Supervisor side. */
|
|
46
|
+
export function createTerminalHandoff(directory, options) {
|
|
47
|
+
const statePath = join(directory, STATE_FILE);
|
|
48
|
+
const acknowledgementPath = join(directory, ACKNOWLEDGEMENT_FILE);
|
|
49
|
+
const timeout = options?.acknowledgementTimeoutMs ?? DEFAULT_ACKNOWLEDGEMENT_TIMEOUT_MS;
|
|
50
|
+
// Every borrow chains onto the last, which is what keeps two escalations that arrive
|
|
51
|
+
// together from drawing over each other.
|
|
52
|
+
let queue = Promise.resolve();
|
|
53
|
+
function write(state) {
|
|
54
|
+
try {
|
|
55
|
+
writeFileSync(statePath, `${state}\n`);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// A handoff we cannot request still leaves the prompt readable often enough to
|
|
59
|
+
// be worth attempting; it is never worth failing the escalation over.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function waitForAcknowledgement() {
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
let watcher;
|
|
65
|
+
let timer;
|
|
66
|
+
let poll;
|
|
67
|
+
const finish = () => {
|
|
68
|
+
if (timer)
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
if (poll)
|
|
71
|
+
clearInterval(poll);
|
|
72
|
+
watcher?.close();
|
|
73
|
+
resolve();
|
|
74
|
+
};
|
|
75
|
+
const check = () => {
|
|
76
|
+
try {
|
|
77
|
+
if (readFileSync(acknowledgementPath, "utf8").trim() === "suspended")
|
|
78
|
+
finish();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Not written yet.
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
timer = setTimeout(finish, timeout);
|
|
85
|
+
try {
|
|
86
|
+
watcher = watch(directory, check);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Without a watcher the poll below still delivers, just less promptly.
|
|
90
|
+
}
|
|
91
|
+
poll = setInterval(check, POLL_INTERVAL_MS);
|
|
92
|
+
poll.unref();
|
|
93
|
+
check();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
async function run(prompt) {
|
|
97
|
+
try {
|
|
98
|
+
rmSync(acknowledgementPath, { force: true });
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// A stale acknowledgement is handled by the content check, not by its absence.
|
|
102
|
+
}
|
|
103
|
+
write("suspend");
|
|
104
|
+
await waitForAcknowledgement();
|
|
105
|
+
try {
|
|
106
|
+
return await prompt();
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
write("resume");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
borrowTerminal(prompt) {
|
|
114
|
+
const next = queue.then(() => run(prompt), () => run(prompt));
|
|
115
|
+
// Swallowed here only so one rejected borrow does not poison the queue for the
|
|
116
|
+
// next; the caller still receives the rejection through `next`.
|
|
117
|
+
queue = next.then(() => undefined, () => undefined);
|
|
118
|
+
return next;
|
|
119
|
+
},
|
|
120
|
+
stop() {
|
|
121
|
+
write("resume");
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** Child side. Acknowledges a suspend once it has actually stopped drawing and reading. */
|
|
126
|
+
export function observeTerminalHandoff(directory, hooks) {
|
|
127
|
+
const statePath = join(directory, STATE_FILE);
|
|
128
|
+
const acknowledgementPath = join(directory, ACKNOWLEDGEMENT_FILE);
|
|
129
|
+
let current = "resume";
|
|
130
|
+
let applying = Promise.resolve();
|
|
131
|
+
const apply = () => {
|
|
132
|
+
let state;
|
|
133
|
+
try {
|
|
134
|
+
state = parseState(readFileSync(statePath, "utf8"));
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (!state || state === current)
|
|
140
|
+
return;
|
|
141
|
+
current = state;
|
|
142
|
+
applying = applying.then(async () => {
|
|
143
|
+
if (state === "suspend") {
|
|
144
|
+
await hooks.suspend();
|
|
145
|
+
try {
|
|
146
|
+
writeFileSync(acknowledgementPath, "suspended\n");
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// The supervisor's timeout covers an acknowledgement we cannot write.
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
await hooks.resume();
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
apply();
|
|
158
|
+
let watcher;
|
|
159
|
+
try {
|
|
160
|
+
watcher = watch(directory, apply);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Without a watcher the poll below still delivers, just less promptly.
|
|
164
|
+
}
|
|
165
|
+
const poll = setInterval(apply, POLL_INTERVAL_MS);
|
|
166
|
+
// Never hold the process open for this. The child exits on its own schedule and a
|
|
167
|
+
// handoff that stops being observed at shutdown has nothing left to deliver.
|
|
168
|
+
poll.unref();
|
|
169
|
+
return {
|
|
170
|
+
stop: () => {
|
|
171
|
+
watcher?.close();
|
|
172
|
+
clearInterval(poll);
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=terminal-handoff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal-handoff.js","sourceRoot":"","sources":["../../../src/core/sandbox/terminal-handoff.ts"],"names":[],"mappings":"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","sourcesContent":["import { type FSWatcher, readFileSync, rmSync, watch, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/**\n * Lends the terminal from the sandboxed child back to the supervisor for a prompt.\n *\n * The supervisor has to draw the escalation prompt itself. Per ADR 0023 an approval\n * asserted from inside the boundary is indistinguishable from one forged by the code the\n * boundary exists to contain, so the human's answer must be read by the side that owns the\n * decision. The child cannot read it in any case: it runs under `bwrap --new-session` and\n * has no controlling terminal, which `terminal-size.ts` documents for the same reason.\n *\n * The transport is the one `terminal-size.ts` already established -- a file in the\n * workspace, which is bind-mounted into the child, watched on the other side. Signals are\n * unavailable because bwrap sits between the processes as PID 1 and forwards nothing.\n *\n * Two files rather than one, because the supervisor must know the child has actually let\n * go of stdin before it reads an answer. The child inherits stdin and its TUI holds it in\n * raw mode; prompting while both sides read would lose keystrokes to whichever won.\n *\n * The child is asked, never obeyed. A child that never acknowledges delays the prompt by\n * the timeout and no longer, because a contained process that could withhold\n * acknowledgement indefinitely would be able to veto the human's decision -- which is the\n * authority failure ADR 0023 forbids. An unread prompt is a legibility failure, and that\n * is the direction this is allowed to fail in.\n */\n\n/** Env var naming the directory both sides agree on. */\nexport const TERMINAL_HANDOFF_PATH_VARIABLE = \"APEX_TERMINAL_HANDOFF_PATH\";\n\nconst STATE_FILE = \"terminal-handoff\";\nconst ACKNOWLEDGEMENT_FILE = \"terminal-handoff-ack\";\nconst DEFAULT_ACKNOWLEDGEMENT_TIMEOUT_MS = 1_000;\n\n/**\n * How often each side re-reads the state file, independently of the watcher.\n *\n * `fs.watch` is a thin wrapper over whatever the platform provides, and the platforms do\n * not agree. macOS CI delivered a resume more than a second after it was written, and\n * intermittently: the event arrives, late enough that a TUI would sit visibly frozen\n * after the human had already answered. The watcher still does the work in the common\n * case; this only bounds how wrong it can be.\n */\nconst POLL_INTERVAL_MS = 100;\n\ntype HandoffState = \"suspend\" | \"resume\";\n\nfunction parseState(contents: string): HandoffState | undefined {\n\tconst value = contents.trim();\n\treturn value === \"suspend\" || value === \"resume\" ? value : undefined;\n}\n\nexport interface TerminalHandoff {\n\t/**\n\t * Suspend the child, run `prompt` with the terminal to itself, then resume the child.\n\t * Overlapping calls are serialised, so two prompts never share the terminal.\n\t */\n\tborrowTerminal<T>(prompt: () => Promise<T>): Promise<T>;\n\tstop(): void;\n}\n\n/** Supervisor side. */\nexport function createTerminalHandoff(\n\tdirectory: string,\n\toptions?: { acknowledgementTimeoutMs?: number },\n): TerminalHandoff {\n\tconst statePath = join(directory, STATE_FILE);\n\tconst acknowledgementPath = join(directory, ACKNOWLEDGEMENT_FILE);\n\tconst timeout = options?.acknowledgementTimeoutMs ?? DEFAULT_ACKNOWLEDGEMENT_TIMEOUT_MS;\n\t// Every borrow chains onto the last, which is what keeps two escalations that arrive\n\t// together from drawing over each other.\n\tlet queue: Promise<unknown> = Promise.resolve();\n\n\tfunction write(state: HandoffState): void {\n\t\ttry {\n\t\t\twriteFileSync(statePath, `${state}\\n`);\n\t\t} catch {\n\t\t\t// A handoff we cannot request still leaves the prompt readable often enough to\n\t\t\t// be worth attempting; it is never worth failing the escalation over.\n\t\t}\n\t}\n\n\tfunction waitForAcknowledgement(): Promise<void> {\n\t\treturn new Promise((resolve) => {\n\t\t\tlet watcher: FSWatcher | undefined;\n\t\t\tlet timer: NodeJS.Timeout | undefined;\n\t\t\tlet poll: NodeJS.Timeout | undefined;\n\t\t\tconst finish = () => {\n\t\t\t\tif (timer) clearTimeout(timer);\n\t\t\t\tif (poll) clearInterval(poll);\n\t\t\t\twatcher?.close();\n\t\t\t\tresolve();\n\t\t\t};\n\t\t\tconst check = () => {\n\t\t\t\ttry {\n\t\t\t\t\tif (readFileSync(acknowledgementPath, \"utf8\").trim() === \"suspended\") finish();\n\t\t\t\t} catch {\n\t\t\t\t\t// Not written yet.\n\t\t\t\t}\n\t\t\t};\n\t\t\ttimer = setTimeout(finish, timeout);\n\t\t\ttry {\n\t\t\t\twatcher = watch(directory, check);\n\t\t\t} catch {\n\t\t\t\t// Without a watcher the poll below still delivers, just less promptly.\n\t\t\t}\n\t\t\tpoll = setInterval(check, POLL_INTERVAL_MS);\n\t\t\tpoll.unref();\n\t\t\tcheck();\n\t\t});\n\t}\n\n\tasync function run<T>(prompt: () => Promise<T>): Promise<T> {\n\t\ttry {\n\t\t\trmSync(acknowledgementPath, { force: true });\n\t\t} catch {\n\t\t\t// A stale acknowledgement is handled by the content check, not by its absence.\n\t\t}\n\t\twrite(\"suspend\");\n\t\tawait waitForAcknowledgement();\n\t\ttry {\n\t\t\treturn await prompt();\n\t\t} finally {\n\t\t\twrite(\"resume\");\n\t\t}\n\t}\n\n\treturn {\n\t\tborrowTerminal<T>(prompt: () => Promise<T>): Promise<T> {\n\t\t\tconst next = queue.then(\n\t\t\t\t() => run(prompt),\n\t\t\t\t() => run(prompt),\n\t\t\t);\n\t\t\t// Swallowed here only so one rejected borrow does not poison the queue for the\n\t\t\t// next; the caller still receives the rejection through `next`.\n\t\t\tqueue = next.then(\n\t\t\t\t() => undefined,\n\t\t\t\t() => undefined,\n\t\t\t);\n\t\t\treturn next;\n\t\t},\n\t\tstop(): void {\n\t\t\twrite(\"resume\");\n\t\t},\n\t};\n}\n\n/** Child side. Acknowledges a suspend once it has actually stopped drawing and reading. */\nexport function observeTerminalHandoff(\n\tdirectory: string,\n\thooks: { suspend: () => void | Promise<void>; resume: () => void | Promise<void> },\n): { stop: () => void } {\n\tconst statePath = join(directory, STATE_FILE);\n\tconst acknowledgementPath = join(directory, ACKNOWLEDGEMENT_FILE);\n\tlet current: HandoffState = \"resume\";\n\tlet applying: Promise<void> = Promise.resolve();\n\n\tconst apply = () => {\n\t\tlet state: HandoffState | undefined;\n\t\ttry {\n\t\t\tstate = parseState(readFileSync(statePath, \"utf8\"));\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t\tif (!state || state === current) return;\n\t\tcurrent = state;\n\t\tapplying = applying.then(async () => {\n\t\t\tif (state === \"suspend\") {\n\t\t\t\tawait hooks.suspend();\n\t\t\t\ttry {\n\t\t\t\t\twriteFileSync(acknowledgementPath, \"suspended\\n\");\n\t\t\t\t} catch {\n\t\t\t\t\t// The supervisor's timeout covers an acknowledgement we cannot write.\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tawait hooks.resume();\n\t\t\t}\n\t\t});\n\t};\n\n\tapply();\n\n\tlet watcher: FSWatcher | undefined;\n\ttry {\n\t\twatcher = watch(directory, apply);\n\t} catch {\n\t\t// Without a watcher the poll below still delivers, just less promptly.\n\t}\n\tconst poll = setInterval(apply, POLL_INTERVAL_MS);\n\t// Never hold the process open for this. The child exits on its own schedule and a\n\t// handoff that stops being observed at shutdown has nothing left to deliver.\n\tpoll.unref();\n\n\treturn {\n\t\tstop: () => {\n\t\t\twatcher?.close();\n\t\t\tclearInterval(poll);\n\t\t},\n\t};\n}\n"]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carries the host terminal's size across the sandbox boundary.
|
|
3
|
+
*
|
|
4
|
+
* The sandboxed child runs under `bwrap --new-session`, so it has no
|
|
5
|
+
* controlling terminal: the kernel never sends it SIGWINCH, and the window size
|
|
6
|
+
* its stdout reports is frozen at whatever it was when the sandbox was created.
|
|
7
|
+
* Everything downstream reads `process.stdout.columns`, so a resize left the TUI
|
|
8
|
+
* rendering to a width the terminal no longer had.
|
|
9
|
+
*
|
|
10
|
+
* The supervisor owns a real TTY and does get the resize event. It publishes the
|
|
11
|
+
* new size to a file inside the workspace, which is already bind-mounted into
|
|
12
|
+
* the sandbox; the child watches that file and republishes it as a `resize` on
|
|
13
|
+
* its own stdout. Signals are deliberately not used as the transport: bwrap sits
|
|
14
|
+
* between the two processes as PID 1 and does not forward SIGWINCH.
|
|
15
|
+
*/
|
|
16
|
+
/** Env var naming the file both sides agree on. */
|
|
17
|
+
export declare const TERMINAL_SIZE_PATH_VARIABLE = "APEX_TERMINAL_SIZE_PATH";
|
|
18
|
+
export interface TerminalSize {
|
|
19
|
+
columns: number;
|
|
20
|
+
rows: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function parseTerminalSize(contents: string): TerminalSize | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Host side. Publishes the current size, then republishes on every resize.
|
|
25
|
+
* Returns a stop function; the caller must run it on every launch exit path.
|
|
26
|
+
*/
|
|
27
|
+
export declare function publishTerminalSize(path: string, stdout?: NodeJS.WriteStream): () => void;
|
|
28
|
+
/**
|
|
29
|
+
* Child side. Applies the published size to `process.stdout` and keeps applying
|
|
30
|
+
* it as the file changes.
|
|
31
|
+
*
|
|
32
|
+
* `columns` and `rows` are redefined as getters because Node recomputes them
|
|
33
|
+
* from the sandbox's own console on SIGWINCH, which is exactly the stale value
|
|
34
|
+
* this exists to override. The setters swallow those writes rather than throwing.
|
|
35
|
+
*/
|
|
36
|
+
export declare function applyTerminalSize(path: string, stdout?: NodeJS.WriteStream): {
|
|
37
|
+
stop: () => void;
|
|
38
|
+
refresh: () => void;
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=terminal-size.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal-size.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/terminal-size.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AAEH,mDAAmD;AACnD,eAAO,MAAM,2BAA2B,4BAA4B,CAAC;AAErE,MAAM,WAAW,YAAY;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACb;AAMD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAK5E;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,WAA4B,GAAG,MAAM,IAAI,CAmBzG;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAChC,IAAI,EAAE,MAAM,EACZ,MAAM,GAAE,MAAM,CAAC,WAA4B,GACzC;IAAE,IAAI,EAAE,MAAM,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CAyC3C","sourcesContent":["import { type FSWatcher, readFileSync, watch, writeFileSync } from \"node:fs\";\n\n/**\n * Carries the host terminal's size across the sandbox boundary.\n *\n * The sandboxed child runs under `bwrap --new-session`, so it has no\n * controlling terminal: the kernel never sends it SIGWINCH, and the window size\n * its stdout reports is frozen at whatever it was when the sandbox was created.\n * Everything downstream reads `process.stdout.columns`, so a resize left the TUI\n * rendering to a width the terminal no longer had.\n *\n * The supervisor owns a real TTY and does get the resize event. It publishes the\n * new size to a file inside the workspace, which is already bind-mounted into\n * the sandbox; the child watches that file and republishes it as a `resize` on\n * its own stdout. Signals are deliberately not used as the transport: bwrap sits\n * between the two processes as PID 1 and does not forward SIGWINCH.\n */\n\n/** Env var naming the file both sides agree on. */\nexport const TERMINAL_SIZE_PATH_VARIABLE = \"APEX_TERMINAL_SIZE_PATH\";\n\nexport interface TerminalSize {\n\tcolumns: number;\n\trows: number;\n}\n\nfunction formatSize(size: TerminalSize): string {\n\treturn `${size.columns} ${size.rows}\\n`;\n}\n\nexport function parseTerminalSize(contents: string): TerminalSize | undefined {\n\tconst [columns, rows] = contents.trim().split(/\\s+/).map(Number);\n\tif (!Number.isInteger(columns) || !Number.isInteger(rows)) return undefined;\n\tif (columns <= 0 || rows <= 0) return undefined;\n\treturn { columns, rows };\n}\n\n/**\n * Host side. Publishes the current size, then republishes on every resize.\n * Returns a stop function; the caller must run it on every launch exit path.\n */\nexport function publishTerminalSize(path: string, stdout: NodeJS.WriteStream = process.stdout): () => void {\n\tif (!stdout.isTTY) return () => {};\n\n\tconst write = () => {\n\t\tconst columns = stdout.columns;\n\t\tconst rows = stdout.rows;\n\t\tif (!Number.isInteger(columns) || !Number.isInteger(rows)) return;\n\t\ttry {\n\t\t\twriteFileSync(path, formatSize({ columns, rows }));\n\t\t} catch {\n\t\t\t// A size we cannot publish is not worth failing a launch over.\n\t\t}\n\t};\n\n\twrite();\n\tstdout.on(\"resize\", write);\n\treturn () => {\n\t\tstdout.off(\"resize\", write);\n\t};\n}\n\n/**\n * Child side. Applies the published size to `process.stdout` and keeps applying\n * it as the file changes.\n *\n * `columns` and `rows` are redefined as getters because Node recomputes them\n * from the sandbox's own console on SIGWINCH, which is exactly the stale value\n * this exists to override. The setters swallow those writes rather than throwing.\n */\nexport function applyTerminalSize(\n\tpath: string,\n\tstdout: NodeJS.WriteStream = process.stdout,\n): { stop: () => void; refresh: () => void } {\n\tlet current: TerminalSize | undefined;\n\n\tconst refresh = () => {\n\t\tlet size: TerminalSize | undefined;\n\t\ttry {\n\t\t\tsize = parseTerminalSize(readFileSync(path, \"utf8\"));\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t\tif (!size) return;\n\t\tif (current && current.columns === size.columns && current.rows === size.rows) return;\n\t\tcurrent = size;\n\t\tObject.defineProperty(stdout, \"columns\", {\n\t\t\tconfigurable: true,\n\t\t\tget: () => current?.columns,\n\t\t\tset: () => {},\n\t\t});\n\t\tObject.defineProperty(stdout, \"rows\", {\n\t\t\tconfigurable: true,\n\t\t\tget: () => current?.rows,\n\t\t\tset: () => {},\n\t\t});\n\t\tstdout.emit(\"resize\");\n\t};\n\n\trefresh();\n\n\tlet watcher: FSWatcher | undefined;\n\ttry {\n\t\twatcher = watch(path, refresh);\n\t} catch {\n\t\t// Without a watcher the initial size still applies; only live resize is lost.\n\t}\n\n\treturn {\n\t\trefresh,\n\t\tstop: () => {\n\t\t\twatcher?.close();\n\t\t},\n\t};\n}\n"]}
|