apex-code 0.0.1-alpha.8 → 0.0.4
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 +231 -0
- package/README.md +9 -8
- package/dist/cli/args.d.ts +26 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +72 -10
- package/dist/cli/args.js.map +1 -1
- 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/mcp-command.d.ts +9 -0
- package/dist/cli/mcp-command.d.ts.map +1 -0
- package/dist/cli/mcp-command.js +53 -0
- package/dist/cli/mcp-command.js.map +1 -0
- package/dist/cli/project-trust.d.ts.map +1 -1
- package/dist/cli/project-trust.js +3 -1
- package/dist/cli/project-trust.js.map +1 -1
- package/dist/cli/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +2 -1
- package/dist/cli/startup-ui.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +94 -5
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +3 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +45 -18
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +3 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +1 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +183 -15
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +728 -117
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +6 -7
- 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 +7 -0
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/checkpoints/git-checkpoints.d.ts +39 -0
- package/dist/core/checkpoints/git-checkpoints.d.ts.map +1 -0
- package/dist/core/checkpoints/git-checkpoints.js +251 -0
- package/dist/core/checkpoints/git-checkpoints.js.map +1 -0
- package/dist/core/checkpoints/session-checkpoints.d.ts +23 -0
- package/dist/core/checkpoints/session-checkpoints.d.ts.map +1 -0
- package/dist/core/checkpoints/session-checkpoints.js +36 -0
- package/dist/core/checkpoints/session-checkpoints.js.map +1 -0
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +7 -3
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +9 -3
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +59 -36
- package/dist/core/compaction/compaction.js.map +1 -1
- 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/runtime.d.ts +6 -5
- package/dist/core/delegation/runtime.d.ts.map +1 -1
- package/dist/core/delegation/runtime.js +4 -3
- package/dist/core/delegation/runtime.js.map +1 -1
- package/dist/core/durable-state/sqlite.d.ts.map +1 -1
- package/dist/core/durable-state/sqlite.js +26 -2
- package/dist/core/durable-state/sqlite.js.map +1 -1
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js +5 -2
- package/dist/core/exec.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 +123 -50
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +5 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +45 -1
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/source-runtime.d.ts +14 -0
- package/dist/core/extensions/source-runtime.d.ts.map +1 -0
- package/dist/core/extensions/source-runtime.js +56 -0
- package/dist/core/extensions/source-runtime.js.map +1 -0
- package/dist/core/extensions/types.d.ts +60 -11
- 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/formatter-lifecycle.d.ts +69 -0
- package/dist/core/formatter-lifecycle.d.ts.map +1 -0
- package/dist/core/formatter-lifecycle.js +360 -0
- package/dist/core/formatter-lifecycle.js.map +1 -0
- package/dist/core/hooks/command-handler.d.ts +19 -0
- package/dist/core/hooks/command-handler.d.ts.map +1 -0
- package/dist/core/hooks/command-handler.js +112 -0
- package/dist/core/hooks/command-handler.js.map +1 -0
- package/dist/core/hooks/http-handler.d.ts +10 -0
- package/dist/core/hooks/http-handler.d.ts.map +1 -0
- package/dist/core/hooks/http-handler.js +36 -0
- package/dist/core/hooks/http-handler.js.map +1 -0
- package/dist/core/hooks/loader.d.ts +29 -0
- package/dist/core/hooks/loader.d.ts.map +1 -0
- package/dist/core/hooks/loader.js +139 -0
- package/dist/core/hooks/loader.js.map +1 -0
- package/dist/core/hooks/runtime.d.ts +13 -0
- package/dist/core/hooks/runtime.d.ts.map +1 -0
- package/dist/core/hooks/runtime.js +63 -0
- package/dist/core/hooks/runtime.js.map +1 -0
- package/dist/core/hooks/types.d.ts +83 -0
- package/dist/core/hooks/types.d.ts.map +1 -0
- package/dist/core/hooks/types.js +24 -0
- package/dist/core/hooks/types.js.map +1 -0
- package/dist/core/keybindings.d.ts +20 -19
- 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/mcp/config.d.ts +16 -0
- package/dist/core/mcp/config.d.ts.map +1 -0
- package/dist/core/mcp/config.js +166 -0
- package/dist/core/mcp/config.js.map +1 -0
- package/dist/core/mcp/connector.d.ts +35 -0
- package/dist/core/mcp/connector.d.ts.map +1 -0
- package/dist/core/mcp/connector.js +105 -0
- package/dist/core/mcp/connector.js.map +1 -0
- package/dist/core/mcp/contract.d.ts +19 -0
- package/dist/core/mcp/contract.d.ts.map +1 -0
- package/dist/core/mcp/contract.js +89 -0
- package/dist/core/mcp/contract.js.map +1 -0
- package/dist/core/mcp/mcp-tool.d.ts +32 -0
- package/dist/core/mcp/mcp-tool.d.ts.map +1 -0
- package/dist/core/mcp/mcp-tool.js +171 -0
- package/dist/core/mcp/mcp-tool.js.map +1 -0
- package/dist/core/mcp/metadata-cache.d.ts +29 -0
- package/dist/core/mcp/metadata-cache.d.ts.map +1 -0
- package/dist/core/mcp/metadata-cache.js +99 -0
- package/dist/core/mcp/metadata-cache.js.map +1 -0
- package/dist/core/mcp/oauth/authorize.d.ts +21 -0
- package/dist/core/mcp/oauth/authorize.d.ts.map +1 -0
- package/dist/core/mcp/oauth/authorize.js +32 -0
- package/dist/core/mcp/oauth/authorize.js.map +1 -0
- package/dist/core/mcp/oauth/discover.d.ts +27 -0
- package/dist/core/mcp/oauth/discover.d.ts.map +1 -0
- package/dist/core/mcp/oauth/discover.js +116 -0
- package/dist/core/mcp/oauth/discover.js.map +1 -0
- package/dist/core/mcp/oauth/flow.d.ts +31 -0
- package/dist/core/mcp/oauth/flow.d.ts.map +1 -0
- package/dist/core/mcp/oauth/flow.js +175 -0
- package/dist/core/mcp/oauth/flow.js.map +1 -0
- package/dist/core/mcp/oauth/mcp-token.d.ts +51 -0
- package/dist/core/mcp/oauth/mcp-token.d.ts.map +1 -0
- package/dist/core/mcp/oauth/mcp-token.js +129 -0
- package/dist/core/mcp/oauth/mcp-token.js.map +1 -0
- package/dist/core/mcp/oauth/pkce.d.ts +12 -0
- package/dist/core/mcp/oauth/pkce.d.ts.map +1 -0
- package/dist/core/mcp/oauth/pkce.js +11 -0
- package/dist/core/mcp/oauth/pkce.js.map +1 -0
- package/dist/core/mcp/oauth/register.d.ts +16 -0
- package/dist/core/mcp/oauth/register.d.ts.map +1 -0
- package/dist/core/mcp/oauth/register.js +42 -0
- package/dist/core/mcp/oauth/register.js.map +1 -0
- package/dist/core/mcp/oauth/token-client.d.ts +33 -0
- package/dist/core/mcp/oauth/token-client.d.ts.map +1 -0
- package/dist/core/mcp/oauth/token-client.js +65 -0
- package/dist/core/mcp/oauth/token-client.js.map +1 -0
- package/dist/core/mcp/runtime.d.ts +20 -0
- package/dist/core/mcp/runtime.d.ts.map +1 -0
- package/dist/core/mcp/runtime.js +37 -0
- package/dist/core/mcp/runtime.js.map +1 -0
- package/dist/core/mcp/schema.d.ts +22 -0
- package/dist/core/mcp/schema.d.ts.map +1 -0
- package/dist/core/mcp/schema.js +20 -0
- package/dist/core/mcp/schema.js.map +1 -0
- package/dist/core/mcp/server-manager.d.ts +43 -0
- package/dist/core/mcp/server-manager.d.ts.map +1 -0
- package/dist/core/mcp/server-manager.js +142 -0
- package/dist/core/mcp/server-manager.js.map +1 -0
- package/dist/core/mcp/types.d.ts +100 -0
- package/dist/core/mcp/types.d.ts.map +1 -0
- package/dist/core/mcp/types.js +8 -0
- package/dist/core/mcp/types.js.map +1 -0
- package/dist/core/model-config.d.ts +5 -0
- package/dist/core/model-config.d.ts.map +1 -1
- package/dist/core/model-config.js +3 -1
- package/dist/core/model-config.js.map +1 -1
- package/dist/core/model-resolver.d.ts +1 -0
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +14 -8
- package/dist/core/model-resolver.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/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +22 -14
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/permissions/gate.d.ts.map +1 -1
- package/dist/core/permissions/gate.js +45 -4
- package/dist/core/permissions/gate.js.map +1 -1
- package/dist/core/permissions/operations.d.ts +20 -0
- package/dist/core/permissions/operations.d.ts.map +1 -0
- package/dist/core/permissions/operations.js +9 -0
- package/dist/core/permissions/operations.js.map +1 -0
- package/dist/core/permissions/policy-command.d.ts +42 -0
- package/dist/core/permissions/policy-command.d.ts.map +1 -0
- package/dist/core/permissions/policy-command.js +44 -0
- package/dist/core/permissions/policy-command.js.map +1 -0
- package/dist/core/permissions/responder.d.ts +49 -2
- package/dist/core/permissions/responder.d.ts.map +1 -1
- package/dist/core/permissions/responder.js +20 -8
- package/dist/core/permissions/responder.js.map +1 -1
- package/dist/core/permissions/rules.d.ts.map +1 -1
- package/dist/core/permissions/rules.js +11 -1
- package/dist/core/permissions/rules.js.map +1 -1
- package/dist/core/permissions/store.d.ts +18 -0
- package/dist/core/permissions/store.d.ts.map +1 -1
- package/dist/core/permissions/store.js +89 -8
- package/dist/core/permissions/store.js.map +1 -1
- 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/policy-executor.d.ts +47 -0
- package/dist/core/policy-executor.d.ts.map +1 -0
- package/dist/core/policy-executor.js +228 -0
- package/dist/core/policy-executor.js.map +1 -0
- package/dist/core/policy-loader.d.ts +72 -0
- package/dist/core/policy-loader.d.ts.map +1 -0
- package/dist/core/policy-loader.js +195 -0
- package/dist/core/policy-loader.js.map +1 -0
- package/dist/core/project-trust.d.ts +1 -1
- package/dist/core/project-trust.d.ts.map +1 -1
- package/dist/core/project-trust.js.map +1 -1
- package/dist/core/remote-catalog-provider.d.ts.map +1 -1
- package/dist/core/remote-catalog-provider.js +2 -1
- 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 +41 -0
- package/dist/core/sandbox/bwrap-arguments.d.ts.map +1 -0
- package/dist/core/sandbox/bwrap-arguments.js +136 -0
- package/dist/core/sandbox/bwrap-arguments.js.map +1 -0
- package/dist/core/sandbox/child-entry.d.ts.map +1 -1
- package/dist/core/sandbox/child-entry.js +3 -1
- package/dist/core/sandbox/child-entry.js.map +1 -1
- package/dist/core/sandbox/cli-launch.d.ts +84 -4
- package/dist/core/sandbox/cli-launch.d.ts.map +1 -1
- package/dist/core/sandbox/cli-launch.js +115 -16
- package/dist/core/sandbox/cli-launch.js.map +1 -1
- package/dist/core/sandbox/cli-supervisor.d.ts +2 -0
- package/dist/core/sandbox/cli-supervisor.d.ts.map +1 -1
- package/dist/core/sandbox/cli-supervisor.js +50 -2
- package/dist/core/sandbox/cli-supervisor.js.map +1 -1
- 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 +14 -0
- package/dist/core/sandbox/linux-backend.d.ts.map +1 -1
- package/dist/core/sandbox/linux-backend.js +158 -80
- package/dist/core/sandbox/linux-backend.js.map +1 -1
- package/dist/core/sandbox/macos-backend.d.ts +5 -0
- package/dist/core/sandbox/macos-backend.d.ts.map +1 -1
- package/dist/core/sandbox/macos-backend.js +164 -5
- package/dist/core/sandbox/macos-backend.js.map +1 -1
- package/dist/core/sandbox/network-proxy.d.ts +19 -0
- package/dist/core/sandbox/network-proxy.d.ts.map +1 -1
- package/dist/core/sandbox/network-proxy.js +93 -19
- package/dist/core/sandbox/network-proxy.js.map +1 -1
- package/dist/core/sandbox/policy.d.ts +8 -0
- package/dist/core/sandbox/policy.d.ts.map +1 -1
- package/dist/core/sandbox/policy.js +29 -12
- package/dist/core/sandbox/policy.js.map +1 -1
- 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-proxy.d.ts.map +1 -1
- package/dist/core/sandbox/rpc/credential-proxy.js +2 -96
- package/dist/core/sandbox/rpc/credential-proxy.js.map +1 -1
- 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 +47 -0
- package/dist/core/sandbox/rpc/git-credential-helper.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-helper.js +222 -0
- package/dist/core/sandbox/rpc/git-credential-helper.js.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.d.ts +66 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.js +209 -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 +12 -1
- package/dist/core/sandbox/supervisor.d.ts.map +1 -1
- package/dist/core/sandbox/supervisor.js +15 -1
- package/dist/core/sandbox/supervisor.js.map +1 -1
- package/dist/core/sandbox/terminal-handoff.d.ts +68 -0
- package/dist/core/sandbox/terminal-handoff.d.ts.map +1 -0
- package/dist/core/sandbox/terminal-handoff.js +221 -0
- package/dist/core/sandbox/terminal-handoff.js.map +1 -0
- package/dist/core/sandbox/terminal-size.d.ts +13 -0
- package/dist/core/sandbox/terminal-size.d.ts.map +1 -1
- package/dist/core/sandbox/terminal-size.js +23 -2
- package/dist/core/sandbox/terminal-size.js.map +1 -1
- package/dist/core/sdk.d.ts +24 -2
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +63 -4
- 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-manager.d.ts +13 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +34 -6
- package/dist/core/session-manager.js.map +1 -1
- 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 +172 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +160 -11
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +51 -33
- package/dist/core/skills.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +4 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +8 -2
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/background-shell.d.ts +39 -0
- package/dist/core/tools/background-shell.d.ts.map +1 -0
- package/dist/core/tools/background-shell.js +81 -0
- package/dist/core/tools/background-shell.js.map +1 -0
- package/dist/core/tools/bash-command-segments.d.ts.map +1 -1
- package/dist/core/tools/bash-command-segments.js +2 -0
- package/dist/core/tools/bash-command-segments.js.map +1 -1
- package/dist/core/tools/bash.d.ts +44 -16
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +264 -32
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/contract-snapshot.d.ts +58 -0
- package/dist/core/tools/contract-snapshot.d.ts.map +1 -0
- package/dist/core/tools/contract-snapshot.js +51 -0
- package/dist/core/tools/contract-snapshot.js.map +1 -0
- package/dist/core/tools/contract.d.ts +53 -5
- package/dist/core/tools/contract.d.ts.map +1 -1
- package/dist/core/tools/contract.js +22 -2
- package/dist/core/tools/contract.js.map +1 -1
- package/dist/core/tools/delegate.d.ts +6 -4
- package/dist/core/tools/delegate.d.ts.map +1 -1
- package/dist/core/tools/delegate.js +21 -11
- package/dist/core/tools/delegate.js.map +1 -1
- package/dist/core/tools/edit-diff.d.ts +48 -5
- package/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/core/tools/edit-diff.js +217 -8
- package/dist/core/tools/edit-diff.js.map +1 -1
- package/dist/core/tools/edit.d.ts +8 -0
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +72 -10
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +5 -2
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +5 -2
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +14 -2
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +12 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +5 -2
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/lsp.d.ts +19 -1
- package/dist/core/tools/lsp.d.ts.map +1 -1
- package/dist/core/tools/lsp.js +163 -10
- package/dist/core/tools/lsp.js.map +1 -1
- package/dist/core/tools/path-permission.d.ts +3 -0
- package/dist/core/tools/path-permission.d.ts.map +1 -1
- package/dist/core/tools/path-permission.js +16 -4
- package/dist/core/tools/path-permission.js.map +1 -1
- package/dist/core/tools/path-utils.d.ts +27 -2
- package/dist/core/tools/path-utils.d.ts.map +1 -1
- package/dist/core/tools/path-utils.js +221 -4
- package/dist/core/tools/path-utils.js.map +1 -1
- 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.map +1 -1
- package/dist/core/tools/read.js +20 -11
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/test.d.ts +54 -9
- package/dist/core/tools/test.d.ts.map +1 -1
- package/dist/core/tools/test.js +239 -19
- package/dist/core/tools/test.js.map +1 -1
- package/dist/core/tools/write.d.ts +7 -0
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +64 -9
- 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/verification-lifecycle.d.ts +77 -0
- package/dist/core/verification-lifecycle.d.ts.map +1 -0
- package/dist/core/verification-lifecycle.js +146 -0
- package/dist/core/verification-lifecycle.js.map +1 -0
- package/dist/core/workspace/artifacts.d.ts +45 -0
- package/dist/core/workspace/artifacts.d.ts.map +1 -0
- package/dist/core/workspace/artifacts.js +165 -0
- package/dist/core/workspace/artifacts.js.map +1 -0
- package/dist/core/workspace/comparison.d.ts +27 -0
- package/dist/core/workspace/comparison.d.ts.map +1 -0
- package/dist/core/workspace/comparison.js +94 -0
- package/dist/core/workspace/comparison.js.map +1 -0
- package/dist/core/workspace/git-observer.d.ts +48 -0
- package/dist/core/workspace/git-observer.d.ts.map +1 -0
- package/dist/core/workspace/git-observer.js +346 -0
- package/dist/core/workspace/git-observer.js.map +1 -0
- package/dist/core/workspace/projection.d.ts +3 -0
- package/dist/core/workspace/projection.d.ts.map +1 -0
- package/dist/core/workspace/projection.js +56 -0
- package/dist/core/workspace/projection.js.map +1 -0
- package/dist/core/workspace/state.d.ts +133 -0
- package/dist/core/workspace/state.d.ts.map +1 -0
- package/dist/core/workspace/state.js +96 -0
- package/dist/core/workspace/state.js.map +1 -0
- package/dist/extensions/llama/client.d.ts +6 -0
- package/dist/extensions/llama/client.d.ts.map +1 -1
- package/dist/extensions/llama/client.js +7 -0
- package/dist/extensions/llama/client.js.map +1 -1
- 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 +3 -1
- package/dist/extensions/llama/provider.d.ts.map +1 -1
- package/dist/extensions/llama/provider.js +29 -4
- package/dist/extensions/llama/provider.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +6 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +58 -23
- package/dist/main.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +4 -3
- package/dist/migrations.js.map +1 -1
- package/dist/modes/acp/server.d.ts +55 -0
- package/dist/modes/acp/server.d.ts.map +1 -0
- package/dist/modes/acp/server.js +244 -0
- package/dist/modes/acp/server.js.map +1 -0
- package/dist/modes/acp/translate.d.ts +37 -0
- package/dist/modes/acp/translate.d.ts.map +1 -0
- package/dist/modes/acp/translate.js +50 -0
- package/dist/modes/acp/translate.js.map +1 -0
- package/dist/modes/index.d.ts +1 -0
- package/dist/modes/index.d.ts.map +1 -1
- package/dist/modes/index.js +1 -0
- package/dist/modes/index.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +29 -0
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +182 -23
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts +7 -0
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +20 -0
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +9 -4
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +20 -9
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts +7 -1
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +7 -3
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +19 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +70 -10
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +10 -3
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +56 -14
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/permission-preview.d.ts +15 -0
- package/dist/modes/interactive/components/permission-preview.d.ts.map +1 -0
- package/dist/modes/interactive/components/permission-preview.js +33 -0
- package/dist/modes/interactive/components/permission-preview.js.map +1 -0
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +2 -2
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +9 -2
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +128 -56
- 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/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 +23 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +64 -10
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/external-editor.d.ts.map +1 -1
- package/dist/modes/interactive/external-editor.js +2 -1
- package/dist/modes/interactive/external-editor.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +45 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +395 -43
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +12 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +20 -9
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/json-event.d.ts +16 -13
- package/dist/modes/json-event.d.ts.map +1 -1
- package/dist/modes/json-event.js +20 -6
- package/dist/modes/json-event.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +27 -12
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +7 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +7 -0
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +3 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +12 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.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 +196 -10
- package/dist/package-manager-cli.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 +23 -9
- 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/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +6 -4
- package/dist/utils/version-check.js.map +1 -1
- package/docs/compaction.md +19 -0
- package/docs/custom-provider.md +12 -9
- package/docs/environment-variables.md +1 -1
- package/docs/extensions.md +35 -7
- package/docs/json.md +7 -2
- package/docs/keybindings.md +9 -9
- package/docs/llama-cpp.md +2 -0
- package/docs/models.md +8 -2
- package/docs/packages.md +2 -2
- package/docs/quickstart.md +1 -1
- package/docs/rpc.md +32 -3
- package/docs/sdk.md +32 -2
- package/docs/settings.md +23 -7
- package/docs/skills.md +4 -3
- package/docs/terminal-setup.md +23 -7
- package/docs/usage.md +10 -5
- package/docs/windows.md +24 -2
- package/examples/extensions/border-status-editor.ts +1 -1
- package/examples/extensions/built-in-tool-renderer.ts +8 -4
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/git-checkpoint.ts +24 -36
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/minimal-mode.ts +2 -2
- package/examples/extensions/notify.ts +3 -1
- package/examples/extensions/permission-gate.ts +1 -1
- package/examples/extensions/plan-mode/index.ts +1 -1
- package/examples/extensions/rpc-demo.ts +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/subagent/README.md +1 -1
- package/examples/extensions/subagent/index.ts +6 -1
- 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 +439 -405
- package/package.json +17 -17
- package/dist/server/create-harness.d.ts +0 -26
- package/dist/server/create-harness.d.ts.map +0 -1
- package/dist/server/create-harness.js +0 -107
- package/dist/server/create-harness.js.map +0 -1
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
import { rmSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
1
3
|
import { parseArgs } from "../../cli/args.js";
|
|
2
4
|
import { reportConcurrentSessionRefusal } from "../../cli/concurrent-session.js";
|
|
5
|
+
import { getAgentDir } from "../../config.js";
|
|
3
6
|
import { acquireSessionLease, readLiveSessionLeases } from "../session-lease.js";
|
|
4
|
-
import { buildSandboxedCliLaunch, getSandboxSessionDirectory } from "./cli-launch.js";
|
|
7
|
+
import { buildSandboxedCliLaunch, createSupervisorStateDirectory, getSandboxSessionDirectory, resolveSupervisorPolicyPath, writeSupervisorPolicySnapshot, } from "./cli-launch.js";
|
|
8
|
+
import { createProjectedGitConfig, resolveHostGitIdentity } from "./git-identity.js";
|
|
5
9
|
import { createLinuxSandboxBackend } from "./linux-backend.js";
|
|
6
10
|
import { createMacosSandboxBackend } from "./macos-backend.js";
|
|
7
11
|
import { createSandboxPolicy } from "./policy.js";
|
|
8
12
|
import { createCredentialProxy, resolveCredentialChannelPaths } from "./rpc/credential-proxy.js";
|
|
13
|
+
import { gitCredentialHelperCommand } from "./rpc/git-credential-helper.js";
|
|
9
14
|
import { createSandboxSupervisor } from "./supervisor.js";
|
|
10
15
|
import { SandboxViolationStore } from "./violations.js";
|
|
11
16
|
/** Routes to the platform adapter for the running OS; each adapter self-reports
|
|
@@ -26,18 +31,36 @@ const defaultDependencies = {
|
|
|
26
31
|
*/
|
|
27
32
|
export async function launchSandboxedCli(options) {
|
|
28
33
|
const dependencies = { ...defaultDependencies, ...options.dependencies };
|
|
29
|
-
const policyResult = createSandboxPolicy({
|
|
34
|
+
const policyResult = createSandboxPolicy({
|
|
35
|
+
workspace: options.workspace,
|
|
36
|
+
allowedHosts: options.allowedHosts,
|
|
37
|
+
additionalWritableRoots: options.additionalWritableRoots,
|
|
38
|
+
});
|
|
30
39
|
if (policyResult.kind === "invalid") {
|
|
31
40
|
dependencies.stderr.write(`Error: OS sandbox is not enforcing this agent session: ${policyResult.reason}\n`);
|
|
32
41
|
return 1;
|
|
33
42
|
}
|
|
34
43
|
const violationStore = new SandboxViolationStore();
|
|
35
44
|
const backend = dependencies.createBackend({ violationStore });
|
|
45
|
+
const agentDir = options.agentDir ?? getAgentDir();
|
|
46
|
+
const supervisorState = createSupervisorStateDirectory();
|
|
36
47
|
const supervisor = createSandboxSupervisor({ backend, policy: policyResult.policy });
|
|
37
48
|
let credentialProxy;
|
|
38
49
|
let lease;
|
|
50
|
+
let projectedGitConfig;
|
|
39
51
|
try {
|
|
40
52
|
const parsed = parseArgs(options.args);
|
|
53
|
+
const resolvedSessionPath = parsed.session &&
|
|
54
|
+
(parsed.session.includes("/") || parsed.session.includes("\\") || parsed.session.endsWith(".jsonl"))
|
|
55
|
+
? resolve(options.workspace, parsed.session)
|
|
56
|
+
: undefined;
|
|
57
|
+
const sessionRelativePath = resolvedSessionPath ? relative(options.workspace, resolvedSessionPath) : undefined;
|
|
58
|
+
const externalSessionPath = resolvedSessionPath &&
|
|
59
|
+
sessionRelativePath !== undefined &&
|
|
60
|
+
sessionRelativePath !== "" &&
|
|
61
|
+
(sessionRelativePath === ".." || sessionRelativePath.startsWith(`..${sep}`) || isAbsolute(sessionRelativePath))
|
|
62
|
+
? resolvedSessionPath
|
|
63
|
+
: undefined;
|
|
41
64
|
const wantsPersistentSession = !parsed.noSession && !parsed.help && parsed.listModels === undefined;
|
|
42
65
|
const sessionDirectory = getSandboxSessionDirectory(policyResult.policy.workspace);
|
|
43
66
|
if (wantsPersistentSession && !parsed.allowConcurrent) {
|
|
@@ -64,6 +87,24 @@ export async function launchSandboxedCli(options) {
|
|
|
64
87
|
});
|
|
65
88
|
credentialChannel = paths;
|
|
66
89
|
}
|
|
90
|
+
// Resolved here rather than in the caller because this function owns the teardown
|
|
91
|
+
// that removes the directory again. The supervisor is unsandboxed, so the host home
|
|
92
|
+
// is still visible at this point; inside the child it is not.
|
|
93
|
+
const identity = resolveHostGitIdentity({ environment: options.environment });
|
|
94
|
+
const snapshotPath = writeSupervisorPolicySnapshot({
|
|
95
|
+
directory: supervisorState.path,
|
|
96
|
+
agentDir,
|
|
97
|
+
policyPath: resolveSupervisorPolicyPath(options.environment),
|
|
98
|
+
environment: options.environment,
|
|
99
|
+
});
|
|
100
|
+
// The helper command is derived from the state directory rather than handed back by
|
|
101
|
+
// the backend, because the config has to name it before any backend exists. Both
|
|
102
|
+
// sides derive the same path, so the config never names a file nothing created.
|
|
103
|
+
projectedGitConfig = identity
|
|
104
|
+
? createProjectedGitConfig(identity, {
|
|
105
|
+
credentialHelper: gitCredentialHelperCommand(supervisorState.path),
|
|
106
|
+
})
|
|
107
|
+
: undefined;
|
|
67
108
|
const launch = buildSandboxedCliLaunch({
|
|
68
109
|
workspace: policyResult.policy.workspace,
|
|
69
110
|
command: options.command,
|
|
@@ -71,10 +112,14 @@ export async function launchSandboxedCli(options) {
|
|
|
71
112
|
environment: options.environment,
|
|
72
113
|
allowedHosts: options.allowedHosts,
|
|
73
114
|
readOnlyPaths: options.readOnlyPaths,
|
|
115
|
+
sessionPath: externalSessionPath,
|
|
74
116
|
authPath: options.authPath,
|
|
117
|
+
gitConfigPath: projectedGitConfig?.path,
|
|
75
118
|
toolBinaries: options.toolBinaries,
|
|
76
119
|
skillPaths: options.skillPaths,
|
|
77
120
|
credentialChannel,
|
|
121
|
+
supervisorStateDirectory: supervisorState.path,
|
|
122
|
+
policySnapshotPath: snapshotPath,
|
|
78
123
|
});
|
|
79
124
|
return await supervisor.launch(launch);
|
|
80
125
|
}
|
|
@@ -85,7 +130,10 @@ export async function launchSandboxedCli(options) {
|
|
|
85
130
|
}
|
|
86
131
|
finally {
|
|
87
132
|
lease?.release();
|
|
133
|
+
if (projectedGitConfig)
|
|
134
|
+
rmSync(projectedGitConfig.directory, { force: true, recursive: true });
|
|
88
135
|
const cleanupResults = await Promise.allSettled([supervisor.close(), credentialProxy?.close()]);
|
|
136
|
+
supervisorState.dispose();
|
|
89
137
|
for (const violation of violationStore.list()) {
|
|
90
138
|
dependencies.stderr.write(`Sandbox violation (${violation.kind}): ${violation.command} — ${violation.detail}\n`);
|
|
91
139
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-supervisor.js","sourceRoot":"","sources":["../../../src/core/sandbox/cli-supervisor.ts"],"names":[],"mappings":"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","sourcesContent":["import { parseArgs } from \"../../cli/args.ts\";\nimport { reportConcurrentSessionRefusal } from \"../../cli/concurrent-session.ts\";\nimport type { HostToolBinary } from \"../../utils/tools-manager.ts\";\nimport { acquireSessionLease, readLiveSessionLeases } from \"../session-lease.ts\";\nimport { buildSandboxedCliLaunch, getSandboxSessionDirectory, type HostSkillPaths } from \"./cli-launch.ts\";\nimport { createLinuxSandboxBackend } from \"./linux-backend.ts\";\nimport { createMacosSandboxBackend } from \"./macos-backend.ts\";\nimport { createSandboxPolicy } from \"./policy.ts\";\nimport { createCredentialProxy, resolveCredentialChannelPaths } from \"./rpc/credential-proxy.ts\";\nimport { createSandboxSupervisor, type SandboxBackend, type SandboxLaunch } from \"./supervisor.ts\";\nimport { SandboxViolationStore } from \"./violations.ts\";\n\nexport interface CliSandboxDependencies {\n\tcreateBackend: (options: { violationStore: SandboxViolationStore }) => SandboxBackend;\n\tstderr: { write(message: string): boolean };\n}\n\n/** Routes to the platform adapter for the running OS; each adapter self-reports\n * `unavailable` on any other platform, so this only needs to pick the one whose\n * enforcement is actually reachable here. */\nfunction createDefaultSandboxBackend(options: { violationStore: SandboxViolationStore }): SandboxBackend {\n\tif (process.platform === \"darwin\") return createMacosSandboxBackend(options);\n\treturn createLinuxSandboxBackend(options);\n}\n\nconst defaultDependencies: CliSandboxDependencies = {\n\tcreateBackend: createDefaultSandboxBackend,\n\tstderr: process.stderr,\n};\n\n/**\n * Start a normal CLI runtime beneath the whole-process boundary. The caller returns\n * the result directly so the outer process never continues into `main()` afterward.\n */\nexport async function launchSandboxedCli(options: {\n\tcommand: string;\n\targs: readonly string[];\n\tenvironment: NodeJS.ProcessEnv;\n\tworkspace: string;\n\tallowedHosts?: readonly string[];\n\treadOnlyPaths?: readonly string[];\n\tauthPath?: string;\n\ttoolBinaries?: readonly HostToolBinary[];\n\tskillPaths?: HostSkillPaths;\n\tdependencies?: Partial<CliSandboxDependencies>;\n}): Promise<number> {\n\tconst dependencies = { ...defaultDependencies, ...options.dependencies };\n\tconst policyResult = createSandboxPolicy({ workspace: options.workspace, allowedHosts: options.allowedHosts });\n\tif (policyResult.kind === \"invalid\") {\n\t\tdependencies.stderr.write(`Error: OS sandbox is not enforcing this agent session: ${policyResult.reason}\\n`);\n\t\treturn 1;\n\t}\n\tconst violationStore = new SandboxViolationStore();\n\tconst backend = dependencies.createBackend({ violationStore });\n\n\tconst supervisor = createSandboxSupervisor({ backend, policy: policyResult.policy });\n\tlet credentialProxy: Awaited<ReturnType<typeof createCredentialProxy>> | undefined;\n\tlet lease: ReturnType<typeof acquireSessionLease> | undefined;\n\ttry {\n\t\tconst parsed = parseArgs(options.args);\n\t\tconst wantsPersistentSession = !parsed.noSession && !parsed.help && parsed.listModels === undefined;\n\t\tconst sessionDirectory = getSandboxSessionDirectory(policyResult.policy.workspace);\n\t\tif (wantsPersistentSession && !parsed.allowConcurrent) {\n\t\t\tconst liveSessions = readLiveSessionLeases(sessionDirectory, policyResult.policy.workspace);\n\t\t\tif (liveSessions.length > 0) {\n\t\t\t\treportConcurrentSessionRefusal(liveSessions, policyResult.policy.workspace);\n\t\t\t\treturn 1;\n\t\t\t}\n\t\t}\n\t\tlease = wantsPersistentSession\n\t\t\t? acquireSessionLease(sessionDirectory, policyResult.policy.workspace, `supervisor-${process.pid}`)\n\t\t\t: undefined;\n\n\t\t// Open the host-owned credential channel only after startup checks that can\n\t\t// return early. AuthStorage creates a missing canonical file as 0600, so a\n\t\t// first-run credential writes get the same channel and read-only projection as later runs.\n\t\tlet credentialChannel: SandboxLaunch[\"credentialChannel\"];\n\t\tif (options.authPath && backend.status.kind === \"enforced\") {\n\t\t\tconst paths = resolveCredentialChannelPaths();\n\t\t\tcredentialProxy = await createCredentialProxy({\n\t\t\t\tauthPath: options.authPath,\n\t\t\t\tviolationStore,\n\t\t\t\tsocketPath: paths.hostSocketPath,\n\t\t\t\tcleanupDirectory: paths.hostSocketDirectory,\n\t\t\t});\n\t\t\tcredentialChannel = paths;\n\t\t}\n\n\t\tconst launch = buildSandboxedCliLaunch({\n\t\t\tworkspace: policyResult.policy.workspace,\n\t\t\tcommand: options.command,\n\t\t\targs: options.args,\n\t\t\tenvironment: options.environment,\n\t\t\tallowedHosts: options.allowedHosts,\n\t\t\treadOnlyPaths: options.readOnlyPaths,\n\t\t\tauthPath: options.authPath,\n\t\t\ttoolBinaries: options.toolBinaries,\n\t\t\tskillPaths: options.skillPaths,\n\t\t\tcredentialChannel,\n\t\t});\n\t\treturn await supervisor.launch(launch);\n\t} catch (error: unknown) {\n\t\tconst message = error instanceof Error ? error.message : \"Failed to start OS sandbox.\";\n\t\tdependencies.stderr.write(`Error: ${message}\\n`);\n\t\treturn 1;\n\t} finally {\n\t\tlease?.release();\n\t\tconst cleanupResults = await Promise.allSettled([supervisor.close(), credentialProxy?.close()]);\n\t\tfor (const violation of violationStore.list()) {\n\t\t\tdependencies.stderr.write(\n\t\t\t\t`Sandbox violation (${violation.kind}): ${violation.command} — ${violation.detail}\\n`,\n\t\t\t);\n\t\t}\n\t\tfor (const result of cleanupResults) {\n\t\t\tif (result.status === \"rejected\") {\n\t\t\t\tdependencies.stderr.write(\"Warning: sandbox cleanup did not complete cleanly.\\n\");\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"cli-supervisor.js","sourceRoot":"","sources":["../../../src/core/sandbox/cli-supervisor.ts"],"names":[],"mappings":"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","sourcesContent":["import { rmSync } from \"node:fs\";\nimport { isAbsolute, relative, resolve, sep } from \"node:path\";\nimport { parseArgs } from \"../../cli/args.ts\";\nimport { reportConcurrentSessionRefusal } from \"../../cli/concurrent-session.ts\";\nimport { getAgentDir } from \"../../config.ts\";\nimport type { HostToolBinary } from \"../../utils/tools-manager.ts\";\nimport { acquireSessionLease, readLiveSessionLeases } from \"../session-lease.ts\";\nimport {\n\tbuildSandboxedCliLaunch,\n\tcreateSupervisorStateDirectory,\n\tgetSandboxSessionDirectory,\n\ttype HostSkillPaths,\n\tresolveSupervisorPolicyPath,\n\twriteSupervisorPolicySnapshot,\n} from \"./cli-launch.ts\";\nimport { createProjectedGitConfig, resolveHostGitIdentity } from \"./git-identity.ts\";\nimport { createLinuxSandboxBackend } from \"./linux-backend.ts\";\nimport { createMacosSandboxBackend } from \"./macos-backend.ts\";\nimport { createSandboxPolicy } from \"./policy.ts\";\nimport { createCredentialProxy, resolveCredentialChannelPaths } from \"./rpc/credential-proxy.ts\";\nimport { gitCredentialHelperCommand } from \"./rpc/git-credential-helper.ts\";\nimport { createSandboxSupervisor, type SandboxBackend, type SandboxLaunch } from \"./supervisor.ts\";\nimport { SandboxViolationStore } from \"./violations.ts\";\n\nexport interface CliSandboxDependencies {\n\tcreateBackend: (options: { violationStore: SandboxViolationStore }) => SandboxBackend;\n\tstderr: { write(message: string): boolean };\n}\n\n/** Routes to the platform adapter for the running OS; each adapter self-reports\n * `unavailable` on any other platform, so this only needs to pick the one whose\n * enforcement is actually reachable here. */\nfunction createDefaultSandboxBackend(options: { violationStore: SandboxViolationStore }): SandboxBackend {\n\tif (process.platform === \"darwin\") return createMacosSandboxBackend(options);\n\treturn createLinuxSandboxBackend(options);\n}\n\nconst defaultDependencies: CliSandboxDependencies = {\n\tcreateBackend: createDefaultSandboxBackend,\n\tstderr: process.stderr,\n};\n\n/**\n * Start a normal CLI runtime beneath the whole-process boundary. The caller returns\n * the result directly so the outer process never continues into `main()` afterward.\n */\nexport async function launchSandboxedCli(options: {\n\tcommand: string;\n\targs: readonly string[];\n\tenvironment: NodeJS.ProcessEnv;\n\tworkspace: string;\n\tagentDir?: string;\n\tallowedHosts?: readonly string[];\n\tadditionalWritableRoots?: readonly string[];\n\treadOnlyPaths?: readonly string[];\n\tauthPath?: string;\n\ttoolBinaries?: readonly HostToolBinary[];\n\tskillPaths?: HostSkillPaths;\n\tdependencies?: Partial<CliSandboxDependencies>;\n}): Promise<number> {\n\tconst dependencies = { ...defaultDependencies, ...options.dependencies };\n\tconst policyResult = createSandboxPolicy({\n\t\tworkspace: options.workspace,\n\t\tallowedHosts: options.allowedHosts,\n\t\tadditionalWritableRoots: options.additionalWritableRoots,\n\t});\n\tif (policyResult.kind === \"invalid\") {\n\t\tdependencies.stderr.write(`Error: OS sandbox is not enforcing this agent session: ${policyResult.reason}\\n`);\n\t\treturn 1;\n\t}\n\tconst violationStore = new SandboxViolationStore();\n\tconst backend = dependencies.createBackend({ violationStore });\n\tconst agentDir = options.agentDir ?? getAgentDir();\n\tconst supervisorState = createSupervisorStateDirectory();\n\n\tconst supervisor = createSandboxSupervisor({ backend, policy: policyResult.policy });\n\tlet credentialProxy: Awaited<ReturnType<typeof createCredentialProxy>> | undefined;\n\tlet lease: ReturnType<typeof acquireSessionLease> | undefined;\n\tlet projectedGitConfig: ReturnType<typeof createProjectedGitConfig> | undefined;\n\ttry {\n\t\tconst parsed = parseArgs(options.args);\n\t\tconst resolvedSessionPath =\n\t\t\tparsed.session &&\n\t\t\t(parsed.session.includes(\"/\") || parsed.session.includes(\"\\\\\") || parsed.session.endsWith(\".jsonl\"))\n\t\t\t\t? resolve(options.workspace, parsed.session)\n\t\t\t\t: undefined;\n\t\tconst sessionRelativePath = resolvedSessionPath ? relative(options.workspace, resolvedSessionPath) : undefined;\n\t\tconst externalSessionPath =\n\t\t\tresolvedSessionPath &&\n\t\t\tsessionRelativePath !== undefined &&\n\t\t\tsessionRelativePath !== \"\" &&\n\t\t\t(sessionRelativePath === \"..\" || sessionRelativePath.startsWith(`..${sep}`) || isAbsolute(sessionRelativePath))\n\t\t\t\t? resolvedSessionPath\n\t\t\t\t: undefined;\n\t\tconst wantsPersistentSession = !parsed.noSession && !parsed.help && parsed.listModels === undefined;\n\t\tconst sessionDirectory = getSandboxSessionDirectory(policyResult.policy.workspace);\n\t\tif (wantsPersistentSession && !parsed.allowConcurrent) {\n\t\t\tconst liveSessions = readLiveSessionLeases(sessionDirectory, policyResult.policy.workspace);\n\t\t\tif (liveSessions.length > 0) {\n\t\t\t\treportConcurrentSessionRefusal(liveSessions, policyResult.policy.workspace);\n\t\t\t\treturn 1;\n\t\t\t}\n\t\t}\n\t\tlease = wantsPersistentSession\n\t\t\t? acquireSessionLease(sessionDirectory, policyResult.policy.workspace, `supervisor-${process.pid}`)\n\t\t\t: undefined;\n\n\t\t// Open the host-owned credential channel only after startup checks that can\n\t\t// return early. AuthStorage creates a missing canonical file as 0600, so a\n\t\t// first-run credential writes get the same channel and read-only projection as later runs.\n\t\tlet credentialChannel: SandboxLaunch[\"credentialChannel\"];\n\t\tif (options.authPath && backend.status.kind === \"enforced\") {\n\t\t\tconst paths = resolveCredentialChannelPaths();\n\t\t\tcredentialProxy = await createCredentialProxy({\n\t\t\t\tauthPath: options.authPath,\n\t\t\t\tviolationStore,\n\t\t\t\tsocketPath: paths.hostSocketPath,\n\t\t\t\tcleanupDirectory: paths.hostSocketDirectory,\n\t\t\t});\n\t\t\tcredentialChannel = paths;\n\t\t}\n\n\t\t// Resolved here rather than in the caller because this function owns the teardown\n\t\t// that removes the directory again. The supervisor is unsandboxed, so the host home\n\t\t// is still visible at this point; inside the child it is not.\n\t\tconst identity = resolveHostGitIdentity({ environment: options.environment });\n\t\tconst snapshotPath = writeSupervisorPolicySnapshot({\n\t\t\tdirectory: supervisorState.path,\n\t\t\tagentDir,\n\t\t\tpolicyPath: resolveSupervisorPolicyPath(options.environment),\n\t\t\tenvironment: options.environment,\n\t\t});\n\t\t// The helper command is derived from the state directory rather than handed back by\n\t\t// the backend, because the config has to name it before any backend exists. Both\n\t\t// sides derive the same path, so the config never names a file nothing created.\n\t\tprojectedGitConfig = identity\n\t\t\t? createProjectedGitConfig(identity, {\n\t\t\t\t\tcredentialHelper: gitCredentialHelperCommand(supervisorState.path),\n\t\t\t\t})\n\t\t\t: undefined;\n\n\t\tconst launch = buildSandboxedCliLaunch({\n\t\t\tworkspace: policyResult.policy.workspace,\n\t\t\tcommand: options.command,\n\t\t\targs: options.args,\n\t\t\tenvironment: options.environment,\n\t\t\tallowedHosts: options.allowedHosts,\n\t\t\treadOnlyPaths: options.readOnlyPaths,\n\t\t\tsessionPath: externalSessionPath,\n\t\t\tauthPath: options.authPath,\n\t\t\tgitConfigPath: projectedGitConfig?.path,\n\t\t\ttoolBinaries: options.toolBinaries,\n\t\t\tskillPaths: options.skillPaths,\n\t\t\tcredentialChannel,\n\t\t\tsupervisorStateDirectory: supervisorState.path,\n\t\t\tpolicySnapshotPath: snapshotPath,\n\t\t});\n\t\treturn await supervisor.launch(launch);\n\t} catch (error: unknown) {\n\t\tconst message = error instanceof Error ? error.message : \"Failed to start OS sandbox.\";\n\t\tdependencies.stderr.write(`Error: ${message}\\n`);\n\t\treturn 1;\n\t} finally {\n\t\tlease?.release();\n\t\tif (projectedGitConfig) rmSync(projectedGitConfig.directory, { force: true, recursive: true });\n\t\tconst cleanupResults = await Promise.allSettled([supervisor.close(), credentialProxy?.close()]);\n\t\tsupervisorState.dispose();\n\t\tfor (const violation of violationStore.list()) {\n\t\t\tdependencies.stderr.write(\n\t\t\t\t`Sandbox violation (${violation.kind}): ${violation.command} — ${violation.detail}\\n`,\n\t\t\t);\n\t\t}\n\t\tfor (const result of cleanupResults) {\n\t\t\tif (result.status === \"rejected\") {\n\t\t\t\tdependencies.stderr.write(\"Warning: sandbox cleanup did not complete cleanly.\\n\");\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The explicit opt-out from OS containment.
|
|
3
|
+
*
|
|
4
|
+
* ADR 0005 shipped without one on purpose, and its 2026-08-28 amendment records why that
|
|
5
|
+
* is now a flag rather than an absence: the delegation work removed the reasons people
|
|
6
|
+
* were reaching for an escape hatch, so the hatch can exist without becoming the default
|
|
7
|
+
* route around a boundary that could not otherwise be worked with.
|
|
8
|
+
*
|
|
9
|
+
* Two rules keep it honest. It is spelled out in full on the command line, never inferred
|
|
10
|
+
* from settings, so no repository and no saved configuration can turn the boundary off.
|
|
11
|
+
* And it always announces itself, because a session that silently ran unconfined is
|
|
12
|
+
* indistinguishable afterwards from one that did not.
|
|
13
|
+
*/
|
|
14
|
+
export declare function writeFullAccessBanner(stderr: {
|
|
15
|
+
write(message: string): boolean;
|
|
16
|
+
}): void;
|
|
17
|
+
/**
|
|
18
|
+
* Confirm the opt-out interactively, or accept it unprompted where no human could answer.
|
|
19
|
+
*
|
|
20
|
+
* A non-interactive session is the case the flag legitimately exists for -- CI that is
|
|
21
|
+
* already externally sandboxed -- and there is nobody there to type. Prompting anyway
|
|
22
|
+
* would hang the run rather than protect anything, so the banner carries the weight.
|
|
23
|
+
*/
|
|
24
|
+
export declare function confirmFullAccess(options: {
|
|
25
|
+
input?: NodeJS.ReadStream;
|
|
26
|
+
output?: NodeJS.WriteStream;
|
|
27
|
+
}): Promise<boolean>;
|
|
28
|
+
//# sourceMappingURL=full-access.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"full-access.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/full-access.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAYH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE;IAAE,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,GAAG,IAAI,CAEvF;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC,WAAW,CAAC;CAC5B,GAAG,OAAO,CAAC,OAAO,CAAC,CA4BnB","sourcesContent":["/**\n * The explicit opt-out from OS containment.\n *\n * ADR 0005 shipped without one on purpose, and its 2026-08-28 amendment records why that\n * is now a flag rather than an absence: the delegation work removed the reasons people\n * were reaching for an escape hatch, so the hatch can exist without becoming the default\n * route around a boundary that could not otherwise be worked with.\n *\n * Two rules keep it honest. It is spelled out in full on the command line, never inferred\n * from settings, so no repository and no saved configuration can turn the boundary off.\n * And it always announces itself, because a session that silently ran unconfined is\n * indistinguishable afterwards from one that did not.\n */\n\nconst BANNER = [\n\t\"\",\n\t\" ! OS sandbox disabled for this session (--sandbox danger-full-access).\",\n\t\"\",\n\t\" Tool calls and every process they spawn run with your full account authority:\",\n\t\" your home directory, your credentials, and unrestricted network access.\",\n\t\" The permission gate still applies. It is not a containment boundary.\",\n\t\"\",\n].join(\"\\n\");\n\nexport function writeFullAccessBanner(stderr: { write(message: string): boolean }): void {\n\tstderr.write(`${BANNER}\\n`);\n}\n\n/**\n * Confirm the opt-out interactively, or accept it unprompted where no human could answer.\n *\n * A non-interactive session is the case the flag legitimately exists for -- CI that is\n * already externally sandboxed -- and there is nobody there to type. Prompting anyway\n * would hang the run rather than protect anything, so the banner carries the weight.\n */\nexport function confirmFullAccess(options: {\n\tinput?: NodeJS.ReadStream;\n\toutput?: NodeJS.WriteStream;\n}): Promise<boolean> {\n\tconst input = options.input ?? process.stdin;\n\tconst output = options.output ?? process.stdout;\n\tif (!input.isTTY || !output.isTTY) return Promise.resolve(true);\n\n\toutput.write(\"Run this session with no OS sandbox? [y/N] \");\n\treturn new Promise((resolve) => {\n\t\tlet buffer = \"\";\n\t\tconst finish = (value: string) => {\n\t\t\tinput.off(\"data\", onData);\n\t\t\tinput.off(\"error\", onError);\n\t\t\tinput.pause();\n\t\t\tconst answer = value.trim().toLowerCase();\n\t\t\toutput.write(\"\\n\");\n\t\t\tresolve(answer === \"y\" || answer === \"yes\");\n\t\t};\n\t\tconst onData = (chunk: Buffer | string) => {\n\t\t\tbuffer += chunk.toString();\n\t\t\tconst newline = buffer.indexOf(\"\\n\");\n\t\t\tif (newline >= 0) finish(buffer.slice(0, newline));\n\t\t};\n\t\t// Anything unreadable declines, because an unanswerable prompt must not be the way\n\t\t// containment gets switched off.\n\t\tconst onError = () => finish(\"\");\n\t\tinput.on(\"data\", onData);\n\t\tinput.on(\"error\", onError);\n\t\tinput.resume();\n\t});\n}\n"]}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The explicit opt-out from OS containment.
|
|
3
|
+
*
|
|
4
|
+
* ADR 0005 shipped without one on purpose, and its 2026-08-28 amendment records why that
|
|
5
|
+
* is now a flag rather than an absence: the delegation work removed the reasons people
|
|
6
|
+
* were reaching for an escape hatch, so the hatch can exist without becoming the default
|
|
7
|
+
* route around a boundary that could not otherwise be worked with.
|
|
8
|
+
*
|
|
9
|
+
* Two rules keep it honest. It is spelled out in full on the command line, never inferred
|
|
10
|
+
* from settings, so no repository and no saved configuration can turn the boundary off.
|
|
11
|
+
* And it always announces itself, because a session that silently ran unconfined is
|
|
12
|
+
* indistinguishable afterwards from one that did not.
|
|
13
|
+
*/
|
|
14
|
+
const BANNER = [
|
|
15
|
+
"",
|
|
16
|
+
" ! OS sandbox disabled for this session (--sandbox danger-full-access).",
|
|
17
|
+
"",
|
|
18
|
+
" Tool calls and every process they spawn run with your full account authority:",
|
|
19
|
+
" your home directory, your credentials, and unrestricted network access.",
|
|
20
|
+
" The permission gate still applies. It is not a containment boundary.",
|
|
21
|
+
"",
|
|
22
|
+
].join("\n");
|
|
23
|
+
export function writeFullAccessBanner(stderr) {
|
|
24
|
+
stderr.write(`${BANNER}\n`);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Confirm the opt-out interactively, or accept it unprompted where no human could answer.
|
|
28
|
+
*
|
|
29
|
+
* A non-interactive session is the case the flag legitimately exists for -- CI that is
|
|
30
|
+
* already externally sandboxed -- and there is nobody there to type. Prompting anyway
|
|
31
|
+
* would hang the run rather than protect anything, so the banner carries the weight.
|
|
32
|
+
*/
|
|
33
|
+
export function confirmFullAccess(options) {
|
|
34
|
+
const input = options.input ?? process.stdin;
|
|
35
|
+
const output = options.output ?? process.stdout;
|
|
36
|
+
if (!input.isTTY || !output.isTTY)
|
|
37
|
+
return Promise.resolve(true);
|
|
38
|
+
output.write("Run this session with no OS sandbox? [y/N] ");
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
let buffer = "";
|
|
41
|
+
const finish = (value) => {
|
|
42
|
+
input.off("data", onData);
|
|
43
|
+
input.off("error", onError);
|
|
44
|
+
input.pause();
|
|
45
|
+
const answer = value.trim().toLowerCase();
|
|
46
|
+
output.write("\n");
|
|
47
|
+
resolve(answer === "y" || answer === "yes");
|
|
48
|
+
};
|
|
49
|
+
const onData = (chunk) => {
|
|
50
|
+
buffer += chunk.toString();
|
|
51
|
+
const newline = buffer.indexOf("\n");
|
|
52
|
+
if (newline >= 0)
|
|
53
|
+
finish(buffer.slice(0, newline));
|
|
54
|
+
};
|
|
55
|
+
// Anything unreadable declines, because an unanswerable prompt must not be the way
|
|
56
|
+
// containment gets switched off.
|
|
57
|
+
const onError = () => finish("");
|
|
58
|
+
input.on("data", onData);
|
|
59
|
+
input.on("error", onError);
|
|
60
|
+
input.resume();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=full-access.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"full-access.js","sourceRoot":"","sources":["../../../src/core/sandbox/full-access.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,MAAM,GAAG;IACd,EAAE;IACF,0EAA0E;IAC1E,EAAE;IACF,mFAAmF;IACnF,6EAA6E;IAC7E,0EAA0E;IAC1E,EAAE;CACF,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb,MAAM,UAAU,qBAAqB,CAAC,MAA2C,EAAQ;IACxF,MAAM,CAAC,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC,CAAC;AAAA,CAC5B;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAGjC,EAAoB;IACpB,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK,CAAC;IAC7C,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;IAChD,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,MAAM,CAAC,KAAK;QAAE,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAEhE,MAAM,CAAC,KAAK,CAAC,6CAA6C,CAAC,CAAC;IAC5D,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;QAC/B,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,MAAM,MAAM,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC;YACjC,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YAC1B,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC5B,KAAK,CAAC,KAAK,EAAE,CAAC;YACd,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YAC1C,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACnB,OAAO,CAAC,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,KAAK,CAAC,CAAC;QAAA,CAC5C,CAAC;QACF,MAAM,MAAM,GAAG,CAAC,KAAsB,EAAE,EAAE,CAAC;YAC1C,MAAM,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YAC3B,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YACrC,IAAI,OAAO,IAAI,CAAC;gBAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;QAAA,CACnD,CAAC;QACF,mFAAmF;QACnF,iCAAiC;QACjC,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACjC,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACzB,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC3B,KAAK,CAAC,MAAM,EAAE,CAAC;IAAA,CACf,CAAC,CAAC;AAAA,CACH","sourcesContent":["/**\n * The explicit opt-out from OS containment.\n *\n * ADR 0005 shipped without one on purpose, and its 2026-08-28 amendment records why that\n * is now a flag rather than an absence: the delegation work removed the reasons people\n * were reaching for an escape hatch, so the hatch can exist without becoming the default\n * route around a boundary that could not otherwise be worked with.\n *\n * Two rules keep it honest. It is spelled out in full on the command line, never inferred\n * from settings, so no repository and no saved configuration can turn the boundary off.\n * And it always announces itself, because a session that silently ran unconfined is\n * indistinguishable afterwards from one that did not.\n */\n\nconst BANNER = [\n\t\"\",\n\t\" ! OS sandbox disabled for this session (--sandbox danger-full-access).\",\n\t\"\",\n\t\" Tool calls and every process they spawn run with your full account authority:\",\n\t\" your home directory, your credentials, and unrestricted network access.\",\n\t\" The permission gate still applies. It is not a containment boundary.\",\n\t\"\",\n].join(\"\\n\");\n\nexport function writeFullAccessBanner(stderr: { write(message: string): boolean }): void {\n\tstderr.write(`${BANNER}\\n`);\n}\n\n/**\n * Confirm the opt-out interactively, or accept it unprompted where no human could answer.\n *\n * A non-interactive session is the case the flag legitimately exists for -- CI that is\n * already externally sandboxed -- and there is nobody there to type. Prompting anyway\n * would hang the run rather than protect anything, so the banner carries the weight.\n */\nexport function confirmFullAccess(options: {\n\tinput?: NodeJS.ReadStream;\n\toutput?: NodeJS.WriteStream;\n}): Promise<boolean> {\n\tconst input = options.input ?? process.stdin;\n\tconst output = options.output ?? process.stdout;\n\tif (!input.isTTY || !output.isTTY) return Promise.resolve(true);\n\n\toutput.write(\"Run this session with no OS sandbox? [y/N] \");\n\treturn new Promise((resolve) => {\n\t\tlet buffer = \"\";\n\t\tconst finish = (value: string) => {\n\t\t\tinput.off(\"data\", onData);\n\t\t\tinput.off(\"error\", onError);\n\t\t\tinput.pause();\n\t\t\tconst answer = value.trim().toLowerCase();\n\t\t\toutput.write(\"\\n\");\n\t\t\tresolve(answer === \"y\" || answer === \"yes\");\n\t\t};\n\t\tconst onData = (chunk: Buffer | string) => {\n\t\t\tbuffer += chunk.toString();\n\t\t\tconst newline = buffer.indexOf(\"\\n\");\n\t\t\tif (newline >= 0) finish(buffer.slice(0, newline));\n\t\t};\n\t\t// Anything unreadable declines, because an unanswerable prompt must not be the way\n\t\t// containment gets switched off.\n\t\tconst onError = () => finish(\"\");\n\t\tinput.on(\"data\", onData);\n\t\tinput.on(\"error\", onError);\n\t\tinput.resume();\n\t});\n}\n"]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface HostGitIdentity {
|
|
2
|
+
readonly name: string;
|
|
3
|
+
readonly email: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ProjectedGitConfig {
|
|
6
|
+
readonly directory: string;
|
|
7
|
+
readonly path: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Read the host's git identity before the sandbox exists to hide it.
|
|
11
|
+
*
|
|
12
|
+
* `--global` rather than a plain lookup, because the scope flag is what excludes the
|
|
13
|
+
* workspace's own `.git/config` by construction rather than by choosing a lucky working
|
|
14
|
+
* directory. A repository must not be able to decide what the supervisor projects, the
|
|
15
|
+
* same rule ADR 0016 applies to the network allowlist. Repository scope still works
|
|
16
|
+
* normally inside the child: the workspace is bind-mounted read-write, so git reads
|
|
17
|
+
* `.git/config` there itself and it wins over this projection, exactly as it would on the
|
|
18
|
+
* host.
|
|
19
|
+
*
|
|
20
|
+
* System scope (`/etc/gitconfig`) is deliberately not consulted. A machine-wide commit
|
|
21
|
+
* identity is not a real configuration, and a second `spawnSync` to find one would cost
|
|
22
|
+
* every launch what it saves nobody.
|
|
23
|
+
*
|
|
24
|
+
* Returns undefined unless both keys are present. A name without an email still fails to
|
|
25
|
+
* author a commit, so projecting half an identity would replace one confusing error with
|
|
26
|
+
* another.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveHostGitIdentity(options?: {
|
|
29
|
+
/** Injectable only for tests; production uses the supervisor's own environment. */
|
|
30
|
+
environment?: NodeJS.ProcessEnv;
|
|
31
|
+
/**
|
|
32
|
+
* Injectable only for tests, so one can prove repository scope stays excluded even
|
|
33
|
+
* when the lookup runs from inside a repository.
|
|
34
|
+
*/
|
|
35
|
+
cwd?: string;
|
|
36
|
+
}): HostGitIdentity | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Write a two-key config the child can be pointed at through `GIT_CONFIG_GLOBAL`.
|
|
39
|
+
*
|
|
40
|
+
* Synthesized rather than copied. A real `~/.gitconfig` can carry a `credential.helper`
|
|
41
|
+
* that runs a command, or an `insteadOf` rule rewriting a remote to one carrying a token;
|
|
42
|
+
* projecting the host file would hand both to the child along with the name we wanted.
|
|
43
|
+
* Building the file from a resolved identity means it cannot contain anything else.
|
|
44
|
+
*
|
|
45
|
+
* The directory is its own, not shared with the credential projection, and `0700` for the
|
|
46
|
+
* same reason `resolveCredentialChannelPaths` uses one. TMPDIR is deliberately not
|
|
47
|
+
* honoured: it can point inside the workspace on macOS, which the sandbox may write.
|
|
48
|
+
*/
|
|
49
|
+
export declare function createProjectedGitConfig(identity: HostGitIdentity, options?: {
|
|
50
|
+
/**
|
|
51
|
+
* A `credential.helper` value for the child. It names the supervisor-mediated
|
|
52
|
+
* helper, never a host one: the point is that the child asks over the channel
|
|
53
|
+
* rather than reading any store itself.
|
|
54
|
+
*/
|
|
55
|
+
credentialHelper?: string;
|
|
56
|
+
}): ProjectedGitConfig;
|
|
57
|
+
//# sourceMappingURL=git-identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-identity.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/git-identity.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAKD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,CAAC,EAAE;IAChD,mFAAmF;IACnF,WAAW,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAChC;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACb,GAAG,eAAe,GAAG,SAAS,CAoB9B;AAOD;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CACvC,QAAQ,EAAE,eAAe,EACzB,OAAO,CAAC,EAAE;IACT;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B,GACC,kBAAkB,CAUpB","sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport { chmodSync, mkdtempSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { supervisorTempDirectory } from \"./supervisor-temp.ts\";\n\nexport interface HostGitIdentity {\n\treadonly name: string;\n\treadonly email: string;\n}\n\nexport interface ProjectedGitConfig {\n\treadonly directory: string;\n\treadonly path: string;\n}\n\n/** Long enough for a cold `git` on a slow disk, short enough not to stall every launch. */\nconst RESOLUTION_TIMEOUT_MILLISECONDS = 2_000;\n\n/**\n * Read the host's git identity before the sandbox exists to hide it.\n *\n * `--global` rather than a plain lookup, because the scope flag is what excludes the\n * workspace's own `.git/config` by construction rather than by choosing a lucky working\n * directory. A repository must not be able to decide what the supervisor projects, the\n * same rule ADR 0016 applies to the network allowlist. Repository scope still works\n * normally inside the child: the workspace is bind-mounted read-write, so git reads\n * `.git/config` there itself and it wins over this projection, exactly as it would on the\n * host.\n *\n * System scope (`/etc/gitconfig`) is deliberately not consulted. A machine-wide commit\n * identity is not a real configuration, and a second `spawnSync` to find one would cost\n * every launch what it saves nobody.\n *\n * Returns undefined unless both keys are present. A name without an email still fails to\n * author a commit, so projecting half an identity would replace one confusing error with\n * another.\n */\nexport function resolveHostGitIdentity(options?: {\n\t/** Injectable only for tests; production uses the supervisor's own environment. */\n\tenvironment?: NodeJS.ProcessEnv;\n\t/**\n\t * Injectable only for tests, so one can prove repository scope stays excluded even\n\t * when the lookup runs from inside a repository.\n\t */\n\tcwd?: string;\n}): HostGitIdentity | undefined {\n\tconst result = spawnSync(\"git\", [\"config\", \"--global\", \"--get-regexp\", \"^user\\\\.(name|email)$\"], {\n\t\tcwd: options?.cwd,\n\t\tencoding: \"utf8\",\n\t\tenv: options?.environment ?? process.env,\n\t\ttimeout: RESOLUTION_TIMEOUT_MILLISECONDS,\n\t});\n\tif (result.status !== 0 || typeof result.stdout !== \"string\") return undefined;\n\n\tlet name: string | undefined;\n\tlet email: string | undefined;\n\tfor (const line of result.stdout.split(\"\\n\")) {\n\t\tconst separator = line.indexOf(\" \");\n\t\tif (separator < 0) continue;\n\t\tconst value = line.slice(separator + 1).trim();\n\t\tif (value.length === 0) continue;\n\t\tif (line.slice(0, separator) === \"user.name\") name = value;\n\t\tif (line.slice(0, separator) === \"user.email\") email = value;\n\t}\n\treturn name && email ? { name, email } : undefined;\n}\n\n/** Escape a value for git's config syntax, which reads a quoted string with backslash escapes. */\nfunction quote(value: string): string {\n\treturn `\"${value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"')}\"`;\n}\n\n/**\n * Write a two-key config the child can be pointed at through `GIT_CONFIG_GLOBAL`.\n *\n * Synthesized rather than copied. A real `~/.gitconfig` can carry a `credential.helper`\n * that runs a command, or an `insteadOf` rule rewriting a remote to one carrying a token;\n * projecting the host file would hand both to the child along with the name we wanted.\n * Building the file from a resolved identity means it cannot contain anything else.\n *\n * The directory is its own, not shared with the credential projection, and `0700` for the\n * same reason `resolveCredentialChannelPaths` uses one. TMPDIR is deliberately not\n * honoured: it can point inside the workspace on macOS, which the sandbox may write.\n */\nexport function createProjectedGitConfig(\n\tidentity: HostGitIdentity,\n\toptions?: {\n\t\t/**\n\t\t * A `credential.helper` value for the child. It names the supervisor-mediated\n\t\t * helper, never a host one: the point is that the child asks over the channel\n\t\t * rather than reading any store itself.\n\t\t */\n\t\tcredentialHelper?: string;\n\t},\n): ProjectedGitConfig {\n\tconst directory = mkdtempSync(join(supervisorTempDirectory(), `apex-git-${process.pid}-`), { encoding: \"utf8\" });\n\tchmodSync(directory, 0o700);\n\tconst path = join(directory, \"config\");\n\tconst sections = [`[user]\\n\\tname = ${quote(identity.name)}\\n\\temail = ${quote(identity.email)}\\n`];\n\tif (options?.credentialHelper) {\n\t\tsections.push(`[credential]\\n\\thelper = ${quote(options.credentialHelper)}\\n`);\n\t}\n\twriteFileSync(path, sections.join(\"\"), { mode: 0o600 });\n\treturn { directory, path };\n}\n"]}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { chmodSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { supervisorTempDirectory } from "./supervisor-temp.js";
|
|
5
|
+
/** Long enough for a cold `git` on a slow disk, short enough not to stall every launch. */
|
|
6
|
+
const RESOLUTION_TIMEOUT_MILLISECONDS = 2_000;
|
|
7
|
+
/**
|
|
8
|
+
* Read the host's git identity before the sandbox exists to hide it.
|
|
9
|
+
*
|
|
10
|
+
* `--global` rather than a plain lookup, because the scope flag is what excludes the
|
|
11
|
+
* workspace's own `.git/config` by construction rather than by choosing a lucky working
|
|
12
|
+
* directory. A repository must not be able to decide what the supervisor projects, the
|
|
13
|
+
* same rule ADR 0016 applies to the network allowlist. Repository scope still works
|
|
14
|
+
* normally inside the child: the workspace is bind-mounted read-write, so git reads
|
|
15
|
+
* `.git/config` there itself and it wins over this projection, exactly as it would on the
|
|
16
|
+
* host.
|
|
17
|
+
*
|
|
18
|
+
* System scope (`/etc/gitconfig`) is deliberately not consulted. A machine-wide commit
|
|
19
|
+
* identity is not a real configuration, and a second `spawnSync` to find one would cost
|
|
20
|
+
* every launch what it saves nobody.
|
|
21
|
+
*
|
|
22
|
+
* Returns undefined unless both keys are present. A name without an email still fails to
|
|
23
|
+
* author a commit, so projecting half an identity would replace one confusing error with
|
|
24
|
+
* another.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveHostGitIdentity(options) {
|
|
27
|
+
const result = spawnSync("git", ["config", "--global", "--get-regexp", "^user\\.(name|email)$"], {
|
|
28
|
+
cwd: options?.cwd,
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
env: options?.environment ?? process.env,
|
|
31
|
+
timeout: RESOLUTION_TIMEOUT_MILLISECONDS,
|
|
32
|
+
});
|
|
33
|
+
if (result.status !== 0 || typeof result.stdout !== "string")
|
|
34
|
+
return undefined;
|
|
35
|
+
let name;
|
|
36
|
+
let email;
|
|
37
|
+
for (const line of result.stdout.split("\n")) {
|
|
38
|
+
const separator = line.indexOf(" ");
|
|
39
|
+
if (separator < 0)
|
|
40
|
+
continue;
|
|
41
|
+
const value = line.slice(separator + 1).trim();
|
|
42
|
+
if (value.length === 0)
|
|
43
|
+
continue;
|
|
44
|
+
if (line.slice(0, separator) === "user.name")
|
|
45
|
+
name = value;
|
|
46
|
+
if (line.slice(0, separator) === "user.email")
|
|
47
|
+
email = value;
|
|
48
|
+
}
|
|
49
|
+
return name && email ? { name, email } : undefined;
|
|
50
|
+
}
|
|
51
|
+
/** Escape a value for git's config syntax, which reads a quoted string with backslash escapes. */
|
|
52
|
+
function quote(value) {
|
|
53
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Write a two-key config the child can be pointed at through `GIT_CONFIG_GLOBAL`.
|
|
57
|
+
*
|
|
58
|
+
* Synthesized rather than copied. A real `~/.gitconfig` can carry a `credential.helper`
|
|
59
|
+
* that runs a command, or an `insteadOf` rule rewriting a remote to one carrying a token;
|
|
60
|
+
* projecting the host file would hand both to the child along with the name we wanted.
|
|
61
|
+
* Building the file from a resolved identity means it cannot contain anything else.
|
|
62
|
+
*
|
|
63
|
+
* The directory is its own, not shared with the credential projection, and `0700` for the
|
|
64
|
+
* same reason `resolveCredentialChannelPaths` uses one. TMPDIR is deliberately not
|
|
65
|
+
* honoured: it can point inside the workspace on macOS, which the sandbox may write.
|
|
66
|
+
*/
|
|
67
|
+
export function createProjectedGitConfig(identity, options) {
|
|
68
|
+
const directory = mkdtempSync(join(supervisorTempDirectory(), `apex-git-${process.pid}-`), { encoding: "utf8" });
|
|
69
|
+
chmodSync(directory, 0o700);
|
|
70
|
+
const path = join(directory, "config");
|
|
71
|
+
const sections = [`[user]\n\tname = ${quote(identity.name)}\n\temail = ${quote(identity.email)}\n`];
|
|
72
|
+
if (options?.credentialHelper) {
|
|
73
|
+
sections.push(`[credential]\n\thelper = ${quote(options.credentialHelper)}\n`);
|
|
74
|
+
}
|
|
75
|
+
writeFileSync(path, sections.join(""), { mode: 0o600 });
|
|
76
|
+
return { directory, path };
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=git-identity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-identity.js","sourceRoot":"","sources":["../../../src/core/sandbox/git-identity.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport { chmodSync, mkdtempSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { supervisorTempDirectory } from \"./supervisor-temp.ts\";\n\nexport interface HostGitIdentity {\n\treadonly name: string;\n\treadonly email: string;\n}\n\nexport interface ProjectedGitConfig {\n\treadonly directory: string;\n\treadonly path: string;\n}\n\n/** Long enough for a cold `git` on a slow disk, short enough not to stall every launch. */\nconst RESOLUTION_TIMEOUT_MILLISECONDS = 2_000;\n\n/**\n * Read the host's git identity before the sandbox exists to hide it.\n *\n * `--global` rather than a plain lookup, because the scope flag is what excludes the\n * workspace's own `.git/config` by construction rather than by choosing a lucky working\n * directory. A repository must not be able to decide what the supervisor projects, the\n * same rule ADR 0016 applies to the network allowlist. Repository scope still works\n * normally inside the child: the workspace is bind-mounted read-write, so git reads\n * `.git/config` there itself and it wins over this projection, exactly as it would on the\n * host.\n *\n * System scope (`/etc/gitconfig`) is deliberately not consulted. A machine-wide commit\n * identity is not a real configuration, and a second `spawnSync` to find one would cost\n * every launch what it saves nobody.\n *\n * Returns undefined unless both keys are present. A name without an email still fails to\n * author a commit, so projecting half an identity would replace one confusing error with\n * another.\n */\nexport function resolveHostGitIdentity(options?: {\n\t/** Injectable only for tests; production uses the supervisor's own environment. */\n\tenvironment?: NodeJS.ProcessEnv;\n\t/**\n\t * Injectable only for tests, so one can prove repository scope stays excluded even\n\t * when the lookup runs from inside a repository.\n\t */\n\tcwd?: string;\n}): HostGitIdentity | undefined {\n\tconst result = spawnSync(\"git\", [\"config\", \"--global\", \"--get-regexp\", \"^user\\\\.(name|email)$\"], {\n\t\tcwd: options?.cwd,\n\t\tencoding: \"utf8\",\n\t\tenv: options?.environment ?? process.env,\n\t\ttimeout: RESOLUTION_TIMEOUT_MILLISECONDS,\n\t});\n\tif (result.status !== 0 || typeof result.stdout !== \"string\") return undefined;\n\n\tlet name: string | undefined;\n\tlet email: string | undefined;\n\tfor (const line of result.stdout.split(\"\\n\")) {\n\t\tconst separator = line.indexOf(\" \");\n\t\tif (separator < 0) continue;\n\t\tconst value = line.slice(separator + 1).trim();\n\t\tif (value.length === 0) continue;\n\t\tif (line.slice(0, separator) === \"user.name\") name = value;\n\t\tif (line.slice(0, separator) === \"user.email\") email = value;\n\t}\n\treturn name && email ? { name, email } : undefined;\n}\n\n/** Escape a value for git's config syntax, which reads a quoted string with backslash escapes. */\nfunction quote(value: string): string {\n\treturn `\"${value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"')}\"`;\n}\n\n/**\n * Write a two-key config the child can be pointed at through `GIT_CONFIG_GLOBAL`.\n *\n * Synthesized rather than copied. A real `~/.gitconfig` can carry a `credential.helper`\n * that runs a command, or an `insteadOf` rule rewriting a remote to one carrying a token;\n * projecting the host file would hand both to the child along with the name we wanted.\n * Building the file from a resolved identity means it cannot contain anything else.\n *\n * The directory is its own, not shared with the credential projection, and `0700` for the\n * same reason `resolveCredentialChannelPaths` uses one. TMPDIR is deliberately not\n * honoured: it can point inside the workspace on macOS, which the sandbox may write.\n */\nexport function createProjectedGitConfig(\n\tidentity: HostGitIdentity,\n\toptions?: {\n\t\t/**\n\t\t * A `credential.helper` value for the child. It names the supervisor-mediated\n\t\t * helper, never a host one: the point is that the child asks over the channel\n\t\t * rather than reading any store itself.\n\t\t */\n\t\tcredentialHelper?: string;\n\t},\n): ProjectedGitConfig {\n\tconst directory = mkdtempSync(join(supervisorTempDirectory(), `apex-git-${process.pid}-`), { encoding: \"utf8\" });\n\tchmodSync(directory, 0o700);\n\tconst path = join(directory, \"config\");\n\tconst sections = [`[user]\\n\\tname = ${quote(identity.name)}\\n\\temail = ${quote(identity.email)}\\n`];\n\tif (options?.credentialHelper) {\n\t\tsections.push(`[credential]\\n\\thelper = ${quote(options.credentialHelper)}\\n`);\n\t}\n\twriteFileSync(path, sections.join(\"\"), { mode: 0o600 });\n\treturn { directory, path };\n}\n"]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { TerminalHandoff } from "./terminal-handoff.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The supervisor's own approval prompt for one refused host.
|
|
4
|
+
*
|
|
5
|
+
* ADR 0023 puts the decision here rather than in the child. An approval asserted from
|
|
6
|
+
* inside the boundary is indistinguishable, at the supervisor, from one forged by a
|
|
7
|
+
* postinstall script or a git hook -- the code the boundary exists to contain -- so the
|
|
8
|
+
* human's answer is read by the side that owns the decision, on a terminal borrowed back
|
|
9
|
+
* from the child for the duration.
|
|
10
|
+
*
|
|
11
|
+
* Returning undefined when there is no terminal is the mechanism, not a fallback. The
|
|
12
|
+
* proxy's own deny-without-asking path then runs unchanged, so ADR 0005's headless, print,
|
|
13
|
+
* JSON, and RPC behaviour holds by construction rather than by a mode check that a later
|
|
14
|
+
* change could forget to make.
|
|
15
|
+
*/
|
|
16
|
+
export interface TerminalPromptOptions {
|
|
17
|
+
handoff: TerminalHandoff;
|
|
18
|
+
input?: NodeJS.ReadStream;
|
|
19
|
+
output?: NodeJS.WriteStream;
|
|
20
|
+
}
|
|
21
|
+
/** Approve one refused host for the rest of the session. */
|
|
22
|
+
export declare function createHostApprover(options: TerminalPromptOptions): ((hostname: string, port: number) => Promise<boolean>) | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Release the host's own git credential for one host, for the rest of the session.
|
|
25
|
+
*
|
|
26
|
+
* Separate from the host approval above even though both are yes/no, because they are
|
|
27
|
+
* different grants and conflating them would let approving a push also release a token to
|
|
28
|
+
* everything else the session later contacts.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createCredentialReleaser(options: TerminalPromptOptions): ((host: string) => Promise<boolean>) | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Approve one command to run in a separate child with one extra writable root.
|
|
33
|
+
*
|
|
34
|
+
* Unlike a host or a credential, nothing is remembered. A command is not a stable subject
|
|
35
|
+
* to grant against, and a remembered approval would silently cover the next command that
|
|
36
|
+
* happened to name the same root.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createCommandEscalationApprover(options: TerminalPromptOptions): ((request: {
|
|
39
|
+
command: string;
|
|
40
|
+
writableRoot: string;
|
|
41
|
+
}) => Promise<boolean>) | undefined;
|
|
42
|
+
//# sourceMappingURL=host-approval.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-approval.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/host-approval.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,qBAAqB;IACrC,OAAO,EAAE,eAAe,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC,WAAW,CAAC;CAC5B;AAyDD,4DAA4D;AAC5D,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,qBAAqB,GAC5B,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,SAAS,CAUpE;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACvC,OAAO,EAAE,qBAAqB,GAC5B,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,SAAS,CASlD;AAED;;;;;;GAMG;AACH,wBAAgB,+BAA+B,CAC9C,OAAO,EAAE,qBAAqB,GAC5B,CAAC,CAAC,OAAO,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,SAAS,CAYxF","sourcesContent":["import type { TerminalHandoff } from \"./terminal-handoff.ts\";\n\n/**\n * The supervisor's own approval prompt for one refused host.\n *\n * ADR 0023 puts the decision here rather than in the child. An approval asserted from\n * inside the boundary is indistinguishable, at the supervisor, from one forged by a\n * postinstall script or a git hook -- the code the boundary exists to contain -- so the\n * human's answer is read by the side that owns the decision, on a terminal borrowed back\n * from the child for the duration.\n *\n * Returning undefined when there is no terminal is the mechanism, not a fallback. The\n * proxy's own deny-without-asking path then runs unchanged, so ADR 0005's headless, print,\n * JSON, and RPC behaviour holds by construction rather than by a mode check that a later\n * change could forget to make.\n */\nexport interface TerminalPromptOptions {\n\thandoff: TerminalHandoff;\n\tinput?: NodeJS.ReadStream;\n\toutput?: NodeJS.WriteStream;\n}\n\n/**\n * Build a yes/no prompt the supervisor owns, or nothing at all when there is no terminal.\n *\n * Both sandbox escalations ask the same shape of question and must fail closed the same\n * way, so they share one implementation. Returning undefined rather than a\n * function-that-says-no is deliberate: the caller's own refuse path then runs untouched,\n * which is how ADR 0005's headless behaviour stays correct without a mode check.\n */\nfunction createTerminalPrompt(\n\toptions: TerminalPromptOptions,\n\tcompose: (subject: string) => { question: string; granted: string; refused: string },\n): ((subject: string) => Promise<boolean>) | undefined {\n\tconst input = options.input ?? process.stdin;\n\tconst output = options.output ?? process.stdout;\n\tif (!input.isTTY || !output.isTTY) return undefined;\n\n\tfunction readAnswer(): Promise<string> {\n\t\treturn new Promise((resolve) => {\n\t\t\tlet buffer = \"\";\n\t\t\tconst finish = (value: string) => {\n\t\t\t\tinput.off(\"data\", onData);\n\t\t\t\tinput.off(\"error\", onError);\n\t\t\t\tinput.off(\"end\", onEnd);\n\t\t\t\t// The child had stdin in raw mode and takes it back on resume, so the\n\t\t\t\t// supervisor leaves the stream paused rather than owning it past the answer.\n\t\t\t\tinput.pause();\n\t\t\t\tresolve(value);\n\t\t\t};\n\t\t\tconst onData = (chunk: Buffer | string) => {\n\t\t\t\tbuffer += chunk.toString();\n\t\t\t\tconst newline = buffer.indexOf(\"\\n\");\n\t\t\t\tif (newline >= 0) finish(buffer.slice(0, newline));\n\t\t\t};\n\t\t\t// Anything unreadable declines. Treating an unanswerable prompt as consent would\n\t\t\t// hand the grant to whatever broke the terminal.\n\t\t\tconst onError = () => finish(\"\");\n\t\t\tconst onEnd = () => finish(buffer);\n\t\t\tinput.on(\"data\", onData);\n\t\t\tinput.on(\"error\", onError);\n\t\t\tinput.on(\"end\", onEnd);\n\t\t\tinput.resume();\n\t\t});\n\t}\n\n\treturn (subject: string): Promise<boolean> =>\n\t\toptions.handoff.borrowTerminal(async () => {\n\t\t\tconst text = compose(subject);\n\t\t\toutput.write(`\\n${text.question}`);\n\t\t\tconst answer = (await readAnswer()).trim().toLowerCase();\n\t\t\tconst approved = answer === \"y\" || answer === \"yes\";\n\t\t\toutput.write(`${approved ? text.granted : text.refused}\\n\\n`);\n\t\t\treturn approved;\n\t\t});\n}\n\n/** Approve one refused host for the rest of the session. */\nexport function createHostApprover(\n\toptions: TerminalPromptOptions,\n): ((hostname: string, port: number) => Promise<boolean>) | undefined {\n\tconst prompt = createTerminalPrompt(options, (subject) => ({\n\t\tquestion:\n\t\t\t`The sandboxed session asked to reach ${subject}, which is not on the network allowlist.\\n` +\n\t\t\t`Allow it for this session only? [y/N] `,\n\t\tgranted: `Allowed ${subject} for this session. Add it to \"network.allowedHosts\" in your global settings to make it permanent.`,\n\t\trefused: `Refused ${subject}.`,\n\t}));\n\tif (!prompt) return undefined;\n\treturn (hostname: string, port: number) => prompt(`${hostname}:${port}`);\n}\n\n/**\n * Release the host's own git credential for one host, for the rest of the session.\n *\n * Separate from the host approval above even though both are yes/no, because they are\n * different grants and conflating them would let approving a push also release a token to\n * everything else the session later contacts.\n */\nexport function createCredentialReleaser(\n\toptions: TerminalPromptOptions,\n): ((host: string) => Promise<boolean>) | undefined {\n\treturn createTerminalPrompt(options, (subject) => ({\n\t\tquestion:\n\t\t\t`The sandboxed session asked for your git credential for ${subject}.\\n` +\n\t\t\t`It is read from your host credential store and never written into the workspace.\\n` +\n\t\t\t`Release it for this session only? [y/N] `,\n\t\tgranted: `Released the ${subject} credential for this session.`,\n\t\trefused: `Refused the ${subject} credential.`,\n\t}));\n}\n\n/**\n * Approve one command to run in a separate child with one extra writable root.\n *\n * Unlike a host or a credential, nothing is remembered. A command is not a stable subject\n * to grant against, and a remembered approval would silently cover the next command that\n * happened to name the same root.\n */\nexport function createCommandEscalationApprover(\n\toptions: TerminalPromptOptions,\n): ((request: { command: string; writableRoot: string }) => Promise<boolean>) | undefined {\n\tconst prompt = createTerminalPrompt(options, (subject) => ({\n\t\tquestion:\n\t\t\t`The sandboxed session asked to run a command with write access outside its workspace.\\n\\n` +\n\t\t\t` ${subject}\\n\\n` +\n\t\t\t`It runs once, in a separate sandbox. The session's own boundary does not change.\\n` +\n\t\t\t`Allow it? [y/N] `,\n\t\tgranted: \"Ran the command once with that root writable.\",\n\t\trefused: \"Refused.\",\n\t}));\n\tif (!prompt) return undefined;\n\treturn (request) => prompt(`${request.command}\\n (writable: ${request.writableRoot})`);\n}\n"]}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a yes/no prompt the supervisor owns, or nothing at all when there is no terminal.
|
|
3
|
+
*
|
|
4
|
+
* Both sandbox escalations ask the same shape of question and must fail closed the same
|
|
5
|
+
* way, so they share one implementation. Returning undefined rather than a
|
|
6
|
+
* function-that-says-no is deliberate: the caller's own refuse path then runs untouched,
|
|
7
|
+
* which is how ADR 0005's headless behaviour stays correct without a mode check.
|
|
8
|
+
*/
|
|
9
|
+
function createTerminalPrompt(options, compose) {
|
|
10
|
+
const input = options.input ?? process.stdin;
|
|
11
|
+
const output = options.output ?? process.stdout;
|
|
12
|
+
if (!input.isTTY || !output.isTTY)
|
|
13
|
+
return undefined;
|
|
14
|
+
function readAnswer() {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
let buffer = "";
|
|
17
|
+
const finish = (value) => {
|
|
18
|
+
input.off("data", onData);
|
|
19
|
+
input.off("error", onError);
|
|
20
|
+
input.off("end", onEnd);
|
|
21
|
+
// The child had stdin in raw mode and takes it back on resume, so the
|
|
22
|
+
// supervisor leaves the stream paused rather than owning it past the answer.
|
|
23
|
+
input.pause();
|
|
24
|
+
resolve(value);
|
|
25
|
+
};
|
|
26
|
+
const onData = (chunk) => {
|
|
27
|
+
buffer += chunk.toString();
|
|
28
|
+
const newline = buffer.indexOf("\n");
|
|
29
|
+
if (newline >= 0)
|
|
30
|
+
finish(buffer.slice(0, newline));
|
|
31
|
+
};
|
|
32
|
+
// Anything unreadable declines. Treating an unanswerable prompt as consent would
|
|
33
|
+
// hand the grant to whatever broke the terminal.
|
|
34
|
+
const onError = () => finish("");
|
|
35
|
+
const onEnd = () => finish(buffer);
|
|
36
|
+
input.on("data", onData);
|
|
37
|
+
input.on("error", onError);
|
|
38
|
+
input.on("end", onEnd);
|
|
39
|
+
input.resume();
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return (subject) => options.handoff.borrowTerminal(async () => {
|
|
43
|
+
const text = compose(subject);
|
|
44
|
+
output.write(`\n${text.question}`);
|
|
45
|
+
const answer = (await readAnswer()).trim().toLowerCase();
|
|
46
|
+
const approved = answer === "y" || answer === "yes";
|
|
47
|
+
output.write(`${approved ? text.granted : text.refused}\n\n`);
|
|
48
|
+
return approved;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** Approve one refused host for the rest of the session. */
|
|
52
|
+
export function createHostApprover(options) {
|
|
53
|
+
const prompt = createTerminalPrompt(options, (subject) => ({
|
|
54
|
+
question: `The sandboxed session asked to reach ${subject}, which is not on the network allowlist.\n` +
|
|
55
|
+
`Allow it for this session only? [y/N] `,
|
|
56
|
+
granted: `Allowed ${subject} for this session. Add it to "network.allowedHosts" in your global settings to make it permanent.`,
|
|
57
|
+
refused: `Refused ${subject}.`,
|
|
58
|
+
}));
|
|
59
|
+
if (!prompt)
|
|
60
|
+
return undefined;
|
|
61
|
+
return (hostname, port) => prompt(`${hostname}:${port}`);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Release the host's own git credential for one host, for the rest of the session.
|
|
65
|
+
*
|
|
66
|
+
* Separate from the host approval above even though both are yes/no, because they are
|
|
67
|
+
* different grants and conflating them would let approving a push also release a token to
|
|
68
|
+
* everything else the session later contacts.
|
|
69
|
+
*/
|
|
70
|
+
export function createCredentialReleaser(options) {
|
|
71
|
+
return createTerminalPrompt(options, (subject) => ({
|
|
72
|
+
question: `The sandboxed session asked for your git credential for ${subject}.\n` +
|
|
73
|
+
`It is read from your host credential store and never written into the workspace.\n` +
|
|
74
|
+
`Release it for this session only? [y/N] `,
|
|
75
|
+
granted: `Released the ${subject} credential for this session.`,
|
|
76
|
+
refused: `Refused the ${subject} credential.`,
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Approve one command to run in a separate child with one extra writable root.
|
|
81
|
+
*
|
|
82
|
+
* Unlike a host or a credential, nothing is remembered. A command is not a stable subject
|
|
83
|
+
* to grant against, and a remembered approval would silently cover the next command that
|
|
84
|
+
* happened to name the same root.
|
|
85
|
+
*/
|
|
86
|
+
export function createCommandEscalationApprover(options) {
|
|
87
|
+
const prompt = createTerminalPrompt(options, (subject) => ({
|
|
88
|
+
question: `The sandboxed session asked to run a command with write access outside its workspace.\n\n` +
|
|
89
|
+
` ${subject}\n\n` +
|
|
90
|
+
`It runs once, in a separate sandbox. The session's own boundary does not change.\n` +
|
|
91
|
+
`Allow it? [y/N] `,
|
|
92
|
+
granted: "Ran the command once with that root writable.",
|
|
93
|
+
refused: "Refused.",
|
|
94
|
+
}));
|
|
95
|
+
if (!prompt)
|
|
96
|
+
return undefined;
|
|
97
|
+
return (request) => prompt(`${request.command}\n (writable: ${request.writableRoot})`);
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=host-approval.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-approval.js","sourceRoot":"","sources":["../../../src/core/sandbox/host-approval.ts"],"names":[],"mappings":"AAsBA;;;;;;;GAOG;AACH,SAAS,oBAAoB,CAC5B,OAA8B,EAC9B,OAAoF,EAC9B;IACtD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK,CAAC;IAC7C,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;IAChD,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,MAAM,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAEpD,SAAS,UAAU,GAAoB;QACtC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;YAC/B,IAAI,MAAM,GAAG,EAAE,CAAC;YAChB,MAAM,MAAM,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC;gBACjC,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;gBAC1B,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;gBAC5B,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;gBACxB,sEAAsE;gBACtE,6EAA6E;gBAC7E,KAAK,CAAC,KAAK,EAAE,CAAC;gBACd,OAAO,CAAC,KAAK,CAAC,CAAC;YAAA,CACf,CAAC;YACF,MAAM,MAAM,GAAG,CAAC,KAAsB,EAAE,EAAE,CAAC;gBAC1C,MAAM,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;gBAC3B,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;gBACrC,IAAI,OAAO,IAAI,CAAC;oBAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;YAAA,CACnD,CAAC;YACF,iFAAiF;YACjF,iDAAiD;YACjD,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACjC,MAAM,KAAK,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YACnC,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YACzB,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC3B,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YACvB,KAAK,CAAC,MAAM,EAAE,CAAC;QAAA,CACf,CAAC,CAAC;IAAA,CACH;IAED,OAAO,CAAC,OAAe,EAAoB,EAAE,CAC5C,OAAO,CAAC,OAAO,CAAC,cAAc,CAAC,KAAK,IAAI,EAAE,CAAC;QAC1C,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QAC9B,MAAM,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;QACnC,MAAM,MAAM,GAAG,CAAC,MAAM,UAAU,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACzD,MAAM,QAAQ,GAAG,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,KAAK,CAAC;QACpD,MAAM,CAAC,KAAK,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,MAAM,CAAC,CAAC;QAC9D,OAAO,QAAQ,CAAC;IAAA,CAChB,CAAC,CAAC;AAAA,CACJ;AAED,4DAA4D;AAC5D,MAAM,UAAU,kBAAkB,CACjC,OAA8B,EACuC;IACrE,MAAM,MAAM,GAAG,oBAAoB,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QAC1D,QAAQ,EACP,wCAAwC,OAAO,4CAA4C;YAC3F,wCAAwC;QACzC,OAAO,EAAE,WAAW,OAAO,mGAAmG;QAC9H,OAAO,EAAE,WAAW,OAAO,GAAG;KAC9B,CAAC,CAAC,CAAC;IACJ,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAC9B,OAAO,CAAC,QAAgB,EAAE,IAAY,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,QAAQ,IAAI,IAAI,EAAE,CAAC,CAAC;AAAA,CACzE;AAED;;;;;;GAMG;AACH,MAAM,UAAU,wBAAwB,CACvC,OAA8B,EACqB;IACnD,OAAO,oBAAoB,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QAClD,QAAQ,EACP,2DAA2D,OAAO,KAAK;YACvE,oFAAoF;YACpF,0CAA0C;QAC3C,OAAO,EAAE,gBAAgB,OAAO,+BAA+B;QAC/D,OAAO,EAAE,eAAe,OAAO,cAAc;KAC7C,CAAC,CAAC,CAAC;AAAA,CACJ;AAED;;;;;;GAMG;AACH,MAAM,UAAU,+BAA+B,CAC9C,OAA8B,EAC2D;IACzF,MAAM,MAAM,GAAG,oBAAoB,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QAC1D,QAAQ,EACP,2FAA2F;YAC3F,KAAK,OAAO,MAAM;YAClB,oFAAoF;YACpF,kBAAkB;QACnB,OAAO,EAAE,+CAA+C;QACxD,OAAO,EAAE,UAAU;KACnB,CAAC,CAAC,CAAC;IACJ,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAC9B,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,OAAO,kBAAkB,OAAO,CAAC,YAAY,GAAG,CAAC,CAAC;AAAA,CACxF","sourcesContent":["import type { TerminalHandoff } from \"./terminal-handoff.ts\";\n\n/**\n * The supervisor's own approval prompt for one refused host.\n *\n * ADR 0023 puts the decision here rather than in the child. An approval asserted from\n * inside the boundary is indistinguishable, at the supervisor, from one forged by a\n * postinstall script or a git hook -- the code the boundary exists to contain -- so the\n * human's answer is read by the side that owns the decision, on a terminal borrowed back\n * from the child for the duration.\n *\n * Returning undefined when there is no terminal is the mechanism, not a fallback. The\n * proxy's own deny-without-asking path then runs unchanged, so ADR 0005's headless, print,\n * JSON, and RPC behaviour holds by construction rather than by a mode check that a later\n * change could forget to make.\n */\nexport interface TerminalPromptOptions {\n\thandoff: TerminalHandoff;\n\tinput?: NodeJS.ReadStream;\n\toutput?: NodeJS.WriteStream;\n}\n\n/**\n * Build a yes/no prompt the supervisor owns, or nothing at all when there is no terminal.\n *\n * Both sandbox escalations ask the same shape of question and must fail closed the same\n * way, so they share one implementation. Returning undefined rather than a\n * function-that-says-no is deliberate: the caller's own refuse path then runs untouched,\n * which is how ADR 0005's headless behaviour stays correct without a mode check.\n */\nfunction createTerminalPrompt(\n\toptions: TerminalPromptOptions,\n\tcompose: (subject: string) => { question: string; granted: string; refused: string },\n): ((subject: string) => Promise<boolean>) | undefined {\n\tconst input = options.input ?? process.stdin;\n\tconst output = options.output ?? process.stdout;\n\tif (!input.isTTY || !output.isTTY) return undefined;\n\n\tfunction readAnswer(): Promise<string> {\n\t\treturn new Promise((resolve) => {\n\t\t\tlet buffer = \"\";\n\t\t\tconst finish = (value: string) => {\n\t\t\t\tinput.off(\"data\", onData);\n\t\t\t\tinput.off(\"error\", onError);\n\t\t\t\tinput.off(\"end\", onEnd);\n\t\t\t\t// The child had stdin in raw mode and takes it back on resume, so the\n\t\t\t\t// supervisor leaves the stream paused rather than owning it past the answer.\n\t\t\t\tinput.pause();\n\t\t\t\tresolve(value);\n\t\t\t};\n\t\t\tconst onData = (chunk: Buffer | string) => {\n\t\t\t\tbuffer += chunk.toString();\n\t\t\t\tconst newline = buffer.indexOf(\"\\n\");\n\t\t\t\tif (newline >= 0) finish(buffer.slice(0, newline));\n\t\t\t};\n\t\t\t// Anything unreadable declines. Treating an unanswerable prompt as consent would\n\t\t\t// hand the grant to whatever broke the terminal.\n\t\t\tconst onError = () => finish(\"\");\n\t\t\tconst onEnd = () => finish(buffer);\n\t\t\tinput.on(\"data\", onData);\n\t\t\tinput.on(\"error\", onError);\n\t\t\tinput.on(\"end\", onEnd);\n\t\t\tinput.resume();\n\t\t});\n\t}\n\n\treturn (subject: string): Promise<boolean> =>\n\t\toptions.handoff.borrowTerminal(async () => {\n\t\t\tconst text = compose(subject);\n\t\t\toutput.write(`\\n${text.question}`);\n\t\t\tconst answer = (await readAnswer()).trim().toLowerCase();\n\t\t\tconst approved = answer === \"y\" || answer === \"yes\";\n\t\t\toutput.write(`${approved ? text.granted : text.refused}\\n\\n`);\n\t\t\treturn approved;\n\t\t});\n}\n\n/** Approve one refused host for the rest of the session. */\nexport function createHostApprover(\n\toptions: TerminalPromptOptions,\n): ((hostname: string, port: number) => Promise<boolean>) | undefined {\n\tconst prompt = createTerminalPrompt(options, (subject) => ({\n\t\tquestion:\n\t\t\t`The sandboxed session asked to reach ${subject}, which is not on the network allowlist.\\n` +\n\t\t\t`Allow it for this session only? [y/N] `,\n\t\tgranted: `Allowed ${subject} for this session. Add it to \"network.allowedHosts\" in your global settings to make it permanent.`,\n\t\trefused: `Refused ${subject}.`,\n\t}));\n\tif (!prompt) return undefined;\n\treturn (hostname: string, port: number) => prompt(`${hostname}:${port}`);\n}\n\n/**\n * Release the host's own git credential for one host, for the rest of the session.\n *\n * Separate from the host approval above even though both are yes/no, because they are\n * different grants and conflating them would let approving a push also release a token to\n * everything else the session later contacts.\n */\nexport function createCredentialReleaser(\n\toptions: TerminalPromptOptions,\n): ((host: string) => Promise<boolean>) | undefined {\n\treturn createTerminalPrompt(options, (subject) => ({\n\t\tquestion:\n\t\t\t`The sandboxed session asked for your git credential for ${subject}.\\n` +\n\t\t\t`It is read from your host credential store and never written into the workspace.\\n` +\n\t\t\t`Release it for this session only? [y/N] `,\n\t\tgranted: `Released the ${subject} credential for this session.`,\n\t\trefused: `Refused the ${subject} credential.`,\n\t}));\n}\n\n/**\n * Approve one command to run in a separate child with one extra writable root.\n *\n * Unlike a host or a credential, nothing is remembered. A command is not a stable subject\n * to grant against, and a remembered approval would silently cover the next command that\n * happened to name the same root.\n */\nexport function createCommandEscalationApprover(\n\toptions: TerminalPromptOptions,\n): ((request: { command: string; writableRoot: string }) => Promise<boolean>) | undefined {\n\tconst prompt = createTerminalPrompt(options, (subject) => ({\n\t\tquestion:\n\t\t\t`The sandboxed session asked to run a command with write access outside its workspace.\\n\\n` +\n\t\t\t` ${subject}\\n\\n` +\n\t\t\t`It runs once, in a separate sandbox. The session's own boundary does not change.\\n` +\n\t\t\t`Allow it? [y/N] `,\n\t\tgranted: \"Ran the command once with that root writable.\",\n\t\trefused: \"Refused.\",\n\t}));\n\tif (!prompt) return undefined;\n\treturn (request) => prompt(`${request.command}\\n (writable: ${request.writableRoot})`);\n}\n"]}
|