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
|
@@ -2,6 +2,57 @@ import { existsSync, unlinkSync } from "node:fs";
|
|
|
2
2
|
import * as http from "node:http";
|
|
3
3
|
import * as net from "node:net";
|
|
4
4
|
export function createSandboxNetworkProxy(options) {
|
|
5
|
+
/** Hosts a human permitted this session. Never persisted; a durable entry is a settings edit. */
|
|
6
|
+
const granted = new Set();
|
|
7
|
+
/**
|
|
8
|
+
* Approvals currently awaiting an answer, keyed the same way a grant is.
|
|
9
|
+
*
|
|
10
|
+
* A real prompt takes as long as a human takes to read it, and a refused host usually
|
|
11
|
+
* arrives as several near-simultaneous connections. Without this, each one raises its
|
|
12
|
+
* own prompt for the same question.
|
|
13
|
+
*/
|
|
14
|
+
const pending = new Map();
|
|
15
|
+
function isHostReachable(hostname) {
|
|
16
|
+
if (options.allowedHosts.some((entry) => entry === hostname || entry.startsWith(`${hostname}:`)))
|
|
17
|
+
return true;
|
|
18
|
+
for (const grant of granted) {
|
|
19
|
+
if (grant === hostname || grant.startsWith(`${hostname}:`))
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
function isAllowed(hostname, port) {
|
|
25
|
+
return (options.allowedHosts.includes(hostname) ||
|
|
26
|
+
options.allowedHosts.includes(`${hostname}:${port}`) ||
|
|
27
|
+
granted.has(`${hostname}:${port}`));
|
|
28
|
+
}
|
|
29
|
+
async function approve(hostname, port) {
|
|
30
|
+
const key = `${hostname}:${port}`;
|
|
31
|
+
if (granted.has(key))
|
|
32
|
+
return true;
|
|
33
|
+
const inFlight = pending.get(key);
|
|
34
|
+
if (inFlight)
|
|
35
|
+
return inFlight;
|
|
36
|
+
// Declining is deliberately not remembered. A second attempt asks again, because a
|
|
37
|
+
// cached "no" would be indistinguishable to the user from the boundary having
|
|
38
|
+
// silently stopped asking.
|
|
39
|
+
const question = (async () => {
|
|
40
|
+
try {
|
|
41
|
+
const approved = await options.requestApproval(hostname, port);
|
|
42
|
+
if (approved)
|
|
43
|
+
granted.add(key);
|
|
44
|
+
return approved;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
pending.delete(key);
|
|
51
|
+
}
|
|
52
|
+
})();
|
|
53
|
+
pending.set(key, question);
|
|
54
|
+
return question;
|
|
55
|
+
}
|
|
5
56
|
return new Promise((resolve, reject) => {
|
|
6
57
|
const server = http.createServer((_req, res) => {
|
|
7
58
|
res.writeHead(405, { "Content-Type": "text/plain" });
|
|
@@ -15,33 +66,54 @@ export function createSandboxNetworkProxy(options) {
|
|
|
15
66
|
}
|
|
16
67
|
const hostname = match[1];
|
|
17
68
|
const port = Number.parseInt(match[2], 10);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
69
|
+
function refuse() {
|
|
70
|
+
if (!clientSocket.destroyed) {
|
|
71
|
+
clientSocket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
72
|
+
clientSocket.destroy();
|
|
73
|
+
}
|
|
22
74
|
options.violationStore?.add({
|
|
23
75
|
kind: "network",
|
|
24
76
|
command: `CONNECT ${hostname}:${port}`,
|
|
25
77
|
detail: `Host ${hostname} refused by allowlist policy.`,
|
|
26
78
|
timestamp: new Date(),
|
|
27
79
|
});
|
|
28
|
-
return;
|
|
29
80
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
clientSocket.pipe(serverSocket);
|
|
35
|
-
});
|
|
36
|
-
serverSocket.on("error", () => {
|
|
37
|
-
if (!clientSocket.destroyed) {
|
|
38
|
-
clientSocket.write(`HTTP/1.1 502 Bad Gateway\r\n\r\n`);
|
|
39
|
-
clientSocket.destroy();
|
|
81
|
+
if (!isAllowed(hostname, port)) {
|
|
82
|
+
if (!options.requestApproval) {
|
|
83
|
+
refuse();
|
|
84
|
+
return;
|
|
40
85
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
86
|
+
// The client is left waiting on its CONNECT while the human reads the
|
|
87
|
+
// prompt, which is what makes an approved host resume the original request
|
|
88
|
+
// instead of surfacing as a failure the caller has to retry.
|
|
89
|
+
void approve(hostname, port).then((approved) => {
|
|
90
|
+
if (approved)
|
|
91
|
+
tunnel();
|
|
92
|
+
else
|
|
93
|
+
refuse();
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
tunnel();
|
|
98
|
+
function tunnel() {
|
|
99
|
+
if (clientSocket.destroyed)
|
|
100
|
+
return;
|
|
101
|
+
const serverSocket = net.connect(Number(port) || 443, hostname, () => {
|
|
102
|
+
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
103
|
+
serverSocket.write(head);
|
|
104
|
+
serverSocket.pipe(clientSocket);
|
|
105
|
+
clientSocket.pipe(serverSocket);
|
|
106
|
+
});
|
|
107
|
+
serverSocket.on("error", () => {
|
|
108
|
+
if (!clientSocket.destroyed) {
|
|
109
|
+
clientSocket.write(`HTTP/1.1 502 Bad Gateway\r\n\r\n`);
|
|
110
|
+
clientSocket.destroy();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
clientSocket.on("error", () => {
|
|
114
|
+
serverSocket.destroy();
|
|
115
|
+
});
|
|
116
|
+
}
|
|
45
117
|
});
|
|
46
118
|
server.on("error", reject);
|
|
47
119
|
if (options.tcpHost !== undefined) {
|
|
@@ -56,6 +128,7 @@ export function createSandboxNetworkProxy(options) {
|
|
|
56
128
|
resolve({
|
|
57
129
|
server,
|
|
58
130
|
port,
|
|
131
|
+
isHostReachable,
|
|
59
132
|
close: () => new Promise((r) => server.close(() => r())),
|
|
60
133
|
});
|
|
61
134
|
});
|
|
@@ -71,6 +144,7 @@ export function createSandboxNetworkProxy(options) {
|
|
|
71
144
|
server.off("error", reject);
|
|
72
145
|
resolve({
|
|
73
146
|
server,
|
|
147
|
+
isHostReachable,
|
|
74
148
|
close: () => new Promise((r) => server.close(() => r())),
|
|
75
149
|
});
|
|
76
150
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"network-proxy.js","sourceRoot":"","sources":["../../../src/core/sandbox/network-proxy.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, unlinkSync } from \"node:fs\";\nimport * as http from \"node:http\";\nimport * as net from \"node:net\";\nimport type { SandboxViolationStore } from \"./violations.ts\";\n\nexport interface SandboxNetworkProxy {\n\treadonly server: http.Server;\n\t/** Set only in TCP-listen mode (`tcpHost`); the ephemeral port the proxy bound. */\n\treadonly port?: number;\n\tclose(): Promise<void>;\n}\n\nexport function createSandboxNetworkProxy(\n\toptions: {\n\t\tallowedHosts: readonly string[];\n\t\tviolationStore?: SandboxViolationStore;\n\t} & ({ socketPath: string; tcpHost?: undefined } | { socketPath?: undefined; tcpHost: string }),\n): Promise<SandboxNetworkProxy> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst server = http.createServer((_req, res) => {\n\t\t\tres.writeHead(405, { \"Content-Type\": \"text/plain\" });\n\t\t\tres.end(\"Method Not Allowed\\n\");\n\t\t});\n\n\t\tserver.on(\"connect\", (req, clientSocket, head) => {\n\t\t\tconst match = req.url?.match(/^([^:]+):(\\d+)$/);\n\t\t\tif (!match) {\n\t\t\t\tclientSocket.destroy();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst hostname = match[1];\n\t\t\tconst port = Number.parseInt(match[2], 10);\n\n\t\t\tconst isAllowed =\n\t\t\t\toptions.allowedHosts.includes(hostname) || options.allowedHosts.includes(`${hostname}:${port}`);\n\t\t\tif (!isAllowed) {\n\t\t\t\tclientSocket.write(\"HTTP/1.1 403 Forbidden\\r\\n\\r\\n\");\n\t\t\t\tclientSocket.destroy();\n\t\t\t\toptions.violationStore?.add({\n\t\t\t\t\tkind: \"network\",\n\t\t\t\t\tcommand: `CONNECT ${hostname}:${port}`,\n\t\t\t\t\tdetail: `Host ${hostname} refused by allowlist policy.`,\n\t\t\t\t\ttimestamp: new Date(),\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst serverSocket = net.connect(Number(port) || 443, hostname, () => {\n\t\t\t\tclientSocket.write(\"HTTP/1.1 200 Connection Established\\r\\n\\r\\n\");\n\t\t\t\tserverSocket.write(head);\n\t\t\t\tserverSocket.pipe(clientSocket);\n\t\t\t\tclientSocket.pipe(serverSocket);\n\t\t\t});\n\n\t\t\tserverSocket.on(\"error\", () => {\n\t\t\t\tif (!clientSocket.destroyed) {\n\t\t\t\t\tclientSocket.write(`HTTP/1.1 502 Bad Gateway\\r\\n\\r\\n`);\n\t\t\t\t\tclientSocket.destroy();\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tclientSocket.on(\"error\", () => {\n\t\t\t\tserverSocket.destroy();\n\t\t\t});\n\t\t});\n\n\t\tserver.on(\"error\", reject);\n\n\t\tif (options.tcpHost !== undefined) {\n\t\t\t// macOS's Seatbelt can permit a specific localhost port directly, so the\n\t\t\t// proxy listens on loopback TCP there instead of a Unix domain socket --\n\t\t\t// no in-child relay process is needed the way Linux's `--unshare-net`\n\t\t\t// requires one.\n\t\t\tserver.listen(0, options.tcpHost, () => {\n\t\t\t\tserver.off(\"error\", reject);\n\t\t\t\tconst address = server.address();\n\t\t\t\tconst port = typeof address === \"object\" && address !== null ? address.port : undefined;\n\t\t\t\tresolve({\n\t\t\t\t\tserver,\n\t\t\t\t\tport,\n\t\t\t\t\tclose: () => new Promise((r) => server.close(() => r())),\n\t\t\t\t});\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\t// A prior sandboxed launch that never called close() (a crash, a SIGKILL)\n\t\t// leaves a stale socket file behind; Node's listen() refuses to bind over\n\t\t// one even though nothing is listening on it.\n\t\tif (existsSync(options.socketPath)) {\n\t\t\tunlinkSync(options.socketPath);\n\t\t}\n\n\t\tserver.listen(options.socketPath, () => {\n\t\t\tserver.off(\"error\", reject);\n\t\t\tresolve({\n\t\t\t\tserver,\n\t\t\t\tclose: () => new Promise((r) => server.close(() => r())),\n\t\t\t});\n\t\t});\n\t});\n}\n"]}
|
|
1
|
+
{"version":3,"file":"network-proxy.js","sourceRoot":"","sources":["../../../src/core/sandbox/network-proxy.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, unlinkSync } from \"node:fs\";\nimport * as http from \"node:http\";\nimport * as net from \"node:net\";\nimport type { SandboxViolationStore } from \"./violations.ts\";\n\nexport interface SandboxNetworkProxy {\n\treadonly server: http.Server;\n\t/** Set only in TCP-listen mode (`tcpHost`); the ephemeral port the proxy bound. */\n\treadonly port?: number;\n\t/**\n\t * Whether this session may reach `hostname` on any port, by configuration or by a\n\t * grant the human made this session.\n\t *\n\t * Exposed so the git credential channel can ask the one component that actually knows,\n\t * rather than keeping a second copy of the allowlist that would drift from this one the\n\t * first time a host is approved at runtime.\n\t */\n\tisHostReachable(hostname: string): boolean;\n\tclose(): Promise<void>;\n}\n\nexport function createSandboxNetworkProxy(\n\toptions: {\n\t\tallowedHosts: readonly string[];\n\t\tviolationStore?: SandboxViolationStore;\n\t\t/**\n\t\t * Ask a human whether to permit one refused host for the rest of this session.\n\t\t *\n\t\t * Supplied by the supervisor and never by the child: per ADR 0023, an approval\n\t\t * asserted from inside the boundary would be indistinguishable from one forged by\n\t\t * the code the boundary exists to contain. Absent means deny without asking, which\n\t\t * is exactly today's behaviour and what ADR 0005 requires of headless, print,\n\t\t * JSON, and RPC modes.\n\t\t */\n\t\trequestApproval?: (hostname: string, port: number) => Promise<boolean>;\n\t} & ({ socketPath: string; tcpHost?: undefined } | { socketPath?: undefined; tcpHost: string }),\n): Promise<SandboxNetworkProxy> {\n\t/** Hosts a human permitted this session. Never persisted; a durable entry is a settings edit. */\n\tconst granted = new Set<string>();\n\t/**\n\t * Approvals currently awaiting an answer, keyed the same way a grant is.\n\t *\n\t * A real prompt takes as long as a human takes to read it, and a refused host usually\n\t * arrives as several near-simultaneous connections. Without this, each one raises its\n\t * own prompt for the same question.\n\t */\n\tconst pending = new Map<string, Promise<boolean>>();\n\n\tfunction isHostReachable(hostname: string): boolean {\n\t\tif (options.allowedHosts.some((entry) => entry === hostname || entry.startsWith(`${hostname}:`))) return true;\n\t\tfor (const grant of granted) {\n\t\t\tif (grant === hostname || grant.startsWith(`${hostname}:`)) return true;\n\t\t}\n\t\treturn false;\n\t}\n\n\tfunction isAllowed(hostname: string, port: number): boolean {\n\t\treturn (\n\t\t\toptions.allowedHosts.includes(hostname) ||\n\t\t\toptions.allowedHosts.includes(`${hostname}:${port}`) ||\n\t\t\tgranted.has(`${hostname}:${port}`)\n\t\t);\n\t}\n\n\tasync function approve(hostname: string, port: number): Promise<boolean> {\n\t\tconst key = `${hostname}:${port}`;\n\t\tif (granted.has(key)) return true;\n\t\tconst inFlight = pending.get(key);\n\t\tif (inFlight) return inFlight;\n\t\t// Declining is deliberately not remembered. A second attempt asks again, because a\n\t\t// cached \"no\" would be indistinguishable to the user from the boundary having\n\t\t// silently stopped asking.\n\t\tconst question = (async () => {\n\t\t\ttry {\n\t\t\t\tconst approved = await (options.requestApproval as (h: string, p: number) => Promise<boolean>)(\n\t\t\t\t\thostname,\n\t\t\t\t\tport,\n\t\t\t\t);\n\t\t\t\tif (approved) granted.add(key);\n\t\t\t\treturn approved;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t} finally {\n\t\t\t\tpending.delete(key);\n\t\t\t}\n\t\t})();\n\t\tpending.set(key, question);\n\t\treturn question;\n\t}\n\n\treturn new Promise((resolve, reject) => {\n\t\tconst server = http.createServer((_req, res) => {\n\t\t\tres.writeHead(405, { \"Content-Type\": \"text/plain\" });\n\t\t\tres.end(\"Method Not Allowed\\n\");\n\t\t});\n\n\t\tserver.on(\"connect\", (req, clientSocket, head) => {\n\t\t\tconst match = req.url?.match(/^([^:]+):(\\d+)$/);\n\t\t\tif (!match) {\n\t\t\t\tclientSocket.destroy();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst hostname = match[1];\n\t\t\tconst port = Number.parseInt(match[2], 10);\n\n\t\t\tfunction refuse(): void {\n\t\t\t\tif (!clientSocket.destroyed) {\n\t\t\t\t\tclientSocket.write(\"HTTP/1.1 403 Forbidden\\r\\n\\r\\n\");\n\t\t\t\t\tclientSocket.destroy();\n\t\t\t\t}\n\t\t\t\toptions.violationStore?.add({\n\t\t\t\t\tkind: \"network\",\n\t\t\t\t\tcommand: `CONNECT ${hostname}:${port}`,\n\t\t\t\t\tdetail: `Host ${hostname} refused by allowlist policy.`,\n\t\t\t\t\ttimestamp: new Date(),\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (!isAllowed(hostname, port)) {\n\t\t\t\tif (!options.requestApproval) {\n\t\t\t\t\trefuse();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// The client is left waiting on its CONNECT while the human reads the\n\t\t\t\t// prompt, which is what makes an approved host resume the original request\n\t\t\t\t// instead of surfacing as a failure the caller has to retry.\n\t\t\t\tvoid approve(hostname, port).then((approved) => {\n\t\t\t\t\tif (approved) tunnel();\n\t\t\t\t\telse refuse();\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttunnel();\n\n\t\t\tfunction tunnel(): void {\n\t\t\t\tif (clientSocket.destroyed) return;\n\t\t\t\tconst serverSocket = net.connect(Number(port) || 443, hostname, () => {\n\t\t\t\t\tclientSocket.write(\"HTTP/1.1 200 Connection Established\\r\\n\\r\\n\");\n\t\t\t\t\tserverSocket.write(head);\n\t\t\t\t\tserverSocket.pipe(clientSocket);\n\t\t\t\t\tclientSocket.pipe(serverSocket);\n\t\t\t\t});\n\n\t\t\t\tserverSocket.on(\"error\", () => {\n\t\t\t\t\tif (!clientSocket.destroyed) {\n\t\t\t\t\t\tclientSocket.write(`HTTP/1.1 502 Bad Gateway\\r\\n\\r\\n`);\n\t\t\t\t\t\tclientSocket.destroy();\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t\tclientSocket.on(\"error\", () => {\n\t\t\t\t\tserverSocket.destroy();\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\n\t\tserver.on(\"error\", reject);\n\n\t\tif (options.tcpHost !== undefined) {\n\t\t\t// macOS's Seatbelt can permit a specific localhost port directly, so the\n\t\t\t// proxy listens on loopback TCP there instead of a Unix domain socket --\n\t\t\t// no in-child relay process is needed the way Linux's `--unshare-net`\n\t\t\t// requires one.\n\t\t\tserver.listen(0, options.tcpHost, () => {\n\t\t\t\tserver.off(\"error\", reject);\n\t\t\t\tconst address = server.address();\n\t\t\t\tconst port = typeof address === \"object\" && address !== null ? address.port : undefined;\n\t\t\t\tresolve({\n\t\t\t\t\tserver,\n\t\t\t\t\tport,\n\t\t\t\t\tisHostReachable,\n\t\t\t\t\tclose: () => new Promise((r) => server.close(() => r())),\n\t\t\t\t});\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\t// A prior sandboxed launch that never called close() (a crash, a SIGKILL)\n\t\t// leaves a stale socket file behind; Node's listen() refuses to bind over\n\t\t// one even though nothing is listening on it.\n\t\tif (existsSync(options.socketPath)) {\n\t\t\tunlinkSync(options.socketPath);\n\t\t}\n\n\t\tserver.listen(options.socketPath, () => {\n\t\t\tserver.off(\"error\", reject);\n\t\t\tresolve({\n\t\t\t\tserver,\n\t\t\t\tisHostReachable,\n\t\t\t\tclose: () => new Promise((r) => server.close(() => r())),\n\t\t\t});\n\t\t});\n\t});\n}\n"]}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
export interface SandboxPolicy {
|
|
2
|
+
/**
|
|
3
|
+
* The one directory the session is anchored to. State, sessions, and the concurrency
|
|
4
|
+
* lease all live under it, and the child starts here, so it stays singular even when
|
|
5
|
+
* more roots are writable.
|
|
6
|
+
*/
|
|
2
7
|
readonly workspace: string;
|
|
3
8
|
readonly allowedHosts: readonly string[];
|
|
9
|
+
/** Further writable directories, named on the command line and validated the same way. */
|
|
10
|
+
readonly additionalWritableRoots: readonly string[];
|
|
4
11
|
}
|
|
5
12
|
export type SandboxPolicyResult = {
|
|
6
13
|
readonly kind: "valid";
|
|
@@ -26,6 +33,7 @@ export declare class SandboxUnavailableError extends Error {
|
|
|
26
33
|
export declare function createSandboxPolicy(options: {
|
|
27
34
|
workspace: string;
|
|
28
35
|
allowedHosts?: readonly string[];
|
|
36
|
+
additionalWritableRoots?: readonly string[];
|
|
29
37
|
}): SandboxPolicyResult;
|
|
30
38
|
/** Throws rather than returning an executable fallback when OS enforcement is absent. */
|
|
31
39
|
export declare function requireSandboxEnforcement(status: SandboxStatus): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/policy.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/policy.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,aAAa;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,0FAA0F;IAC1F,QAAQ,CAAC,uBAAuB,EAAE,SAAS,MAAM,EAAE,CAAC;CACpD;AAED,MAAM,MAAM,mBAAmB,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAA;CAAE,GAC1D;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD,MAAM,MAAM,aAAa,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtH,qBAAa,uBAAwB,SAAQ,KAAK;IACjD,YAAY,MAAM,EAAE,MAAM,EAGzB;CACD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,uBAAuB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5C,GAAG,mBAAmB,CAetB;AAsBD,yFAAyF;AACzF,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAGrE","sourcesContent":["import { existsSync, realpathSync, statSync } from \"node:fs\";\nimport { isAbsolute } from \"node:path\";\n\nexport interface SandboxPolicy {\n\t/**\n\t * The one directory the session is anchored to. State, sessions, and the concurrency\n\t * lease all live under it, and the child starts here, so it stays singular even when\n\t * more roots are writable.\n\t */\n\treadonly workspace: string;\n\treadonly allowedHosts: readonly string[];\n\t/** Further writable directories, named on the command line and validated the same way. */\n\treadonly additionalWritableRoots: readonly string[];\n}\n\nexport type SandboxPolicyResult =\n\t| { readonly kind: \"valid\"; readonly policy: SandboxPolicy }\n\t| { readonly kind: \"invalid\"; readonly reason: string };\n\nexport type SandboxStatus = { readonly kind: \"enforced\" } | { readonly kind: \"unavailable\"; readonly reason: string };\n\nexport class SandboxUnavailableError extends Error {\n\tconstructor(reason: string) {\n\t\tsuper(`OS sandbox is not enforcing this agent session: ${reason}`);\n\t\tthis.name = \"SandboxUnavailableError\";\n\t}\n}\n\n/**\n * Validate the single writable boundary before any platform backend runs. We accept\n * no relative, missing, or non-directory workspace because a backend must never\n * silently broaden an ambiguous path into host authority.\n */\nexport function createSandboxPolicy(options: {\n\tworkspace: string;\n\tallowedHosts?: readonly string[];\n\tadditionalWritableRoots?: readonly string[];\n}): SandboxPolicyResult {\n\tconst workspace = validateRoot(options.workspace, \"workspace\");\n\tif (typeof workspace !== \"string\") return workspace;\n\n\tconst additionalWritableRoots: string[] = [];\n\tfor (const candidate of options.additionalWritableRoots ?? []) {\n\t\tconst resolved = validateRoot(candidate, \"writable root\");\n\t\tif (typeof resolved !== \"string\") return resolved;\n\t\tadditionalWritableRoots.push(resolved);\n\t}\n\n\treturn {\n\t\tkind: \"valid\",\n\t\tpolicy: { workspace, allowedHosts: options.allowedHosts ?? [], additionalWritableRoots },\n\t};\n}\n\n/**\n * Resolve one writable root, or explain why it cannot be one.\n *\n * Every root goes through the same check for the same reason the workspace always did: a\n * backend must never silently broaden an ambiguous path into host authority, and an extra\n * root named on the command line is no less capable of doing that than the first one.\n */\nfunction validateRoot(candidate: string, label: string): string | { kind: \"invalid\"; reason: string } {\n\tif (!isAbsolute(candidate)) {\n\t\treturn { kind: \"invalid\", reason: `Sandbox ${label} must be an absolute path: ${candidate}` };\n\t}\n\tif (!existsSync(candidate)) {\n\t\treturn { kind: \"invalid\", reason: `Sandbox ${label} does not exist: ${candidate}` };\n\t}\n\tif (!statSync(candidate).isDirectory()) {\n\t\treturn { kind: \"invalid\", reason: `Sandbox ${label} is not a directory: ${candidate}` };\n\t}\n\treturn realpathSync(candidate);\n}\n\n/** Throws rather than returning an executable fallback when OS enforcement is absent. */\nexport function requireSandboxEnforcement(status: SandboxStatus): void {\n\tif (status.kind === \"enforced\") return;\n\tthrow new SandboxUnavailableError(status.reason);\n}\n"]}
|
|
@@ -12,23 +12,40 @@ export class SandboxUnavailableError extends Error {
|
|
|
12
12
|
* silently broaden an ambiguous path into host authority.
|
|
13
13
|
*/
|
|
14
14
|
export function createSandboxPolicy(options) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
const workspace = validateRoot(options.workspace, "workspace");
|
|
16
|
+
if (typeof workspace !== "string")
|
|
17
|
+
return workspace;
|
|
18
|
+
const additionalWritableRoots = [];
|
|
19
|
+
for (const candidate of options.additionalWritableRoots ?? []) {
|
|
20
|
+
const resolved = validateRoot(candidate, "writable root");
|
|
21
|
+
if (typeof resolved !== "string")
|
|
22
|
+
return resolved;
|
|
23
|
+
additionalWritableRoots.push(resolved);
|
|
23
24
|
}
|
|
24
25
|
return {
|
|
25
26
|
kind: "valid",
|
|
26
|
-
policy: {
|
|
27
|
-
workspace: realpathSync(options.workspace),
|
|
28
|
-
allowedHosts: options.allowedHosts ?? [],
|
|
29
|
-
},
|
|
27
|
+
policy: { workspace, allowedHosts: options.allowedHosts ?? [], additionalWritableRoots },
|
|
30
28
|
};
|
|
31
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve one writable root, or explain why it cannot be one.
|
|
32
|
+
*
|
|
33
|
+
* Every root goes through the same check for the same reason the workspace always did: a
|
|
34
|
+
* backend must never silently broaden an ambiguous path into host authority, and an extra
|
|
35
|
+
* root named on the command line is no less capable of doing that than the first one.
|
|
36
|
+
*/
|
|
37
|
+
function validateRoot(candidate, label) {
|
|
38
|
+
if (!isAbsolute(candidate)) {
|
|
39
|
+
return { kind: "invalid", reason: `Sandbox ${label} must be an absolute path: ${candidate}` };
|
|
40
|
+
}
|
|
41
|
+
if (!existsSync(candidate)) {
|
|
42
|
+
return { kind: "invalid", reason: `Sandbox ${label} does not exist: ${candidate}` };
|
|
43
|
+
}
|
|
44
|
+
if (!statSync(candidate).isDirectory()) {
|
|
45
|
+
return { kind: "invalid", reason: `Sandbox ${label} is not a directory: ${candidate}` };
|
|
46
|
+
}
|
|
47
|
+
return realpathSync(candidate);
|
|
48
|
+
}
|
|
32
49
|
/** Throws rather than returning an executable fallback when OS enforcement is absent. */
|
|
33
50
|
export function requireSandboxEnforcement(status) {
|
|
34
51
|
if (status.kind === "enforced")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy.js","sourceRoot":"","sources":["../../../src/core/sandbox/policy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"policy.js","sourceRoot":"","sources":["../../../src/core/sandbox/policy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAoBvC,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IACjD,YAAY,MAAc,EAAE;QAC3B,KAAK,CAAC,mDAAmD,MAAM,EAAE,CAAC,CAAC;QACnE,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IAAA,CACtC;CACD;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAInC,EAAuB;IACvB,MAAM,SAAS,GAAG,YAAY,CAAC,OAAO,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;IAC/D,IAAI,OAAO,SAAS,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAEpD,MAAM,uBAAuB,GAAa,EAAE,CAAC;IAC7C,KAAK,MAAM,SAAS,IAAI,OAAO,CAAC,uBAAuB,IAAI,EAAE,EAAE,CAAC;QAC/D,MAAM,QAAQ,GAAG,YAAY,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;QAC1D,IAAI,OAAO,QAAQ,KAAK,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAClD,uBAAuB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACxC,CAAC;IAED,OAAO;QACN,IAAI,EAAE,OAAO;QACb,MAAM,EAAE,EAAE,SAAS,EAAE,YAAY,EAAE,OAAO,CAAC,YAAY,IAAI,EAAE,EAAE,uBAAuB,EAAE;KACxF,CAAC;AAAA,CACF;AAED;;;;;;GAMG;AACH,SAAS,YAAY,CAAC,SAAiB,EAAE,KAAa,EAAgD;IACrG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC5B,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,KAAK,8BAA8B,SAAS,EAAE,EAAE,CAAC;IAC/F,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC5B,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,KAAK,oBAAoB,SAAS,EAAE,EAAE,CAAC;IACrF,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;QACxC,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,KAAK,wBAAwB,SAAS,EAAE,EAAE,CAAC;IACzF,CAAC;IACD,OAAO,YAAY,CAAC,SAAS,CAAC,CAAC;AAAA,CAC/B;AAED,yFAAyF;AACzF,MAAM,UAAU,yBAAyB,CAAC,MAAqB,EAAQ;IACtE,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU;QAAE,OAAO;IACvC,MAAM,IAAI,uBAAuB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AAAA,CACjD","sourcesContent":["import { existsSync, realpathSync, statSync } from \"node:fs\";\nimport { isAbsolute } from \"node:path\";\n\nexport interface SandboxPolicy {\n\t/**\n\t * The one directory the session is anchored to. State, sessions, and the concurrency\n\t * lease all live under it, and the child starts here, so it stays singular even when\n\t * more roots are writable.\n\t */\n\treadonly workspace: string;\n\treadonly allowedHosts: readonly string[];\n\t/** Further writable directories, named on the command line and validated the same way. */\n\treadonly additionalWritableRoots: readonly string[];\n}\n\nexport type SandboxPolicyResult =\n\t| { readonly kind: \"valid\"; readonly policy: SandboxPolicy }\n\t| { readonly kind: \"invalid\"; readonly reason: string };\n\nexport type SandboxStatus = { readonly kind: \"enforced\" } | { readonly kind: \"unavailable\"; readonly reason: string };\n\nexport class SandboxUnavailableError extends Error {\n\tconstructor(reason: string) {\n\t\tsuper(`OS sandbox is not enforcing this agent session: ${reason}`);\n\t\tthis.name = \"SandboxUnavailableError\";\n\t}\n}\n\n/**\n * Validate the single writable boundary before any platform backend runs. We accept\n * no relative, missing, or non-directory workspace because a backend must never\n * silently broaden an ambiguous path into host authority.\n */\nexport function createSandboxPolicy(options: {\n\tworkspace: string;\n\tallowedHosts?: readonly string[];\n\tadditionalWritableRoots?: readonly string[];\n}): SandboxPolicyResult {\n\tconst workspace = validateRoot(options.workspace, \"workspace\");\n\tif (typeof workspace !== \"string\") return workspace;\n\n\tconst additionalWritableRoots: string[] = [];\n\tfor (const candidate of options.additionalWritableRoots ?? []) {\n\t\tconst resolved = validateRoot(candidate, \"writable root\");\n\t\tif (typeof resolved !== \"string\") return resolved;\n\t\tadditionalWritableRoots.push(resolved);\n\t}\n\n\treturn {\n\t\tkind: \"valid\",\n\t\tpolicy: { workspace, allowedHosts: options.allowedHosts ?? [], additionalWritableRoots },\n\t};\n}\n\n/**\n * Resolve one writable root, or explain why it cannot be one.\n *\n * Every root goes through the same check for the same reason the workspace always did: a\n * backend must never silently broaden an ambiguous path into host authority, and an extra\n * root named on the command line is no less capable of doing that than the first one.\n */\nfunction validateRoot(candidate: string, label: string): string | { kind: \"invalid\"; reason: string } {\n\tif (!isAbsolute(candidate)) {\n\t\treturn { kind: \"invalid\", reason: `Sandbox ${label} must be an absolute path: ${candidate}` };\n\t}\n\tif (!existsSync(candidate)) {\n\t\treturn { kind: \"invalid\", reason: `Sandbox ${label} does not exist: ${candidate}` };\n\t}\n\tif (!statSync(candidate).isDirectory()) {\n\t\treturn { kind: \"invalid\", reason: `Sandbox ${label} is not a directory: ${candidate}` };\n\t}\n\treturn realpathSync(candidate);\n}\n\n/** Throws rather than returning an executable fallback when OS enforcement is absent. */\nexport function requireSandboxEnforcement(status: SandboxStatus): void {\n\tif (status.kind === \"enforced\") return;\n\tthrow new SandboxUnavailableError(status.reason);\n}\n"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A named, saved combination of what the OS boundary permits.
|
|
3
|
+
*
|
|
4
|
+
* Apex already has a permission rule model with five modes and eight-source precedence
|
|
5
|
+
* (ADR 0004). That governs the tool gate at `beforeToolCall`, inside the child. This
|
|
6
|
+
* governs the supervisor's launch contract, outside it. They are deliberately separate
|
|
7
|
+
* surfaces, and conflating them is exactly the misreading that makes `bypassPermissions`
|
|
8
|
+
* look like a sandbox escape -- so a profile here carries only boundary inputs and has no
|
|
9
|
+
* way to express a tool-gate mode.
|
|
10
|
+
*
|
|
11
|
+
* It also has no way to express "no boundary". That decision stays on the command line
|
|
12
|
+
* where a human has to type it, per ADR 0005's 2026-08-28 amendment; a profile can widen
|
|
13
|
+
* what is reachable and writable, never remove the boundary that constrains them.
|
|
14
|
+
*
|
|
15
|
+
* Resolved from global settings only, with `projectTrusted: false`, for the reason ADR
|
|
16
|
+
* 0016 gives for every supervisor policy input: a repository that could define the
|
|
17
|
+
* boundary it runs under would be granting itself authority.
|
|
18
|
+
*/
|
|
19
|
+
export interface SandboxProfile {
|
|
20
|
+
readonly allowedHosts: readonly string[];
|
|
21
|
+
readonly additionalWritableRoots: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
/** Resolve one named profile, or undefined when the name is absent or was never given. */
|
|
24
|
+
export declare function resolveSandboxProfile(name: string | undefined, cwd: string, agentDir: string): SandboxProfile | undefined;
|
|
25
|
+
//# sourceMappingURL=profiles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/profiles.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,uBAAuB,EAAE,SAAS,MAAM,EAAE,CAAC;CACpD;AAMD,0FAA0F;AAC1F,wBAAgB,qBAAqB,CACpC,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,GACd,cAAc,GAAG,SAAS,CAc5B","sourcesContent":["import { SettingsManager } from \"../settings-manager.ts\";\n\n/**\n * A named, saved combination of what the OS boundary permits.\n *\n * Apex already has a permission rule model with five modes and eight-source precedence\n * (ADR 0004). That governs the tool gate at `beforeToolCall`, inside the child. This\n * governs the supervisor's launch contract, outside it. They are deliberately separate\n * surfaces, and conflating them is exactly the misreading that makes `bypassPermissions`\n * look like a sandbox escape -- so a profile here carries only boundary inputs and has no\n * way to express a tool-gate mode.\n *\n * It also has no way to express \"no boundary\". That decision stays on the command line\n * where a human has to type it, per ADR 0005's 2026-08-28 amendment; a profile can widen\n * what is reachable and writable, never remove the boundary that constrains them.\n *\n * Resolved from global settings only, with `projectTrusted: false`, for the reason ADR\n * 0016 gives for every supervisor policy input: a repository that could define the\n * boundary it runs under would be granting itself authority.\n */\nexport interface SandboxProfile {\n\treadonly allowedHosts: readonly string[];\n\treadonly additionalWritableRoots: readonly string[];\n}\n\nfunction stringArray(value: unknown): string[] {\n\treturn Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === \"string\") : [];\n}\n\n/** Resolve one named profile, or undefined when the name is absent or was never given. */\nexport function resolveSandboxProfile(\n\tname: string | undefined,\n\tcwd: string,\n\tagentDir: string,\n): SandboxProfile | undefined {\n\tif (!name) return undefined;\n\tconst settings = SettingsManager.create(cwd, agentDir, { projectTrusted: false });\n\tconst profiles = settings.getSandboxProfiles();\n\tconst profile = profiles?.[name];\n\tif (!profile || typeof profile !== \"object\") return undefined;\n\tconst record = profile as Record<string, unknown>;\n\t// Only these two keys are read. An unknown key in a profile is ignored rather than\n\t// rejected, so a profile written for a later version does not break this one, and a\n\t// key this version does not understand can never take effect by accident.\n\treturn {\n\t\tallowedHosts: stringArray(record.allowedHosts),\n\t\tadditionalWritableRoots: stringArray(record.additionalWritableRoots),\n\t};\n}\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { SettingsManager } from "../settings-manager.js";
|
|
2
|
+
function stringArray(value) {
|
|
3
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
4
|
+
}
|
|
5
|
+
/** Resolve one named profile, or undefined when the name is absent or was never given. */
|
|
6
|
+
export function resolveSandboxProfile(name, cwd, agentDir) {
|
|
7
|
+
if (!name)
|
|
8
|
+
return undefined;
|
|
9
|
+
const settings = SettingsManager.create(cwd, agentDir, { projectTrusted: false });
|
|
10
|
+
const profiles = settings.getSandboxProfiles();
|
|
11
|
+
const profile = profiles?.[name];
|
|
12
|
+
if (!profile || typeof profile !== "object")
|
|
13
|
+
return undefined;
|
|
14
|
+
const record = profile;
|
|
15
|
+
// Only these two keys are read. An unknown key in a profile is ignored rather than
|
|
16
|
+
// rejected, so a profile written for a later version does not break this one, and a
|
|
17
|
+
// key this version does not understand can never take effect by accident.
|
|
18
|
+
return {
|
|
19
|
+
allowedHosts: stringArray(record.allowedHosts),
|
|
20
|
+
additionalWritableRoots: stringArray(record.additionalWritableRoots),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=profiles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profiles.js","sourceRoot":"","sources":["../../../src/core/sandbox/profiles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAyBzD,SAAS,WAAW,CAAC,KAAc,EAAY;IAC9C,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAmB,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAAA,CACvG;AAED,0FAA0F;AAC1F,MAAM,UAAU,qBAAqB,CACpC,IAAwB,EACxB,GAAW,EACX,QAAgB,EACa;IAC7B,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5B,MAAM,QAAQ,GAAG,eAAe,CAAC,MAAM,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,CAAC;IAClF,MAAM,QAAQ,GAAG,QAAQ,CAAC,kBAAkB,EAAE,CAAC;IAC/C,MAAM,OAAO,GAAG,QAAQ,EAAE,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC9D,MAAM,MAAM,GAAG,OAAkC,CAAC;IAClD,mFAAmF;IACnF,oFAAoF;IACpF,0EAA0E;IAC1E,OAAO;QACN,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,YAAY,CAAC;QAC9C,uBAAuB,EAAE,WAAW,CAAC,MAAM,CAAC,uBAAuB,CAAC;KACpE,CAAC;AAAA,CACF","sourcesContent":["import { SettingsManager } from \"../settings-manager.ts\";\n\n/**\n * A named, saved combination of what the OS boundary permits.\n *\n * Apex already has a permission rule model with five modes and eight-source precedence\n * (ADR 0004). That governs the tool gate at `beforeToolCall`, inside the child. This\n * governs the supervisor's launch contract, outside it. They are deliberately separate\n * surfaces, and conflating them is exactly the misreading that makes `bypassPermissions`\n * look like a sandbox escape -- so a profile here carries only boundary inputs and has no\n * way to express a tool-gate mode.\n *\n * It also has no way to express \"no boundary\". That decision stays on the command line\n * where a human has to type it, per ADR 0005's 2026-08-28 amendment; a profile can widen\n * what is reachable and writable, never remove the boundary that constrains them.\n *\n * Resolved from global settings only, with `projectTrusted: false`, for the reason ADR\n * 0016 gives for every supervisor policy input: a repository that could define the\n * boundary it runs under would be granting itself authority.\n */\nexport interface SandboxProfile {\n\treadonly allowedHosts: readonly string[];\n\treadonly additionalWritableRoots: readonly string[];\n}\n\nfunction stringArray(value: unknown): string[] {\n\treturn Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === \"string\") : [];\n}\n\n/** Resolve one named profile, or undefined when the name is absent or was never given. */\nexport function resolveSandboxProfile(\n\tname: string | undefined,\n\tcwd: string,\n\tagentDir: string,\n): SandboxProfile | undefined {\n\tif (!name) return undefined;\n\tconst settings = SettingsManager.create(cwd, agentDir, { projectTrusted: false });\n\tconst profiles = settings.getSandboxProfiles();\n\tconst profile = profiles?.[name];\n\tif (!profile || typeof profile !== \"object\") return undefined;\n\tconst record = profile as Record<string, unknown>;\n\t// Only these two keys are read. An unknown key in a profile is ignored rather than\n\t// rejected, so a profile written for a later version does not break this one, and a\n\t// key this version does not understand can never take effect by accident.\n\treturn {\n\t\tallowedHosts: stringArray(record.allowedHosts),\n\t\tadditionalWritableRoots: stringArray(record.additionalWritableRoots),\n\t};\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** The absolute path a sandbox filesystem refusal named, if the output named one. */
|
|
2
|
+
export declare function extractRefusedPath(output: string): string | undefined;
|
|
3
|
+
/** True when the output looks like the boundary refused a write rather than the command failing. */
|
|
4
|
+
export declare function looksLikeSandboxRefusal(output: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* The directory an escalation should make writable for a refused path.
|
|
7
|
+
*
|
|
8
|
+
* The parent, not the file: the command was creating something that does not exist, and a
|
|
9
|
+
* bind of a non-existent path is not a mount bwrap can make.
|
|
10
|
+
*/
|
|
11
|
+
export declare function escalationRootFor(refusedPath: string): string;
|
|
12
|
+
export interface EscalationOutcome {
|
|
13
|
+
readonly code: number;
|
|
14
|
+
readonly stdout: string;
|
|
15
|
+
readonly stderr: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Ask the supervisor to run one command with one extra writable root.
|
|
19
|
+
*
|
|
20
|
+
* Resolves undefined for every refusal and every failure alike, because the caller's
|
|
21
|
+
* behaviour is identical in both cases: report the original error. Only a command that
|
|
22
|
+
* actually ran returns anything.
|
|
23
|
+
*/
|
|
24
|
+
export declare function requestCommandEscalation(request: {
|
|
25
|
+
command: string;
|
|
26
|
+
writableRoot: string;
|
|
27
|
+
}, socketPath?: string | undefined): Promise<EscalationOutcome | undefined>;
|
|
28
|
+
//# sourceMappingURL=command-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-client.d.ts","sourceRoot":"","sources":["../../../../src/core/sandbox/rpc/command-client.ts"],"names":[],"mappings":"AAsBA,qFAAqF;AACrF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOrE;AAED,oGAAoG;AACpG,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACvC,OAAO,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,EAClD,UAAU,qBAAkD,GAC1D,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAgCxC","sourcesContent":["import * as net from \"node:net\";\nimport { dirname, isAbsolute } from \"node:path\";\nimport { COMMAND_ESCALATION_SOCKET_VARIABLE } from \"./command-proxy.ts\";\n\n/**\n * The child's side of per-command escalation.\n *\n * The path this extracts is a guess, and it is allowed to be, because nothing safe rests\n * on it. The supervisor shows the human the exact command and the exact root before\n * anything runs, so a bad guess is refused by someone reading it rather than silently\n * granted. What the guess buys is not having to make the user work out which directory to\n * name; when it finds nothing, no escalation is offered and the command fails as it did\n * before.\n */\n\n/** The wordings the kernel and the shell actually produce for a mount refusal. */\nconst REFUSAL_PATTERNS: readonly RegExp[] = [\n\t/cannot (?:create|touch|remove|open) ['\"]?([^'\":]+)['\"]?: (?:Read-only file system|Permission denied)/i,\n\t/^[^:]*: ?([/][^\\s:]+): (?:Operation not permitted|Permission denied|Read-only file system)/im,\n\t/(?:EROFS|EACCES|EPERM)[^'\"]*['\"]([/][^'\"]+)['\"]/,\n];\n\n/** The absolute path a sandbox filesystem refusal named, if the output named one. */\nexport function extractRefusedPath(output: string): string | undefined {\n\tfor (const pattern of REFUSAL_PATTERNS) {\n\t\tconst match = pattern.exec(output);\n\t\tconst candidate = match?.[1]?.trim();\n\t\tif (candidate && isAbsolute(candidate)) return candidate;\n\t}\n\treturn undefined;\n}\n\n/** True when the output looks like the boundary refused a write rather than the command failing. */\nexport function looksLikeSandboxRefusal(output: string): boolean {\n\treturn /Read-only file system|Operation not permitted|Permission denied/i.test(output);\n}\n\n/**\n * The directory an escalation should make writable for a refused path.\n *\n * The parent, not the file: the command was creating something that does not exist, and a\n * bind of a non-existent path is not a mount bwrap can make.\n */\nexport function escalationRootFor(refusedPath: string): string {\n\treturn dirname(refusedPath);\n}\n\nexport interface EscalationOutcome {\n\treadonly code: number;\n\treadonly stdout: string;\n\treadonly stderr: string;\n}\n\n/**\n * Ask the supervisor to run one command with one extra writable root.\n *\n * Resolves undefined for every refusal and every failure alike, because the caller's\n * behaviour is identical in both cases: report the original error. Only a command that\n * actually ran returns anything.\n */\nexport function requestCommandEscalation(\n\trequest: { command: string; writableRoot: string },\n\tsocketPath = process.env[COMMAND_ESCALATION_SOCKET_VARIABLE],\n): Promise<EscalationOutcome | undefined> {\n\tif (!socketPath) return Promise.resolve(undefined);\n\treturn new Promise((resolve) => {\n\t\tconst socket = net.connect(socketPath);\n\t\tlet buffer = \"\";\n\t\tconst giveUp = () => {\n\t\t\tsocket.destroy();\n\t\t\tresolve(undefined);\n\t\t};\n\t\tsocket.on(\"connect\", () => socket.write(`${JSON.stringify({ op: \"run\", ...request })}\\n`));\n\t\tsocket.on(\"data\", (chunk) => {\n\t\t\tbuffer += chunk.toString();\n\t\t\tconst newline = buffer.indexOf(\"\\n\");\n\t\t\tif (newline < 0) return;\n\t\t\tsocket.destroy();\n\t\t\ttry {\n\t\t\t\tconst response = JSON.parse(buffer.slice(0, newline)) as Record<string, unknown>;\n\t\t\t\tif (response.ok === true && typeof response.code === \"number\") {\n\t\t\t\t\tresolve({\n\t\t\t\t\t\tcode: response.code,\n\t\t\t\t\t\tstdout: typeof response.stdout === \"string\" ? response.stdout : \"\",\n\t\t\t\t\t\tstderr: typeof response.stderr === \"string\" ? response.stderr : \"\",\n\t\t\t\t\t});\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Fall through to the same undefined a refusal produces.\n\t\t\t}\n\t\t\tresolve(undefined);\n\t\t});\n\t\tsocket.on(\"error\", giveUp);\n\t});\n}\n"]}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as net from "node:net";
|
|
2
|
+
import { dirname, isAbsolute } from "node:path";
|
|
3
|
+
import { COMMAND_ESCALATION_SOCKET_VARIABLE } from "./command-proxy.js";
|
|
4
|
+
/**
|
|
5
|
+
* The child's side of per-command escalation.
|
|
6
|
+
*
|
|
7
|
+
* The path this extracts is a guess, and it is allowed to be, because nothing safe rests
|
|
8
|
+
* on it. The supervisor shows the human the exact command and the exact root before
|
|
9
|
+
* anything runs, so a bad guess is refused by someone reading it rather than silently
|
|
10
|
+
* granted. What the guess buys is not having to make the user work out which directory to
|
|
11
|
+
* name; when it finds nothing, no escalation is offered and the command fails as it did
|
|
12
|
+
* before.
|
|
13
|
+
*/
|
|
14
|
+
/** The wordings the kernel and the shell actually produce for a mount refusal. */
|
|
15
|
+
const REFUSAL_PATTERNS = [
|
|
16
|
+
/cannot (?:create|touch|remove|open) ['"]?([^'":]+)['"]?: (?:Read-only file system|Permission denied)/i,
|
|
17
|
+
/^[^:]*: ?([/][^\s:]+): (?:Operation not permitted|Permission denied|Read-only file system)/im,
|
|
18
|
+
/(?:EROFS|EACCES|EPERM)[^'"]*['"]([/][^'"]+)['"]/,
|
|
19
|
+
];
|
|
20
|
+
/** The absolute path a sandbox filesystem refusal named, if the output named one. */
|
|
21
|
+
export function extractRefusedPath(output) {
|
|
22
|
+
for (const pattern of REFUSAL_PATTERNS) {
|
|
23
|
+
const match = pattern.exec(output);
|
|
24
|
+
const candidate = match?.[1]?.trim();
|
|
25
|
+
if (candidate && isAbsolute(candidate))
|
|
26
|
+
return candidate;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
/** True when the output looks like the boundary refused a write rather than the command failing. */
|
|
31
|
+
export function looksLikeSandboxRefusal(output) {
|
|
32
|
+
return /Read-only file system|Operation not permitted|Permission denied/i.test(output);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The directory an escalation should make writable for a refused path.
|
|
36
|
+
*
|
|
37
|
+
* The parent, not the file: the command was creating something that does not exist, and a
|
|
38
|
+
* bind of a non-existent path is not a mount bwrap can make.
|
|
39
|
+
*/
|
|
40
|
+
export function escalationRootFor(refusedPath) {
|
|
41
|
+
return dirname(refusedPath);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Ask the supervisor to run one command with one extra writable root.
|
|
45
|
+
*
|
|
46
|
+
* Resolves undefined for every refusal and every failure alike, because the caller's
|
|
47
|
+
* behaviour is identical in both cases: report the original error. Only a command that
|
|
48
|
+
* actually ran returns anything.
|
|
49
|
+
*/
|
|
50
|
+
export function requestCommandEscalation(request, socketPath = process.env[COMMAND_ESCALATION_SOCKET_VARIABLE]) {
|
|
51
|
+
if (!socketPath)
|
|
52
|
+
return Promise.resolve(undefined);
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
const socket = net.connect(socketPath);
|
|
55
|
+
let buffer = "";
|
|
56
|
+
const giveUp = () => {
|
|
57
|
+
socket.destroy();
|
|
58
|
+
resolve(undefined);
|
|
59
|
+
};
|
|
60
|
+
socket.on("connect", () => socket.write(`${JSON.stringify({ op: "run", ...request })}\n`));
|
|
61
|
+
socket.on("data", (chunk) => {
|
|
62
|
+
buffer += chunk.toString();
|
|
63
|
+
const newline = buffer.indexOf("\n");
|
|
64
|
+
if (newline < 0)
|
|
65
|
+
return;
|
|
66
|
+
socket.destroy();
|
|
67
|
+
try {
|
|
68
|
+
const response = JSON.parse(buffer.slice(0, newline));
|
|
69
|
+
if (response.ok === true && typeof response.code === "number") {
|
|
70
|
+
resolve({
|
|
71
|
+
code: response.code,
|
|
72
|
+
stdout: typeof response.stdout === "string" ? response.stdout : "",
|
|
73
|
+
stderr: typeof response.stderr === "string" ? response.stderr : "",
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Fall through to the same undefined a refusal produces.
|
|
80
|
+
}
|
|
81
|
+
resolve(undefined);
|
|
82
|
+
});
|
|
83
|
+
socket.on("error", giveUp);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=command-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-client.js","sourceRoot":"","sources":["../../../../src/core/sandbox/rpc/command-client.ts"],"names":[],"mappings":"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","sourcesContent":["import * as net from \"node:net\";\nimport { dirname, isAbsolute } from \"node:path\";\nimport { COMMAND_ESCALATION_SOCKET_VARIABLE } from \"./command-proxy.ts\";\n\n/**\n * The child's side of per-command escalation.\n *\n * The path this extracts is a guess, and it is allowed to be, because nothing safe rests\n * on it. The supervisor shows the human the exact command and the exact root before\n * anything runs, so a bad guess is refused by someone reading it rather than silently\n * granted. What the guess buys is not having to make the user work out which directory to\n * name; when it finds nothing, no escalation is offered and the command fails as it did\n * before.\n */\n\n/** The wordings the kernel and the shell actually produce for a mount refusal. */\nconst REFUSAL_PATTERNS: readonly RegExp[] = [\n\t/cannot (?:create|touch|remove|open) ['\"]?([^'\":]+)['\"]?: (?:Read-only file system|Permission denied)/i,\n\t/^[^:]*: ?([/][^\\s:]+): (?:Operation not permitted|Permission denied|Read-only file system)/im,\n\t/(?:EROFS|EACCES|EPERM)[^'\"]*['\"]([/][^'\"]+)['\"]/,\n];\n\n/** The absolute path a sandbox filesystem refusal named, if the output named one. */\nexport function extractRefusedPath(output: string): string | undefined {\n\tfor (const pattern of REFUSAL_PATTERNS) {\n\t\tconst match = pattern.exec(output);\n\t\tconst candidate = match?.[1]?.trim();\n\t\tif (candidate && isAbsolute(candidate)) return candidate;\n\t}\n\treturn undefined;\n}\n\n/** True when the output looks like the boundary refused a write rather than the command failing. */\nexport function looksLikeSandboxRefusal(output: string): boolean {\n\treturn /Read-only file system|Operation not permitted|Permission denied/i.test(output);\n}\n\n/**\n * The directory an escalation should make writable for a refused path.\n *\n * The parent, not the file: the command was creating something that does not exist, and a\n * bind of a non-existent path is not a mount bwrap can make.\n */\nexport function escalationRootFor(refusedPath: string): string {\n\treturn dirname(refusedPath);\n}\n\nexport interface EscalationOutcome {\n\treadonly code: number;\n\treadonly stdout: string;\n\treadonly stderr: string;\n}\n\n/**\n * Ask the supervisor to run one command with one extra writable root.\n *\n * Resolves undefined for every refusal and every failure alike, because the caller's\n * behaviour is identical in both cases: report the original error. Only a command that\n * actually ran returns anything.\n */\nexport function requestCommandEscalation(\n\trequest: { command: string; writableRoot: string },\n\tsocketPath = process.env[COMMAND_ESCALATION_SOCKET_VARIABLE],\n): Promise<EscalationOutcome | undefined> {\n\tif (!socketPath) return Promise.resolve(undefined);\n\treturn new Promise((resolve) => {\n\t\tconst socket = net.connect(socketPath);\n\t\tlet buffer = \"\";\n\t\tconst giveUp = () => {\n\t\t\tsocket.destroy();\n\t\t\tresolve(undefined);\n\t\t};\n\t\tsocket.on(\"connect\", () => socket.write(`${JSON.stringify({ op: \"run\", ...request })}\\n`));\n\t\tsocket.on(\"data\", (chunk) => {\n\t\t\tbuffer += chunk.toString();\n\t\t\tconst newline = buffer.indexOf(\"\\n\");\n\t\t\tif (newline < 0) return;\n\t\t\tsocket.destroy();\n\t\t\ttry {\n\t\t\t\tconst response = JSON.parse(buffer.slice(0, newline)) as Record<string, unknown>;\n\t\t\t\tif (response.ok === true && typeof response.code === \"number\") {\n\t\t\t\t\tresolve({\n\t\t\t\t\t\tcode: response.code,\n\t\t\t\t\t\tstdout: typeof response.stdout === \"string\" ? response.stdout : \"\",\n\t\t\t\t\t\tstderr: typeof response.stderr === \"string\" ? response.stderr : \"\",\n\t\t\t\t\t});\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Fall through to the same undefined a refusal produces.\n\t\t\t}\n\t\t\tresolve(undefined);\n\t\t});\n\t\tsocket.on(\"error\", giveUp);\n\t});\n}\n"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { SandboxViolationStore } from "../violations.ts";
|
|
2
|
+
/** Env var naming the child-side socket. */
|
|
3
|
+
export declare const COMMAND_ESCALATION_SOCKET_VARIABLE = "APEX_COMMAND_ESCALATION_PATH";
|
|
4
|
+
export interface CommandEscalationRequest {
|
|
5
|
+
readonly command: string;
|
|
6
|
+
/** The one directory the command needs writable that it does not already have. */
|
|
7
|
+
readonly writableRoot: string;
|
|
8
|
+
}
|
|
9
|
+
export interface CommandEscalationResult {
|
|
10
|
+
readonly code: number;
|
|
11
|
+
readonly stdout: string;
|
|
12
|
+
readonly stderr: string;
|
|
13
|
+
}
|
|
14
|
+
export interface CommandEscalationChannelPaths {
|
|
15
|
+
readonly hostSocketDirectory: string;
|
|
16
|
+
readonly hostSocketPath: string;
|
|
17
|
+
readonly childSocketPath: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function resolveCommandEscalationChannelPaths(): CommandEscalationChannelPaths;
|
|
20
|
+
/**
|
|
21
|
+
* Run one approved command in a second, differently-mounted child.
|
|
22
|
+
*
|
|
23
|
+
* A refused write is refused by the kernel inside a namespace whose mounts are fixed for
|
|
24
|
+
* its lifetime. There is no in-place equivalent of holding a CONNECT open while a human
|
|
25
|
+
* decides: the syscall has already failed, and nothing inside the namespace can widen it.
|
|
26
|
+
* So the escalation happens where the authority is, outside the boundary, by starting a
|
|
27
|
+
* separate child for that one command.
|
|
28
|
+
*
|
|
29
|
+
* The original child's namespace is never modified, which is what makes this sound rather
|
|
30
|
+
* than a hole. Approving one command grants nothing to the session it came from; the
|
|
31
|
+
* session's own next attempt at the same operation is refused exactly as before.
|
|
32
|
+
*
|
|
33
|
+
* Approval is the supervisor's, per ADR 0023, for the reason that ADR gives: this socket
|
|
34
|
+
* has no peer authentication, so a request arriving on it may have come from anything
|
|
35
|
+
* inside the boundary. Without an approver the channel refuses, which is what keeps
|
|
36
|
+
* headless, print, JSON, and RPC modes at ADR 0005's deny.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createCommandEscalationProxy(options: {
|
|
39
|
+
socketPath: string;
|
|
40
|
+
/** Ask a human. Absent means refuse without asking. */
|
|
41
|
+
requestApproval?: (request: CommandEscalationRequest) => Promise<boolean>;
|
|
42
|
+
/** Start the second child. Supplied by the platform backend, which owns mount shape. */
|
|
43
|
+
runEscalated: (request: CommandEscalationRequest) => Promise<CommandEscalationResult>;
|
|
44
|
+
violationStore?: SandboxViolationStore;
|
|
45
|
+
}): Promise<{
|
|
46
|
+
close(): Promise<void>;
|
|
47
|
+
}>;
|
|
48
|
+
//# sourceMappingURL=command-proxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-proxy.d.ts","sourceRoot":"","sources":["../../../../src/core/sandbox/rpc/command-proxy.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAO9D,4CAA4C;AAC5C,eAAO,MAAM,kCAAkC,iCAAiC,CAAC;AAEjF,MAAM,WAAW,wBAAwB;IACxC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,kFAAkF;IAClF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,uBAAuB;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,6BAA6B;IAC7C,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CACjC;AAED,wBAAgB,oCAAoC,IAAI,6BAA6B,CAWpF;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE;IACrD,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,wBAAwB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1E,wFAAwF;IACxF,YAAY,EAAE,CAAC,OAAO,EAAE,wBAAwB,KAAK,OAAO,CAAC,uBAAuB,CAAC,CAAC;IACtF,cAAc,CAAC,EAAE,qBAAqB,CAAC;CACvC,GAAG,OAAO,CAAC;IAAE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC,CA2EtC","sourcesContent":["import { chmodSync, existsSync, mkdtempSync, unlinkSync } from \"node:fs\";\nimport * as net from \"node:net\";\nimport { join } from \"node:path\";\nimport { supervisorTempDirectory } from \"../supervisor-temp.ts\";\nimport type { SandboxViolationStore } from \"../violations.ts\";\nimport { FrameReader, isRequestObject, writeFrame } from \"./framing.ts\";\n\n/** AF_UNIX `sun_path` is 108 bytes on Linux, including the terminating NUL. */\nconst SUN_PATH_LIMIT = 108;\nconst MAX_CONNECTIONS = 8;\n\n/** Env var naming the child-side socket. */\nexport const COMMAND_ESCALATION_SOCKET_VARIABLE = \"APEX_COMMAND_ESCALATION_PATH\";\n\nexport interface CommandEscalationRequest {\n\treadonly command: string;\n\t/** The one directory the command needs writable that it does not already have. */\n\treadonly writableRoot: string;\n}\n\nexport interface CommandEscalationResult {\n\treadonly code: number;\n\treadonly stdout: string;\n\treadonly stderr: string;\n}\n\nexport interface CommandEscalationChannelPaths {\n\treadonly hostSocketDirectory: string;\n\treadonly hostSocketPath: string;\n\treadonly childSocketPath: string;\n}\n\nexport function resolveCommandEscalationChannelPaths(): CommandEscalationChannelPaths {\n\tconst hostSocketDirectory = mkdtempSync(join(supervisorTempDirectory(), `apex-escalate-${process.pid}-`), {\n\t\tencoding: \"utf8\",\n\t});\n\tchmodSync(hostSocketDirectory, 0o700);\n\tconst hostSocketPath = join(hostSocketDirectory, \"channel.sock\");\n\tif (Buffer.byteLength(hostSocketPath) + 1 > SUN_PATH_LIMIT) {\n\t\tthrow new Error(\"Command escalation channel path exceeds the Unix socket path limit.\");\n\t}\n\tconst childSocketPath = process.platform === \"linux\" ? \"/home/apex-command-escalation.sock\" : hostSocketPath;\n\treturn { hostSocketDirectory, hostSocketPath, childSocketPath };\n}\n\n/**\n * Run one approved command in a second, differently-mounted child.\n *\n * A refused write is refused by the kernel inside a namespace whose mounts are fixed for\n * its lifetime. There is no in-place equivalent of holding a CONNECT open while a human\n * decides: the syscall has already failed, and nothing inside the namespace can widen it.\n * So the escalation happens where the authority is, outside the boundary, by starting a\n * separate child for that one command.\n *\n * The original child's namespace is never modified, which is what makes this sound rather\n * than a hole. Approving one command grants nothing to the session it came from; the\n * session's own next attempt at the same operation is refused exactly as before.\n *\n * Approval is the supervisor's, per ADR 0023, for the reason that ADR gives: this socket\n * has no peer authentication, so a request arriving on it may have come from anything\n * inside the boundary. Without an approver the channel refuses, which is what keeps\n * headless, print, JSON, and RPC modes at ADR 0005's deny.\n */\nexport function createCommandEscalationProxy(options: {\n\tsocketPath: string;\n\t/** Ask a human. Absent means refuse without asking. */\n\trequestApproval?: (request: CommandEscalationRequest) => Promise<boolean>;\n\t/** Start the second child. Supplied by the platform backend, which owns mount shape. */\n\trunEscalated: (request: CommandEscalationRequest) => Promise<CommandEscalationResult>;\n\tviolationStore?: SandboxViolationStore;\n}): Promise<{ close(): Promise<void> }> {\n\tfunction audit(detail: string, command: string): void {\n\t\toptions.violationStore?.add({ kind: \"filesystem\", command, detail, timestamp: new Date() });\n\t}\n\n\tasync function answer(request: unknown): Promise<object> {\n\t\tif (!isRequestObject(request)) return { ok: false, error: \"Malformed request.\" };\n\t\tif (request.op !== \"run\") return { ok: false, error: \"Only command escalation is served here.\" };\n\t\tconst command = typeof request.command === \"string\" ? request.command : \"\";\n\t\tconst writableRoot = typeof request.writableRoot === \"string\" ? request.writableRoot : \"\";\n\t\tif (!command || !writableRoot) return { ok: false, error: \"Request named no command or no root.\" };\n\n\t\t// Every request is asked about individually. There is no session-scoped grant here,\n\t\t// unlike a host or a credential: a command is not a stable subject to remember, and\n\t\t// remembering one would silently cover the next command that named the same root.\n\t\tif (!options.requestApproval) {\n\t\t\taudit(`Refused an escalation with no way to ask: ${command}`, command);\n\t\t\treturn { ok: false, error: \"Escalation is unavailable without a terminal.\" };\n\t\t}\n\t\tlet approved = false;\n\t\ttry {\n\t\t\tapproved = await options.requestApproval({ command, writableRoot });\n\t\t} catch {\n\t\t\tapproved = false;\n\t\t}\n\t\tif (!approved) {\n\t\t\taudit(`Escalation refused for ${writableRoot}`, command);\n\t\t\treturn { ok: false, error: \"Escalation was refused.\" };\n\t\t}\n\t\tconst result = await options.runEscalated({ command, writableRoot });\n\t\taudit(`Escalated with ${writableRoot} writable, exit ${result.code}`, command);\n\t\treturn { ok: true, code: result.code, stdout: result.stdout, stderr: result.stderr };\n\t}\n\n\treturn new Promise((resolve, reject) => {\n\t\tif (existsSync(options.socketPath)) unlinkSync(options.socketPath);\n\t\tconst sockets = new Set<net.Socket>();\n\t\tconst server = net.createServer((socket) => {\n\t\t\tif (sockets.size >= MAX_CONNECTIONS) {\n\t\t\t\tsocket.destroy();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsockets.add(socket);\n\t\t\tsocket.on(\"close\", () => sockets.delete(socket));\n\t\t\tsocket.on(\"error\", () => socket.destroy());\n\t\t\tconst reader = new FrameReader(socket, \"Command escalation channel\", (detail) =>\n\t\t\t\taudit(`Refused: ${detail}`, \"(protocol)\"),\n\t\t\t);\n\t\t\tvoid (async () => {\n\t\t\t\ttry {\n\t\t\t\t\twriteFrame(socket, await answer(await reader.next()));\n\t\t\t\t} catch {\n\t\t\t\t\t// A disconnected or malformed client is the reader's business.\n\t\t\t\t} finally {\n\t\t\t\t\tsocket.end();\n\t\t\t\t}\n\t\t\t})();\n\t\t});\n\t\tserver.on(\"error\", reject);\n\t\tserver.listen(options.socketPath, () => {\n\t\t\tserver.off(\"error\", reject);\n\t\t\ttry {\n\t\t\t\tchmodSync(options.socketPath, 0o600);\n\t\t\t} catch {\n\t\t\t\t// The containing directory is already 0700; this is defence in depth.\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tclose: () =>\n\t\t\t\t\tnew Promise((closed) => {\n\t\t\t\t\t\tfor (const socket of sockets) socket.destroy();\n\t\t\t\t\t\tserver.close(() => closed());\n\t\t\t\t\t}),\n\t\t\t});\n\t\t});\n\t});\n}\n"]}
|