apex-code 0.0.1-alpha.8 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +231 -0
- package/README.md +9 -8
- package/dist/cli/args.d.ts +26 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +72 -10
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/file-processor.d.ts.map +1 -1
- package/dist/cli/file-processor.js +2 -1
- package/dist/cli/file-processor.js.map +1 -1
- package/dist/cli/mcp-command.d.ts +9 -0
- package/dist/cli/mcp-command.d.ts.map +1 -0
- package/dist/cli/mcp-command.js +53 -0
- package/dist/cli/mcp-command.js.map +1 -0
- package/dist/cli/project-trust.d.ts.map +1 -1
- package/dist/cli/project-trust.js +3 -1
- package/dist/cli/project-trust.js.map +1 -1
- package/dist/cli/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +2 -1
- package/dist/cli/startup-ui.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +94 -5
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +3 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +45 -18
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +3 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +1 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +183 -15
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +728 -117
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +6 -7
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +7 -0
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/checkpoints/git-checkpoints.d.ts +39 -0
- package/dist/core/checkpoints/git-checkpoints.d.ts.map +1 -0
- package/dist/core/checkpoints/git-checkpoints.js +251 -0
- package/dist/core/checkpoints/git-checkpoints.js.map +1 -0
- package/dist/core/checkpoints/session-checkpoints.d.ts +23 -0
- package/dist/core/checkpoints/session-checkpoints.d.ts.map +1 -0
- package/dist/core/checkpoints/session-checkpoints.js +36 -0
- package/dist/core/checkpoints/session-checkpoints.js.map +1 -0
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +7 -3
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +9 -3
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +59 -36
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/defaults.d.ts +1 -0
- package/dist/core/defaults.d.ts.map +1 -1
- package/dist/core/defaults.js +9 -0
- package/dist/core/defaults.js.map +1 -1
- package/dist/core/delegation/runtime.d.ts +6 -5
- package/dist/core/delegation/runtime.d.ts.map +1 -1
- package/dist/core/delegation/runtime.js +4 -3
- package/dist/core/delegation/runtime.js.map +1 -1
- package/dist/core/durable-state/sqlite.d.ts.map +1 -1
- package/dist/core/durable-state/sqlite.js +26 -2
- package/dist/core/durable-state/sqlite.js.map +1 -1
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js +5 -2
- package/dist/core/exec.js.map +1 -1
- package/dist/core/extensions/index.d.ts +2 -2
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +123 -50
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +5 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +45 -1
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/source-runtime.d.ts +14 -0
- package/dist/core/extensions/source-runtime.d.ts.map +1 -0
- package/dist/core/extensions/source-runtime.js +56 -0
- package/dist/core/extensions/source-runtime.js.map +1 -0
- package/dist/core/extensions/types.d.ts +60 -11
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js +3 -0
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +1 -1
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +1 -1
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/formatter-lifecycle.d.ts +69 -0
- package/dist/core/formatter-lifecycle.d.ts.map +1 -0
- package/dist/core/formatter-lifecycle.js +360 -0
- package/dist/core/formatter-lifecycle.js.map +1 -0
- package/dist/core/hooks/command-handler.d.ts +19 -0
- package/dist/core/hooks/command-handler.d.ts.map +1 -0
- package/dist/core/hooks/command-handler.js +112 -0
- package/dist/core/hooks/command-handler.js.map +1 -0
- package/dist/core/hooks/http-handler.d.ts +10 -0
- package/dist/core/hooks/http-handler.d.ts.map +1 -0
- package/dist/core/hooks/http-handler.js +36 -0
- package/dist/core/hooks/http-handler.js.map +1 -0
- package/dist/core/hooks/loader.d.ts +29 -0
- package/dist/core/hooks/loader.d.ts.map +1 -0
- package/dist/core/hooks/loader.js +139 -0
- package/dist/core/hooks/loader.js.map +1 -0
- package/dist/core/hooks/runtime.d.ts +13 -0
- package/dist/core/hooks/runtime.d.ts.map +1 -0
- package/dist/core/hooks/runtime.js +63 -0
- package/dist/core/hooks/runtime.js.map +1 -0
- package/dist/core/hooks/types.d.ts +83 -0
- package/dist/core/hooks/types.d.ts.map +1 -0
- package/dist/core/hooks/types.js +24 -0
- package/dist/core/hooks/types.js.map +1 -0
- package/dist/core/keybindings.d.ts +20 -19
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +26 -5
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/mcp/config.d.ts +16 -0
- package/dist/core/mcp/config.d.ts.map +1 -0
- package/dist/core/mcp/config.js +166 -0
- package/dist/core/mcp/config.js.map +1 -0
- package/dist/core/mcp/connector.d.ts +35 -0
- package/dist/core/mcp/connector.d.ts.map +1 -0
- package/dist/core/mcp/connector.js +105 -0
- package/dist/core/mcp/connector.js.map +1 -0
- package/dist/core/mcp/contract.d.ts +19 -0
- package/dist/core/mcp/contract.d.ts.map +1 -0
- package/dist/core/mcp/contract.js +89 -0
- package/dist/core/mcp/contract.js.map +1 -0
- package/dist/core/mcp/mcp-tool.d.ts +32 -0
- package/dist/core/mcp/mcp-tool.d.ts.map +1 -0
- package/dist/core/mcp/mcp-tool.js +171 -0
- package/dist/core/mcp/mcp-tool.js.map +1 -0
- package/dist/core/mcp/metadata-cache.d.ts +29 -0
- package/dist/core/mcp/metadata-cache.d.ts.map +1 -0
- package/dist/core/mcp/metadata-cache.js +99 -0
- package/dist/core/mcp/metadata-cache.js.map +1 -0
- package/dist/core/mcp/oauth/authorize.d.ts +21 -0
- package/dist/core/mcp/oauth/authorize.d.ts.map +1 -0
- package/dist/core/mcp/oauth/authorize.js +32 -0
- package/dist/core/mcp/oauth/authorize.js.map +1 -0
- package/dist/core/mcp/oauth/discover.d.ts +27 -0
- package/dist/core/mcp/oauth/discover.d.ts.map +1 -0
- package/dist/core/mcp/oauth/discover.js +116 -0
- package/dist/core/mcp/oauth/discover.js.map +1 -0
- package/dist/core/mcp/oauth/flow.d.ts +31 -0
- package/dist/core/mcp/oauth/flow.d.ts.map +1 -0
- package/dist/core/mcp/oauth/flow.js +175 -0
- package/dist/core/mcp/oauth/flow.js.map +1 -0
- package/dist/core/mcp/oauth/mcp-token.d.ts +51 -0
- package/dist/core/mcp/oauth/mcp-token.d.ts.map +1 -0
- package/dist/core/mcp/oauth/mcp-token.js +129 -0
- package/dist/core/mcp/oauth/mcp-token.js.map +1 -0
- package/dist/core/mcp/oauth/pkce.d.ts +12 -0
- package/dist/core/mcp/oauth/pkce.d.ts.map +1 -0
- package/dist/core/mcp/oauth/pkce.js +11 -0
- package/dist/core/mcp/oauth/pkce.js.map +1 -0
- package/dist/core/mcp/oauth/register.d.ts +16 -0
- package/dist/core/mcp/oauth/register.d.ts.map +1 -0
- package/dist/core/mcp/oauth/register.js +42 -0
- package/dist/core/mcp/oauth/register.js.map +1 -0
- package/dist/core/mcp/oauth/token-client.d.ts +33 -0
- package/dist/core/mcp/oauth/token-client.d.ts.map +1 -0
- package/dist/core/mcp/oauth/token-client.js +65 -0
- package/dist/core/mcp/oauth/token-client.js.map +1 -0
- package/dist/core/mcp/runtime.d.ts +20 -0
- package/dist/core/mcp/runtime.d.ts.map +1 -0
- package/dist/core/mcp/runtime.js +37 -0
- package/dist/core/mcp/runtime.js.map +1 -0
- package/dist/core/mcp/schema.d.ts +22 -0
- package/dist/core/mcp/schema.d.ts.map +1 -0
- package/dist/core/mcp/schema.js +20 -0
- package/dist/core/mcp/schema.js.map +1 -0
- package/dist/core/mcp/server-manager.d.ts +43 -0
- package/dist/core/mcp/server-manager.d.ts.map +1 -0
- package/dist/core/mcp/server-manager.js +142 -0
- package/dist/core/mcp/server-manager.js.map +1 -0
- package/dist/core/mcp/types.d.ts +100 -0
- package/dist/core/mcp/types.d.ts.map +1 -0
- package/dist/core/mcp/types.js +8 -0
- package/dist/core/mcp/types.js.map +1 -0
- package/dist/core/model-config.d.ts +5 -0
- package/dist/core/model-config.d.ts.map +1 -1
- package/dist/core/model-config.js +3 -1
- package/dist/core/model-config.js.map +1 -1
- package/dist/core/model-resolver.d.ts +1 -0
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +14 -8
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/models-store.d.ts.map +1 -1
- package/dist/core/models-store.js +2 -1
- package/dist/core/models-store.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +22 -14
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/permissions/gate.d.ts.map +1 -1
- package/dist/core/permissions/gate.js +45 -4
- package/dist/core/permissions/gate.js.map +1 -1
- package/dist/core/permissions/operations.d.ts +20 -0
- package/dist/core/permissions/operations.d.ts.map +1 -0
- package/dist/core/permissions/operations.js +9 -0
- package/dist/core/permissions/operations.js.map +1 -0
- package/dist/core/permissions/policy-command.d.ts +42 -0
- package/dist/core/permissions/policy-command.d.ts.map +1 -0
- package/dist/core/permissions/policy-command.js +44 -0
- package/dist/core/permissions/policy-command.js.map +1 -0
- package/dist/core/permissions/responder.d.ts +49 -2
- package/dist/core/permissions/responder.d.ts.map +1 -1
- package/dist/core/permissions/responder.js +20 -8
- package/dist/core/permissions/responder.js.map +1 -1
- package/dist/core/permissions/rules.d.ts.map +1 -1
- package/dist/core/permissions/rules.js +11 -1
- package/dist/core/permissions/rules.js.map +1 -1
- package/dist/core/permissions/store.d.ts +18 -0
- package/dist/core/permissions/store.d.ts.map +1 -1
- package/dist/core/permissions/store.js +89 -8
- package/dist/core/permissions/store.js.map +1 -1
- package/dist/core/pi-manifest.d.ts.map +1 -1
- package/dist/core/pi-manifest.js +2 -1
- package/dist/core/pi-manifest.js.map +1 -1
- package/dist/core/policy-executor.d.ts +47 -0
- package/dist/core/policy-executor.d.ts.map +1 -0
- package/dist/core/policy-executor.js +228 -0
- package/dist/core/policy-executor.js.map +1 -0
- package/dist/core/policy-loader.d.ts +72 -0
- package/dist/core/policy-loader.d.ts.map +1 -0
- package/dist/core/policy-loader.js +195 -0
- package/dist/core/policy-loader.js.map +1 -0
- package/dist/core/project-trust.d.ts +1 -1
- package/dist/core/project-trust.d.ts.map +1 -1
- package/dist/core/project-trust.js.map +1 -1
- package/dist/core/remote-catalog-provider.d.ts.map +1 -1
- package/dist/core/remote-catalog-provider.js +2 -1
- package/dist/core/remote-catalog-provider.js.map +1 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +3 -2
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sandbox/bwrap-arguments.d.ts +41 -0
- package/dist/core/sandbox/bwrap-arguments.d.ts.map +1 -0
- package/dist/core/sandbox/bwrap-arguments.js +136 -0
- package/dist/core/sandbox/bwrap-arguments.js.map +1 -0
- package/dist/core/sandbox/child-entry.d.ts.map +1 -1
- package/dist/core/sandbox/child-entry.js +3 -1
- package/dist/core/sandbox/child-entry.js.map +1 -1
- package/dist/core/sandbox/cli-launch.d.ts +84 -4
- package/dist/core/sandbox/cli-launch.d.ts.map +1 -1
- package/dist/core/sandbox/cli-launch.js +115 -16
- package/dist/core/sandbox/cli-launch.js.map +1 -1
- package/dist/core/sandbox/cli-supervisor.d.ts +2 -0
- package/dist/core/sandbox/cli-supervisor.d.ts.map +1 -1
- package/dist/core/sandbox/cli-supervisor.js +50 -2
- package/dist/core/sandbox/cli-supervisor.js.map +1 -1
- package/dist/core/sandbox/full-access.d.ts +28 -0
- package/dist/core/sandbox/full-access.d.ts.map +1 -0
- package/dist/core/sandbox/full-access.js +63 -0
- package/dist/core/sandbox/full-access.js.map +1 -0
- package/dist/core/sandbox/git-identity.d.ts +57 -0
- package/dist/core/sandbox/git-identity.d.ts.map +1 -0
- package/dist/core/sandbox/git-identity.js +78 -0
- package/dist/core/sandbox/git-identity.js.map +1 -0
- package/dist/core/sandbox/host-approval.d.ts +42 -0
- package/dist/core/sandbox/host-approval.d.ts.map +1 -0
- package/dist/core/sandbox/host-approval.js +99 -0
- package/dist/core/sandbox/host-approval.js.map +1 -0
- package/dist/core/sandbox/linux-backend.d.ts +14 -0
- package/dist/core/sandbox/linux-backend.d.ts.map +1 -1
- package/dist/core/sandbox/linux-backend.js +158 -80
- package/dist/core/sandbox/linux-backend.js.map +1 -1
- package/dist/core/sandbox/macos-backend.d.ts +5 -0
- package/dist/core/sandbox/macos-backend.d.ts.map +1 -1
- package/dist/core/sandbox/macos-backend.js +164 -5
- package/dist/core/sandbox/macos-backend.js.map +1 -1
- package/dist/core/sandbox/network-proxy.d.ts +19 -0
- package/dist/core/sandbox/network-proxy.d.ts.map +1 -1
- package/dist/core/sandbox/network-proxy.js +93 -19
- package/dist/core/sandbox/network-proxy.js.map +1 -1
- package/dist/core/sandbox/policy.d.ts +8 -0
- package/dist/core/sandbox/policy.d.ts.map +1 -1
- package/dist/core/sandbox/policy.js +29 -12
- package/dist/core/sandbox/policy.js.map +1 -1
- package/dist/core/sandbox/profiles.d.ts +25 -0
- package/dist/core/sandbox/profiles.d.ts.map +1 -0
- package/dist/core/sandbox/profiles.js +23 -0
- package/dist/core/sandbox/profiles.js.map +1 -0
- package/dist/core/sandbox/rpc/command-client.d.ts +28 -0
- package/dist/core/sandbox/rpc/command-client.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/command-client.js +86 -0
- package/dist/core/sandbox/rpc/command-client.js.map +1 -0
- package/dist/core/sandbox/rpc/command-proxy.d.ts +48 -0
- package/dist/core/sandbox/rpc/command-proxy.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/command-proxy.js +120 -0
- package/dist/core/sandbox/rpc/command-proxy.js.map +1 -0
- package/dist/core/sandbox/rpc/credential-proxy.d.ts.map +1 -1
- package/dist/core/sandbox/rpc/credential-proxy.js +2 -96
- package/dist/core/sandbox/rpc/credential-proxy.js.map +1 -1
- package/dist/core/sandbox/rpc/framing.d.ts +35 -0
- package/dist/core/sandbox/rpc/framing.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/framing.js +114 -0
- package/dist/core/sandbox/rpc/framing.js.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-helper.d.ts +47 -0
- package/dist/core/sandbox/rpc/git-credential-helper.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-helper.js +222 -0
- package/dist/core/sandbox/rpc/git-credential-helper.js.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.d.ts +66 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.js +209 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.js.map +1 -0
- package/dist/core/sandbox/supervisor-temp.d.ts +18 -0
- package/dist/core/sandbox/supervisor-temp.d.ts.map +1 -0
- package/dist/core/sandbox/supervisor-temp.js +21 -0
- package/dist/core/sandbox/supervisor-temp.js.map +1 -0
- package/dist/core/sandbox/supervisor.d.ts +12 -1
- package/dist/core/sandbox/supervisor.d.ts.map +1 -1
- package/dist/core/sandbox/supervisor.js +15 -1
- package/dist/core/sandbox/supervisor.js.map +1 -1
- package/dist/core/sandbox/terminal-handoff.d.ts +68 -0
- package/dist/core/sandbox/terminal-handoff.d.ts.map +1 -0
- package/dist/core/sandbox/terminal-handoff.js +221 -0
- package/dist/core/sandbox/terminal-handoff.js.map +1 -0
- package/dist/core/sandbox/terminal-size.d.ts +13 -0
- package/dist/core/sandbox/terminal-size.d.ts.map +1 -1
- package/dist/core/sandbox/terminal-size.js +23 -2
- package/dist/core/sandbox/terminal-size.js.map +1 -1
- package/dist/core/sdk.d.ts +24 -2
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +63 -4
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-export.d.ts +4 -0
- package/dist/core/session-export.d.ts.map +1 -0
- package/dist/core/session-export.js +32 -0
- package/dist/core/session-export.js.map +1 -0
- package/dist/core/session-manager.d.ts +13 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +34 -6
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-diagnostics.d.ts +9 -0
- package/dist/core/settings-diagnostics.d.ts.map +1 -0
- package/dist/core/settings-diagnostics.js +21 -0
- package/dist/core/settings-diagnostics.js.map +1 -0
- package/dist/core/settings-manager.d.ts +172 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +160 -11
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +51 -33
- package/dist/core/skills.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +4 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +8 -2
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/background-shell.d.ts +39 -0
- package/dist/core/tools/background-shell.d.ts.map +1 -0
- package/dist/core/tools/background-shell.js +81 -0
- package/dist/core/tools/background-shell.js.map +1 -0
- package/dist/core/tools/bash-command-segments.d.ts.map +1 -1
- package/dist/core/tools/bash-command-segments.js +2 -0
- package/dist/core/tools/bash-command-segments.js.map +1 -1
- package/dist/core/tools/bash.d.ts +44 -16
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +264 -32
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/contract-snapshot.d.ts +58 -0
- package/dist/core/tools/contract-snapshot.d.ts.map +1 -0
- package/dist/core/tools/contract-snapshot.js +51 -0
- package/dist/core/tools/contract-snapshot.js.map +1 -0
- package/dist/core/tools/contract.d.ts +53 -5
- package/dist/core/tools/contract.d.ts.map +1 -1
- package/dist/core/tools/contract.js +22 -2
- package/dist/core/tools/contract.js.map +1 -1
- package/dist/core/tools/delegate.d.ts +6 -4
- package/dist/core/tools/delegate.d.ts.map +1 -1
- package/dist/core/tools/delegate.js +21 -11
- package/dist/core/tools/delegate.js.map +1 -1
- package/dist/core/tools/edit-diff.d.ts +48 -5
- package/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/core/tools/edit-diff.js +217 -8
- package/dist/core/tools/edit-diff.js.map +1 -1
- package/dist/core/tools/edit.d.ts +8 -0
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +72 -10
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +5 -2
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +5 -2
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +14 -2
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +12 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +5 -2
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/lsp.d.ts +19 -1
- package/dist/core/tools/lsp.d.ts.map +1 -1
- package/dist/core/tools/lsp.js +163 -10
- package/dist/core/tools/lsp.js.map +1 -1
- package/dist/core/tools/path-permission.d.ts +3 -0
- package/dist/core/tools/path-permission.d.ts.map +1 -1
- package/dist/core/tools/path-permission.js +16 -4
- package/dist/core/tools/path-permission.js.map +1 -1
- package/dist/core/tools/path-utils.d.ts +27 -2
- package/dist/core/tools/path-utils.d.ts.map +1 -1
- package/dist/core/tools/path-utils.js +221 -4
- package/dist/core/tools/path-utils.js.map +1 -1
- package/dist/core/tools/powershell.d.ts +16 -0
- package/dist/core/tools/powershell.d.ts.map +1 -0
- package/dist/core/tools/powershell.js +39 -0
- package/dist/core/tools/powershell.js.map +1 -0
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +20 -11
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/test.d.ts +54 -9
- package/dist/core/tools/test.d.ts.map +1 -1
- package/dist/core/tools/test.js +239 -19
- package/dist/core/tools/test.js.map +1 -1
- package/dist/core/tools/write.d.ts +7 -0
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +64 -9
- package/dist/core/tools/write.js.map +1 -1
- package/dist/core/trust-manager.d.ts.map +1 -1
- package/dist/core/trust-manager.js +2 -1
- package/dist/core/trust-manager.js.map +1 -1
- package/dist/core/verification-lifecycle.d.ts +77 -0
- package/dist/core/verification-lifecycle.d.ts.map +1 -0
- package/dist/core/verification-lifecycle.js +146 -0
- package/dist/core/verification-lifecycle.js.map +1 -0
- package/dist/core/workspace/artifacts.d.ts +45 -0
- package/dist/core/workspace/artifacts.d.ts.map +1 -0
- package/dist/core/workspace/artifacts.js +165 -0
- package/dist/core/workspace/artifacts.js.map +1 -0
- package/dist/core/workspace/comparison.d.ts +27 -0
- package/dist/core/workspace/comparison.d.ts.map +1 -0
- package/dist/core/workspace/comparison.js +94 -0
- package/dist/core/workspace/comparison.js.map +1 -0
- package/dist/core/workspace/git-observer.d.ts +48 -0
- package/dist/core/workspace/git-observer.d.ts.map +1 -0
- package/dist/core/workspace/git-observer.js +346 -0
- package/dist/core/workspace/git-observer.js.map +1 -0
- package/dist/core/workspace/projection.d.ts +3 -0
- package/dist/core/workspace/projection.d.ts.map +1 -0
- package/dist/core/workspace/projection.js +56 -0
- package/dist/core/workspace/projection.js.map +1 -0
- package/dist/core/workspace/state.d.ts +133 -0
- package/dist/core/workspace/state.d.ts.map +1 -0
- package/dist/core/workspace/state.js +96 -0
- package/dist/core/workspace/state.js.map +1 -0
- package/dist/extensions/llama/client.d.ts +6 -0
- package/dist/extensions/llama/client.d.ts.map +1 -1
- package/dist/extensions/llama/client.js +7 -0
- package/dist/extensions/llama/client.js.map +1 -1
- package/dist/extensions/llama/index.d.ts.map +1 -1
- package/dist/extensions/llama/index.js +2 -0
- package/dist/extensions/llama/index.js.map +1 -1
- package/dist/extensions/llama/provider.d.ts +3 -1
- package/dist/extensions/llama/provider.d.ts.map +1 -1
- package/dist/extensions/llama/provider.js +29 -4
- package/dist/extensions/llama/provider.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +6 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +58 -23
- package/dist/main.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +4 -3
- package/dist/migrations.js.map +1 -1
- package/dist/modes/acp/server.d.ts +55 -0
- package/dist/modes/acp/server.d.ts.map +1 -0
- package/dist/modes/acp/server.js +244 -0
- package/dist/modes/acp/server.js.map +1 -0
- package/dist/modes/acp/translate.d.ts +37 -0
- package/dist/modes/acp/translate.d.ts.map +1 -0
- package/dist/modes/acp/translate.js +50 -0
- package/dist/modes/acp/translate.js.map +1 -0
- package/dist/modes/index.d.ts +1 -0
- package/dist/modes/index.d.ts.map +1 -1
- package/dist/modes/index.js +1 -0
- package/dist/modes/index.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +29 -0
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +182 -23
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts +7 -0
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +20 -0
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +9 -4
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +20 -9
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts +7 -1
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +7 -3
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +19 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +70 -10
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +10 -3
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +56 -14
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/permission-preview.d.ts +15 -0
- package/dist/modes/interactive/components/permission-preview.d.ts.map +1 -0
- package/dist/modes/interactive/components/permission-preview.js +33 -0
- package/dist/modes/interactive/components/permission-preview.js.map +1 -0
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +2 -2
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +9 -2
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +128 -56
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-submenu.d.ts +72 -0
- package/dist/modes/interactive/components/settings-submenu.d.ts.map +1 -0
- package/dist/modes/interactive/components/settings-submenu.js +165 -0
- package/dist/modes/interactive/components/settings-submenu.js.map +1 -0
- package/dist/modes/interactive/components/thinking-selector.d.ts +15 -3
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.js +74 -17
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +23 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +64 -10
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/external-editor.d.ts.map +1 -1
- package/dist/modes/interactive/external-editor.js +2 -1
- package/dist/modes/interactive/external-editor.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +45 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +395 -43
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +12 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +20 -9
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/json-event.d.ts +16 -13
- package/dist/modes/json-event.d.ts.map +1 -1
- package/dist/modes/json-event.js +20 -6
- package/dist/modes/json-event.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +27 -12
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +7 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +7 -0
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +3 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +12 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/package-manager-cli.d.ts +1 -0
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +196 -10
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +2 -1
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/management-http.d.ts +5 -3
- package/dist/utils/management-http.d.ts.map +1 -1
- package/dist/utils/management-http.js +14 -10
- package/dist/utils/management-http.js.map +1 -1
- package/dist/utils/shell.d.ts +3 -0
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +23 -9
- package/dist/utils/shell.js.map +1 -1
- package/dist/utils/syntax-highlight.d.ts +1 -0
- package/dist/utils/syntax-highlight.d.ts.map +1 -1
- package/dist/utils/syntax-highlight.js +60 -1
- package/dist/utils/syntax-highlight.js.map +1 -1
- package/dist/utils/text.d.ts +8 -0
- package/dist/utils/text.d.ts.map +1 -0
- package/dist/utils/text.js +9 -0
- package/dist/utils/text.js.map +1 -0
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +6 -4
- package/dist/utils/version-check.js.map +1 -1
- package/docs/compaction.md +19 -0
- package/docs/custom-provider.md +12 -9
- package/docs/environment-variables.md +1 -1
- package/docs/extensions.md +35 -7
- package/docs/json.md +7 -2
- package/docs/keybindings.md +9 -9
- package/docs/llama-cpp.md +2 -0
- package/docs/models.md +8 -2
- package/docs/packages.md +2 -2
- package/docs/quickstart.md +1 -1
- package/docs/rpc.md +32 -3
- package/docs/sdk.md +32 -2
- package/docs/settings.md +23 -7
- package/docs/skills.md +4 -3
- package/docs/terminal-setup.md +23 -7
- package/docs/usage.md +10 -5
- package/docs/windows.md +24 -2
- package/examples/extensions/border-status-editor.ts +1 -1
- package/examples/extensions/built-in-tool-renderer.ts +8 -4
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/git-checkpoint.ts +24 -36
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/minimal-mode.ts +2 -2
- package/examples/extensions/notify.ts +3 -1
- package/examples/extensions/permission-gate.ts +1 -1
- package/examples/extensions/plan-mode/index.ts +1 -1
- package/examples/extensions/rpc-demo.ts +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/subagent/README.md +1 -1
- package/examples/extensions/subagent/index.ts +6 -1
- package/examples/extensions/titlebar-spinner.ts +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/rpc-extension-ui.ts +1 -1
- package/examples/sdk/06-extensions.ts +1 -1
- package/examples/sdk/README.md +1 -1
- package/npm-shrinkwrap.json +439 -405
- package/package.json +17 -17
- package/dist/server/create-harness.d.ts +0 -26
- package/dist/server/create-harness.d.ts.map +0 -1
- package/dist/server/create-harness.js +0 -107
- package/dist/server/create-harness.js.map +0 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { type CommandEscalationRequest } from "./rpc/command-proxy.ts";
|
|
3
|
+
import { fillHostGitCredential } from "./rpc/git-credential-helper.ts";
|
|
2
4
|
import type { SandboxBackend } from "./supervisor.ts";
|
|
3
5
|
import type { SandboxViolationStore } from "./violations.ts";
|
|
4
6
|
export interface LinuxSandboxBackendOptions {
|
|
@@ -8,6 +10,18 @@ export interface LinuxSandboxBackendOptions {
|
|
|
8
10
|
violationStore?: SandboxViolationStore;
|
|
9
11
|
/** Injectable only to prove crash cleanup in tests; production spawns `bwrap` directly. */
|
|
10
12
|
spawnChild?: typeof spawn;
|
|
13
|
+
/**
|
|
14
|
+
* Injectable only for tests, which have no terminal and would otherwise be unable to
|
|
15
|
+
* exercise a released credential at all. Production builds the releaser from the
|
|
16
|
+
* terminal handoff, and its absence there is what makes a headless session refuse.
|
|
17
|
+
*/
|
|
18
|
+
requestGitCredentialRelease?: (host: string) => Promise<boolean>;
|
|
19
|
+
/** Injectable only for tests; production resolves against the real host store. */
|
|
20
|
+
fillGitCredential?: typeof fillHostGitCredential;
|
|
21
|
+
/** Injectable only for tests, which have no terminal to approve an escalation at. */
|
|
22
|
+
requestCommandEscalation?: (request: CommandEscalationRequest) => Promise<boolean>;
|
|
23
|
+
/** Retained for source compatibility; launch state is now required on each launch. */
|
|
24
|
+
supervisorStateDirectory?: string;
|
|
11
25
|
}
|
|
12
26
|
/**
|
|
13
27
|
* Where the supervisor's proxy socket lives on each side of the boundary.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"linux-backend.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/linux-backend.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAa,MAAM,oBAAoB,CAAC;AAMtD,OAAO,KAAK,EAAE,cAAc,EAAiB,MAAM,iBAAiB,CAAC;AAErE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAK7D,MAAM,WAAW,0BAA0B;IAC1C,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,cAAc,CAAC,EAAE,qBAAqB,CAAC;IACvC,2FAA2F;IAC3F,UAAU,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AA6BD;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,kBAAkB,GAAE,MAAiB,GAAG;IAC/E,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;CACxB,CAKA;AAmBD;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE,0BAA0B,GAAG,cAAc,CA4L9F","sourcesContent":["import { spawn, spawnSync } from \"node:child_process\";\nimport { randomBytes } from \"node:crypto\";\nimport { closeSync, mkdirSync, openSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { createSandboxNetworkProxy, type SandboxNetworkProxy } from \"./network-proxy.ts\";\nimport type { SandboxBackend, SandboxLaunch } from \"./supervisor.ts\";\nimport { publishTerminalSize, TERMINAL_SIZE_PATH_VARIABLE } from \"./terminal-size.ts\";\nimport type { SandboxViolationStore } from \"./violations.ts\";\n\n/** AF_UNIX `sun_path` is 108 bytes on Linux, including the terminating NUL. */\nconst SUN_PATH_LIMIT = 108;\n\nexport interface LinuxSandboxBackendOptions {\n\t/** Injectable only for preflight tests; production uses the running platform. */\n\tplatform?: NodeJS.Platform;\n\tcommandExists?: (command: string) => boolean;\n\tviolationStore?: SandboxViolationStore;\n\t/** Injectable only to prove crash cleanup in tests; production spawns `bwrap` directly. */\n\tspawnChild?: typeof spawn;\n}\n\nfunction commandExists(command: string): boolean {\n\tconst result = spawnSync(command, [\"--version\"], { stdio: \"ignore\", timeout: 1_000 });\n\treturn result.status === 0;\n}\n\nfunction waitForExit(child: ReturnType<typeof spawn>): Promise<number> {\n\treturn new Promise((resolve, reject) => {\n\t\tchild.once(\"error\", reject);\n\t\tchild.once(\"exit\", (code) => resolve(code ?? 1));\n\t});\n}\n\nfunction readOnlyMountArguments(path: string, kind: \"directory\" | \"file\" = \"directory\", descriptor = 3): string[] {\n\tconst target = resolve(path);\n\tconst directory = dirname(target);\n\tconst ancestors: string[] = [];\n\tlet current = directory;\n\twhile (current !== \"/\" && current !== \"/home\") {\n\t\tancestors.push(current);\n\t\tcurrent = dirname(current);\n\t}\n\tconst parentArguments = ancestors.reverse().flatMap((ancestor) => [\"--dir\", ancestor]);\n\treturn kind === \"file\"\n\t\t? [...parentArguments, \"--tmpfs\", directory, \"--perms\", \"0400\", \"--file\", String(descriptor), target]\n\t\t: [...parentArguments, \"--ro-bind\", directory, directory];\n}\n\n/**\n * Where the supervisor's proxy socket lives on each side of the boundary.\n *\n * AF_UNIX caps `sun_path` at 108 bytes, so the socket cannot live under the workspace:\n * a deep-but-ordinary project directory exhausts that budget and the sandbox fails to\n * start at all with `listen EINVAL`. The host side therefore gets a short, unique path\n * under the system temp directory, and it is bind-mounted into the child.\n *\n * The child-side path has to sit under `/home`. That is the one writable mount at the\n * point the mountpoint is created — the sandbox root is a read-only bind, so bwrap\n * cannot create a mountpoint at `/run`, `/tmp`, or anywhere else on it.\n */\nexport function resolveProxySocketPaths(temporaryDirectory: string = tmpdir()): {\n\thostSocketPath: string;\n\tchildSocketPath: string;\n} {\n\tconst name = `apex-${process.pid}-${randomBytes(4).toString(\"hex\")}.sock`;\n\t// An unusually long TMPDIR would reintroduce the very limit this avoids.\n\tconst base = join(temporaryDirectory, name).length + 1 > SUN_PATH_LIMIT ? \"/tmp\" : temporaryDirectory;\n\treturn { hostSocketPath: join(base, name), childSocketPath: `/home/${name}` };\n}\n\n/**\n * Classify an unsuccessful child from what its stderr actually evidences, or return\n * undefined when nothing suggests the boundary refused anything.\n *\n * A non-zero exit is not a violation. An invalid API key, a failing test command, or a\n * script exiting 1 are the child's own business, and recording them as sandbox\n * violations blames the boundary for failures it had no part in — which is worse than\n * silence, because it sends whoever reads the output looking for a refusal that never\n * happened. Only Linux's own refusal wording earns a violation here; the proxy records\n * network refusals itself, before this fallback is consulted.\n */\nfunction classifySandboxFailure(stderr: string): \"filesystem\" | \"network\" | undefined {\n\tif (/Read-only file system|Permission denied|Operation not permitted/i.test(stderr)) return \"filesystem\";\n\tif (/Network is unreachable|ENETUNREACH|EHOSTUNREACH/i.test(stderr)) return \"network\";\n\treturn undefined;\n}\n\n/**\n * Linux's deliberately small platform adapter. The sandbox starts from a read-only\n * host root and bind-mounts only the canonical workspace as writable. `--unshare-net`\n * removes all direct network interfaces for the child and every descendant.\n */\nexport function createLinuxSandboxBackend(options?: LinuxSandboxBackendOptions): SandboxBackend {\n\tconst platform = options?.platform ?? process.platform;\n\tif (platform !== \"linux\") {\n\t\treturn {\n\t\t\tstatus: { kind: \"unavailable\", reason: \"OS sandbox is supported on Linux only.\" },\n\t\t\tasync launch() {\n\t\t\t\tthrow new Error(\"Linux sandbox backend is unavailable.\");\n\t\t\t},\n\t\t\tasync close() {},\n\t\t};\n\t}\n\tconst hasCommand = options?.commandExists ?? commandExists;\n\tconst violationStore = options?.violationStore;\n\tlet proxy: SandboxNetworkProxy | undefined;\n\n\tif (!hasCommand(\"bwrap\")) {\n\t\treturn {\n\t\t\tstatus: { kind: \"unavailable\", reason: \"Bubblewrap (bwrap) is required for OS sandboxing.\" },\n\t\t\tasync launch() {\n\t\t\t\tthrow new Error(\"Linux sandbox backend is unavailable.\");\n\t\t\t},\n\t\t\tasync close() {},\n\t\t};\n\t}\n\treturn {\n\t\tstatus: { kind: \"enforced\" },\n\t\tasync launch(launch: SandboxLaunch): Promise<number> {\n\t\t\tconst stateDirectory = join(launch.policy.workspace, \".apex-code\", \"sandbox-state\");\n\t\t\tmkdirSync(stateDirectory, { recursive: true });\n\n\t\t\tconst violationCountBeforeLaunch = violationStore?.totalCount ?? 0;\n\n\t\t\tconst { hostSocketPath, childSocketPath } = resolveProxySocketPaths();\n\t\t\tproxy = await createSandboxNetworkProxy({\n\t\t\t\tsocketPath: hostSocketPath,\n\t\t\t\tallowedHosts: launch.policy.allowedHosts,\n\t\t\t\tviolationStore,\n\t\t\t});\n\n\t\t\t// .cjs forces CommonJS regardless of the target workspace's package.json \"type\"\n\t\t\t// field -- a plain .js here would be parsed as ESM under \"type\": \"module\" and\n\t\t\t// crash on `require`, since Node resolves module type from the nearest package.json.\n\t\t\tconst relayScriptPath = join(stateDirectory, \"relay.cjs\");\n\t\t\twriteFileSync(\n\t\t\t\trelayScriptPath,\n\t\t\t\t`\nconst net = require(\"node:net\");\nconst child_process = require(\"node:child_process\");\n\nconst socketPath = process.env.APEX_UDS_PATH;\nconst server = net.createServer((c) => {\n\tconst client = net.connect(socketPath);\n\tc.pipe(client).pipe(c);\n\tc.on(\"error\", () => {});\n\tclient.on(\"error\", () => {});\n});\n\nserver.listen(0, \"127.0.0.1\", () => {\n\tconst port = server.address().port;\n\tconst env = Object.assign({}, process.env, {\n\t\tHTTP_PROXY: \\`http://127.0.0.1:\\${port}\\`,\n\t\tHTTPS_PROXY: \\`http://127.0.0.1:\\${port}\\`\n\t});\n\tconst args = process.argv.slice(2);\n\tconst child = child_process.spawn(args[0], args.slice(1), {\n\t\tstdio: \"inherit\",\n\t\tenv: env,\n\t});\n\tchild.on(\"exit\", (code, signal) => {\n\t\tprocess.exit(code ?? (signal ? 128 : 1));\n\t});\n});\nserver.on(\"error\", (err) => {\n\tconsole.error(\"Relay error:\", err);\n\tprocess.exit(1);\n});\n`.trim(),\n\t\t\t);\n\n\t\t\tconst readOnlyMounts = [process.execPath, launch.command, ...(launch.readOnlyPaths ?? [])].flatMap((path) =>\n\t\t\t\treadOnlyMountArguments(path),\n\t\t\t);\n\t\t\tconst readOnlyFileMounts = (launch.readOnlyFiles ?? []).flatMap((path, index) =>\n\t\t\t\treadOnlyMountArguments(path, \"file\", index + 3),\n\t\t\t);\n\t\t\tconst readOnlyFileDescriptors = (launch.readOnlyFiles ?? []).map((path) => openSync(path, \"r\"));\n\t\t\t// The descriptors above are opened before the child spawns and must be closed\n\t\t\t// on every exit path -- including a spawn/wait rejection -- or a crashed launch\n\t\t\t// leaks an open handle onto a host-owned credential file for the process's life.\n\t\t\t// The workspace is bind-mounted read-write into the sandbox, so a file\n\t\t\t// here is visible at the same absolute path on both sides of it.\n\t\t\tconst terminalSizePath = join(stateDirectory, \"terminal-size\");\n\t\t\tconst stopPublishingTerminalSize = publishTerminalSize(terminalSizePath);\n\t\t\ttry {\n\t\t\t\tconst spawnBwrap = options?.spawnChild ?? spawn;\n\t\t\t\tconst child = spawnBwrap(\n\t\t\t\t\t\"bwrap\",\n\t\t\t\t\t[\n\t\t\t\t\t\t\"--new-session\",\n\t\t\t\t\t\t\"--die-with-parent\",\n\t\t\t\t\t\t\"--unshare-user\",\n\t\t\t\t\t\t\"--unshare-pid\",\n\t\t\t\t\t\t\"--unshare-net\",\n\t\t\t\t\t\t\"--ro-bind\",\n\t\t\t\t\t\t\"/\",\n\t\t\t\t\t\t\"/\",\n\t\t\t\t\t\t\"--tmpfs\",\n\t\t\t\t\t\t\"/home\",\n\t\t\t\t\t\t// Immediately after the /home tmpfs: that is the only writable mount at\n\t\t\t\t\t\t// this point, so it is the only place bwrap can create the mountpoint.\n\t\t\t\t\t\t\"--bind\",\n\t\t\t\t\t\thostSocketPath,\n\t\t\t\t\t\tchildSocketPath,\n\t\t\t\t\t\t// The credential channel socket: same writable-under-/home constraint\n\t\t\t\t\t\t// and same position as the network socket above. Its child-side path\n\t\t\t\t\t\t// is what `APEX_CREDENTIAL_PROXY_PATH` names in the launch environment.\n\t\t\t\t\t\t...(launch.credentialChannel\n\t\t\t\t\t\t\t? [\"--bind\", launch.credentialChannel.hostSocketPath, launch.credentialChannel.childSocketPath]\n\t\t\t\t\t\t\t: []),\n\t\t\t\t\t\t...readOnlyMounts,\n\t\t\t\t\t\t...readOnlyFileMounts,\n\t\t\t\t\t\t\"--bind\",\n\t\t\t\t\t\tlaunch.policy.workspace,\n\t\t\t\t\t\tlaunch.policy.workspace,\n\t\t\t\t\t\t// After the workspace bind: these destinations sit inside it, and an\n\t\t\t\t\t\t// earlier mount would be masked when the workspace is bound over them.\n\t\t\t\t\t\t...(launch.readOnlyBinaries ?? []).flatMap(({ source, destination }) => [\n\t\t\t\t\t\t\t\"--ro-bind\",\n\t\t\t\t\t\t\tsource,\n\t\t\t\t\t\t\tdestination,\n\t\t\t\t\t\t]),\n\t\t\t\t\t\t\"--dev\",\n\t\t\t\t\t\t\"/dev\",\n\t\t\t\t\t\t\"--proc\",\n\t\t\t\t\t\t\"/proc\",\n\t\t\t\t\t\t\"--chdir\",\n\t\t\t\t\t\tlaunch.policy.workspace,\n\t\t\t\t\t\t\"--setenv\",\n\t\t\t\t\t\t\"HOME\",\n\t\t\t\t\t\tlaunch.environment?.HOME ?? stateDirectory,\n\t\t\t\t\t\t\"--setenv\",\n\t\t\t\t\t\t\"TMPDIR\",\n\t\t\t\t\t\tlaunch.environment?.TMPDIR ?? stateDirectory,\n\t\t\t\t\t\t\"--setenv\",\n\t\t\t\t\t\t\"APEX_UDS_PATH\",\n\t\t\t\t\t\tchildSocketPath,\n\t\t\t\t\t\t\"--setenv\",\n\t\t\t\t\t\tTERMINAL_SIZE_PATH_VARIABLE,\n\t\t\t\t\t\tterminalSizePath,\n\t\t\t\t\t\t\"--\",\n\t\t\t\t\t\tprocess.execPath,\n\t\t\t\t\t\trelayScriptPath,\n\t\t\t\t\t\tlaunch.command,\n\t\t\t\t\t\t...launch.args,\n\t\t\t\t\t],\n\t\t\t\t\t{ env: launch.environment, stdio: [\"inherit\", \"inherit\", \"pipe\", ...readOnlyFileDescriptors] },\n\t\t\t\t);\n\t\t\t\tlet stderr = \"\";\n\t\t\t\tchild.stderr?.setEncoding(\"utf8\");\n\t\t\t\tchild.stderr?.on(\"data\", (chunk: string) => {\n\t\t\t\t\tstderr += chunk;\n\t\t\t\t\tprocess.stderr.write(chunk);\n\t\t\t\t});\n\t\t\t\tconst exitCode = await waitForExit(child);\n\t\t\t\tawait proxy?.close();\n\t\t\t\tconst proxyAlreadyRecordedAViolation = (violationStore?.totalCount ?? 0) > violationCountBeforeLaunch;\n\t\t\t\tconst kind = exitCode !== 0 && !proxyAlreadyRecordedAViolation ? classifySandboxFailure(stderr) : undefined;\n\t\t\t\tif (kind) {\n\t\t\t\t\tviolationStore?.add({\n\t\t\t\t\t\tkind,\n\t\t\t\t\t\tcommand: [launch.command, ...launch.args].join(\" \"),\n\t\t\t\t\t\tdetail: stderr.trim(),\n\t\t\t\t\t\ttimestamp: new Date(),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn exitCode;\n\t\t\t} finally {\n\t\t\t\tstopPublishingTerminalSize();\n\t\t\t\tfor (const descriptor of readOnlyFileDescriptors) closeSync(descriptor);\n\t\t\t\t// The socket now lives outside the workspace, so nothing else will ever clean\n\t\t\t\t// it up; leaving it would litter the temp directory once per launch.\n\t\t\t\trmSync(hostSocketPath, { force: true });\n\t\t\t}\n\t\t},\n\t\tasync close() {\n\t\t\tawait proxy?.close();\n\t\t},\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"linux-backend.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/linux-backend.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAa,MAAM,oBAAoB,CAAC;AAQtD,OAAO,EAEN,KAAK,wBAAwB,EAI7B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACN,qBAAqB,EAIrB,MAAM,gCAAgC,CAAC;AAExC,OAAO,KAAK,EAAE,cAAc,EAAiB,MAAM,iBAAiB,CAAC;AAQrE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAK7D,MAAM,WAAW,0BAA0B;IAC1C,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,cAAc,CAAC,EAAE,qBAAqB,CAAC;IACvC,2FAA2F;IAC3F,UAAU,CAAC,EAAE,OAAO,KAAK,CAAC;IAC1B;;;;OAIG;IACH,2BAA2B,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACjE,kFAAkF;IAClF,iBAAiB,CAAC,EAAE,OAAO,qBAAqB,CAAC;IACjD,qFAAqF;IACrF,wBAAwB,CAAC,EAAE,CAAC,OAAO,EAAE,wBAAwB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACnF,sFAAsF;IACtF,wBAAwB,CAAC,EAAE,MAAM,CAAC;CAClC;AAcD;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,kBAAkB,GAAE,MAAiB,GAAG;IAC/E,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;CACxB,CAKA;AAmBD;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE,0BAA0B,GAAG,cAAc,CA2N9F","sourcesContent":["import { spawn, spawnSync } from \"node:child_process\";\nimport { randomBytes } from \"node:crypto\";\nimport { closeSync, mkdirSync, openSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { buildBwrapArguments, descriptorBackedReadOnlyPaths } from \"./bwrap-arguments.ts\";\nimport { createCommandEscalationApprover, createCredentialReleaser, createHostApprover } from \"./host-approval.ts\";\nimport { createSandboxNetworkProxy, type SandboxNetworkProxy } from \"./network-proxy.ts\";\nimport {\n\tCOMMAND_ESCALATION_SOCKET_VARIABLE,\n\ttype CommandEscalationRequest,\n\ttype CommandEscalationResult,\n\tcreateCommandEscalationProxy,\n\tresolveCommandEscalationChannelPaths,\n} from \"./rpc/command-proxy.ts\";\nimport {\n\tfillHostGitCredential,\n\tGIT_CREDENTIAL_SOCKET_VARIABLE,\n\tresolveGitCredentialChannelPaths,\n\twriteGitCredentialHelper,\n} from \"./rpc/git-credential-helper.ts\";\nimport { createGitCredentialProxy, type GitCredentialProxy } from \"./rpc/git-credential-proxy.ts\";\nimport type { SandboxBackend, SandboxLaunch } from \"./supervisor.ts\";\nimport {\n\tcreateTerminalHandoff,\n\tTERMINAL_HANDOFF_ACK_PATH_VARIABLE,\n\tTERMINAL_HANDOFF_PATH_VARIABLE,\n\ttype TerminalHandoff,\n} from \"./terminal-handoff.ts\";\nimport { publishTerminalSize, TERMINAL_SIZE_PATH_VARIABLE } from \"./terminal-size.ts\";\nimport type { SandboxViolationStore } from \"./violations.ts\";\n\n/** AF_UNIX `sun_path` is 108 bytes on Linux, including the terminating NUL. */\nconst SUN_PATH_LIMIT = 108;\n\nexport interface LinuxSandboxBackendOptions {\n\t/** Injectable only for preflight tests; production uses the running platform. */\n\tplatform?: NodeJS.Platform;\n\tcommandExists?: (command: string) => boolean;\n\tviolationStore?: SandboxViolationStore;\n\t/** Injectable only to prove crash cleanup in tests; production spawns `bwrap` directly. */\n\tspawnChild?: typeof spawn;\n\t/**\n\t * Injectable only for tests, which have no terminal and would otherwise be unable to\n\t * exercise a released credential at all. Production builds the releaser from the\n\t * terminal handoff, and its absence there is what makes a headless session refuse.\n\t */\n\trequestGitCredentialRelease?: (host: string) => Promise<boolean>;\n\t/** Injectable only for tests; production resolves against the real host store. */\n\tfillGitCredential?: typeof fillHostGitCredential;\n\t/** Injectable only for tests, which have no terminal to approve an escalation at. */\n\trequestCommandEscalation?: (request: CommandEscalationRequest) => Promise<boolean>;\n\t/** Retained for source compatibility; launch state is now required on each launch. */\n\tsupervisorStateDirectory?: string;\n}\n\nfunction commandExists(command: string): boolean {\n\tconst result = spawnSync(command, [\"--version\"], { stdio: \"ignore\", timeout: 1_000 });\n\treturn result.status === 0;\n}\n\nfunction waitForExit(child: ReturnType<typeof spawn>): Promise<number> {\n\treturn new Promise((resolve, reject) => {\n\t\tchild.once(\"error\", reject);\n\t\tchild.once(\"exit\", (code) => resolve(code ?? 1));\n\t});\n}\n\n/**\n * Where the supervisor's proxy socket lives on each side of the boundary.\n *\n * AF_UNIX caps `sun_path` at 108 bytes, so the socket cannot live under the workspace:\n * a deep-but-ordinary project directory exhausts that budget and the sandbox fails to\n * start at all with `listen EINVAL`. The host side therefore gets a short, unique path\n * under the system temp directory, and it is bind-mounted into the child.\n *\n * The child-side path has to sit under `/home`. That is the one writable mount at the\n * point the mountpoint is created — the sandbox root is a read-only bind, so bwrap\n * cannot create a mountpoint at `/run`, `/tmp`, or anywhere else on it.\n */\nexport function resolveProxySocketPaths(temporaryDirectory: string = tmpdir()): {\n\thostSocketPath: string;\n\tchildSocketPath: string;\n} {\n\tconst name = `apex-${process.pid}-${randomBytes(4).toString(\"hex\")}.sock`;\n\t// An unusually long TMPDIR would reintroduce the very limit this avoids.\n\tconst base = join(temporaryDirectory, name).length + 1 > SUN_PATH_LIMIT ? \"/tmp\" : temporaryDirectory;\n\treturn { hostSocketPath: join(base, name), childSocketPath: `/home/${name}` };\n}\n\n/**\n * Classify an unsuccessful child from what its stderr actually evidences, or return\n * undefined when nothing suggests the boundary refused anything.\n *\n * A non-zero exit is not a violation. An invalid API key, a failing test command, or a\n * script exiting 1 are the child's own business, and recording them as sandbox\n * violations blames the boundary for failures it had no part in — which is worse than\n * silence, because it sends whoever reads the output looking for a refusal that never\n * happened. Only Linux's own refusal wording earns a violation here; the proxy records\n * network refusals itself, before this fallback is consulted.\n */\nfunction classifySandboxFailure(stderr: string): \"filesystem\" | \"network\" | undefined {\n\tif (/Read-only file system|Permission denied|Operation not permitted/i.test(stderr)) return \"filesystem\";\n\tif (/Network is unreachable|ENETUNREACH|EHOSTUNREACH/i.test(stderr)) return \"network\";\n\treturn undefined;\n}\n\n/**\n * Linux's deliberately small platform adapter. The sandbox starts from a read-only\n * host root and bind-mounts only the canonical workspace as writable. `--unshare-net`\n * removes all direct network interfaces for the child and every descendant.\n */\nexport function createLinuxSandboxBackend(options?: LinuxSandboxBackendOptions): SandboxBackend {\n\tconst platform = options?.platform ?? process.platform;\n\tif (platform !== \"linux\") {\n\t\treturn {\n\t\t\tstatus: { kind: \"unavailable\", reason: \"OS sandbox is supported on Linux only.\" },\n\t\t\tasync launch() {\n\t\t\t\tthrow new Error(\"Linux sandbox backend is unavailable.\");\n\t\t\t},\n\t\t\tasync close() {},\n\t\t};\n\t}\n\tconst hasCommand = options?.commandExists ?? commandExists;\n\tconst violationStore = options?.violationStore;\n\tlet proxy: SandboxNetworkProxy | undefined;\n\tlet handoff: TerminalHandoff | undefined;\n\tlet gitCredentialProxy: GitCredentialProxy | undefined;\n\tlet gitCredentialDirectory: string | undefined;\n\tlet escalationProxy: Awaited<ReturnType<typeof createCommandEscalationProxy>> | undefined;\n\tlet escalationDirectory: string | undefined;\n\tif (!hasCommand(\"bwrap\")) {\n\t\treturn {\n\t\t\tstatus: { kind: \"unavailable\", reason: \"Bubblewrap (bwrap) is required for OS sandboxing.\" },\n\t\t\tasync launch() {\n\t\t\t\tthrow new Error(\"Linux sandbox backend is unavailable.\");\n\t\t\t},\n\t\t\tasync close() {},\n\t\t};\n\t}\n\treturn {\n\t\tstatus: { kind: \"enforced\" },\n\t\tasync launch(launch: SandboxLaunch): Promise<number> {\n\t\t\tconst supervisorStateDirectory = launch.supervisorStateDirectory;\n\t\t\tmkdirSync(supervisorStateDirectory, { recursive: true, mode: 0o700 });\n\t\t\tconst stateDirectory = launch.policy.workspace;\n\t\t\tconst acknowledgementPath = join(launch.policy.workspace, \".apex-code\", \"sandbox-handoff-ack\");\n\t\t\tmkdirSync(join(launch.policy.workspace, \".apex-code\"), { recursive: true });\n\n\t\t\tconst violationCountBeforeLaunch = violationStore?.totalCount ?? 0;\n\n\t\t\tconst { hostSocketPath, childSocketPath } = resolveProxySocketPaths();\n\t\t\thandoff = createTerminalHandoff(supervisorStateDirectory, { acknowledgementPath });\n\t\t\tconst gitCredentialPaths = resolveGitCredentialChannelPaths();\n\t\t\tgitCredentialDirectory = gitCredentialPaths.hostSocketDirectory;\n\t\t\tproxy = await createSandboxNetworkProxy({\n\t\t\t\tsocketPath: hostSocketPath,\n\t\t\t\tallowedHosts: launch.policy.allowedHosts,\n\t\t\t\tviolationStore,\n\t\t\t\t// Undefined without a terminal, which leaves the proxy's own\n\t\t\t\t// deny-without-asking path in place for headless, print, JSON, and RPC.\n\t\t\t\trequestApproval: createHostApprover({ handoff }),\n\t\t\t});\n\t\t\t// After the network proxy, because reachability is its answer to give: the\n\t\t\t// credential channel must never hand out a token for a host this session\n\t\t\t// cannot even open a connection to.\n\t\t\tgitCredentialProxy = await createGitCredentialProxy({\n\t\t\t\tsocketPath: gitCredentialPaths.hostSocketPath,\n\t\t\t\tisHostAllowed: (host) => proxy?.isHostReachable(host) ?? false,\n\t\t\t\trequestRelease: options?.requestGitCredentialRelease ?? createCredentialReleaser({ handoff }),\n\t\t\t\t// The supervisor's own environment, deliberately: this runs outside the\n\t\t\t\t// boundary and must resolve against the real host home, which is exactly\n\t\t\t\t// what the child cannot see.\n\t\t\t\tfillCredential: (request) =>\n\t\t\t\t\t(options?.fillGitCredential ?? fillHostGitCredential)(request, { environment: process.env }),\n\t\t\t\tviolationStore,\n\t\t\t});\n\t\t\twriteGitCredentialHelper(supervisorStateDirectory);\n\n\t\t\tconst escalationPaths = resolveCommandEscalationChannelPaths();\n\t\t\tescalationDirectory = escalationPaths.hostSocketDirectory;\n\n\t\t\t// .cjs forces CommonJS regardless of the target workspace's package.json \"type\"\n\t\t\t// field -- a plain .js here would be parsed as ESM under \"type\": \"module\" and\n\t\t\t// crash on `require`, since Node resolves module type from the nearest package.json.\n\t\t\tconst relayScriptPath = join(supervisorStateDirectory, \"relay.cjs\");\n\t\t\twriteFileSync(\n\t\t\t\trelayScriptPath,\n\t\t\t\t`\nconst net = require(\"node:net\");\nconst child_process = require(\"node:child_process\");\n\nconst socketPath = process.env.APEX_UDS_PATH;\nconst server = net.createServer((c) => {\n\tconst client = net.connect(socketPath);\n\tc.pipe(client).pipe(c);\n\tc.on(\"error\", () => {});\n\tclient.on(\"error\", () => {});\n});\n\nserver.listen(0, \"127.0.0.1\", () => {\n\tconst port = server.address().port;\n\tconst env = Object.assign({}, process.env, {\n\t\tHTTP_PROXY: \\`http://127.0.0.1:\\${port}\\`,\n\t\tHTTPS_PROXY: \\`http://127.0.0.1:\\${port}\\`\n\t});\n\tconst args = process.argv.slice(2);\n\tconst child = child_process.spawn(args[0], args.slice(1), {\n\t\tstdio: \"inherit\",\n\t\tenv: env,\n\t});\n\tchild.on(\"exit\", (code, signal) => {\n\t\tprocess.exit(code ?? (signal ? 128 : 1));\n\t});\n});\nserver.on(\"error\", (err) => {\n\tconsole.error(\"Relay error:\", err);\n\tprocess.exit(1);\n});\n`.trim(),\n\t\t\t);\n\n\t\t\tconst primaryReadOnlyPaths = [\n\t\t\t\tprocess.execPath,\n\t\t\t\tlaunch.command,\n\t\t\t\tsupervisorStateDirectory,\n\t\t\t\t...(launch.readOnlyPaths ?? []),\n\t\t\t];\n\t\t\tconst readOnlyFileDescriptors = [\n\t\t\t\t...descriptorBackedReadOnlyPaths(primaryReadOnlyPaths),\n\t\t\t\t...(launch.readOnlyFiles ?? []),\n\t\t\t].map((path) => openSync(path, \"r\"));\n\t\t\t// The descriptors above are opened before the child spawns and must be closed\n\t\t\t// on every exit path -- including a spawn/wait rejection -- or a crashed launch\n\t\t\t// leaks an open handle onto a host-owned credential file for the process's life.\n\t\t\t// The workspace is bind-mounted read-write into the sandbox, so a file\n\t\t\t// here is visible at the same absolute path on both sides of it.\n\t\t\tconst terminalSizePath = join(supervisorStateDirectory, \"terminal-size\");\n\t\t\tconst stopPublishingTerminalSize = publishTerminalSize(terminalSizePath);\n\n\t\t\t// The second child derives its argv from the same builder as the first, with one\n\t\t\t// extra writable root. Nothing about the original child's namespace changes.\n\t\t\tescalationProxy = await createCommandEscalationProxy({\n\t\t\t\tsocketPath: escalationPaths.hostSocketPath,\n\t\t\t\trequestApproval: options?.requestCommandEscalation ?? createCommandEscalationApprover({ handoff }),\n\t\t\t\tviolationStore,\n\t\t\t\trunEscalated: (request) =>\n\t\t\t\t\trunEscalatedCommand({\n\t\t\t\t\t\trequest,\n\t\t\t\t\t\tlaunch,\n\t\t\t\t\t\tstateDirectory,\n\t\t\t\t\t\tspawnBwrap: options?.spawnChild ?? spawn,\n\t\t\t\t\t}),\n\t\t\t});\n\t\t\ttry {\n\t\t\t\tconst spawnBwrap = options?.spawnChild ?? spawn;\n\t\t\t\tconst child = spawnBwrap(\n\t\t\t\t\t\"bwrap\",\n\t\t\t\t\tbuildBwrapArguments({\n\t\t\t\t\t\tworkspace: launch.policy.workspace,\n\t\t\t\t\t\tadditionalWritableRoots: launch.policy.additionalWritableRoots,\n\t\t\t\t\t\treadOnlyPaths: primaryReadOnlyPaths,\n\t\t\t\t\t\treadOnlyFiles: launch.readOnlyFiles ?? [],\n\t\t\t\t\t\treadOnlyBinaries: launch.readOnlyBinaries ?? [],\n\t\t\t\t\t\tsockets: [\n\t\t\t\t\t\t\t{ hostPath: hostSocketPath, childPath: childSocketPath },\n\t\t\t\t\t\t\t// Its child-side path is what `APEX_CREDENTIAL_PROXY_PATH` names.\n\t\t\t\t\t\t\t...(launch.credentialChannel\n\t\t\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\thostPath: launch.credentialChannel.hostSocketPath,\n\t\t\t\t\t\t\t\t\t\t\tchildPath: launch.credentialChannel.childSocketPath,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t: []),\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\thostPath: gitCredentialPaths.hostSocketPath,\n\t\t\t\t\t\t\t\tchildPath: gitCredentialPaths.childSocketPath,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t\tenvironment: {\n\t\t\t\t\t\t\tHOME: launch.environment?.HOME ?? stateDirectory,\n\t\t\t\t\t\t\tTMPDIR: launch.environment?.TMPDIR ?? stateDirectory,\n\t\t\t\t\t\t\tAPEX_UDS_PATH: childSocketPath,\n\t\t\t\t\t\t\t[TERMINAL_SIZE_PATH_VARIABLE]: terminalSizePath,\n\t\t\t\t\t\t\t[TERMINAL_HANDOFF_PATH_VARIABLE]: supervisorStateDirectory,\n\t\t\t\t\t\t\t[TERMINAL_HANDOFF_ACK_PATH_VARIABLE]: acknowledgementPath,\n\t\t\t\t\t\t\t[GIT_CREDENTIAL_SOCKET_VARIABLE]: gitCredentialPaths.childSocketPath,\n\t\t\t\t\t\t\t[COMMAND_ESCALATION_SOCKET_VARIABLE]: escalationPaths.childSocketPath,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tcommand: process.execPath,\n\t\t\t\t\t\targs: [relayScriptPath, launch.command, ...launch.args],\n\t\t\t\t\t}),\n\t\t\t\t\t{ env: launch.environment, stdio: [\"inherit\", \"inherit\", \"pipe\", ...readOnlyFileDescriptors] },\n\t\t\t\t);\n\t\t\t\tlet stderr = \"\";\n\t\t\t\tchild.stderr?.setEncoding(\"utf8\");\n\t\t\t\tchild.stderr?.on(\"data\", (chunk: string) => {\n\t\t\t\t\tstderr += chunk;\n\t\t\t\t\tprocess.stderr.write(chunk);\n\t\t\t\t});\n\t\t\t\tconst exitCode = await waitForExit(child);\n\t\t\t\tawait proxy?.close();\n\t\t\t\tconst proxyAlreadyRecordedAViolation = (violationStore?.totalCount ?? 0) > violationCountBeforeLaunch;\n\t\t\t\tconst kind = exitCode !== 0 && !proxyAlreadyRecordedAViolation ? classifySandboxFailure(stderr) : undefined;\n\t\t\t\tif (kind) {\n\t\t\t\t\tviolationStore?.add({\n\t\t\t\t\t\tkind,\n\t\t\t\t\t\tcommand: [launch.command, ...launch.args].join(\" \"),\n\t\t\t\t\t\tdetail: stderr.trim(),\n\t\t\t\t\t\ttimestamp: new Date(),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn exitCode;\n\t\t\t} finally {\n\t\t\t\tstopPublishingTerminalSize();\n\t\t\t\tfor (const descriptor of readOnlyFileDescriptors) closeSync(descriptor);\n\t\t\t\t// The socket now lives outside the workspace, so nothing else will ever clean\n\t\t\t\t// it up; leaving it would litter the temp directory once per launch.\n\t\t\t\trmSync(hostSocketPath, { force: true });\n\t\t\t}\n\t\t},\n\t\tasync close() {\n\t\t\thandoff?.stop();\n\t\t\tawait escalationProxy?.close();\n\t\t\tif (escalationDirectory) rmSync(escalationDirectory, { force: true, recursive: true });\n\t\t\tawait gitCredentialProxy?.close();\n\t\t\tif (gitCredentialDirectory) rmSync(gitCredentialDirectory, { force: true, recursive: true });\n\t\t\tawait proxy?.close();\n\t\t},\n\t};\n}\n\n/**\n * Start one approved command in its own child, with one extra writable root.\n *\n * Deliberately minimal compared with the session child: no network relay, no credential\n * channels, no terminal handoff. An escalated command is a single shell invocation the\n * human just read and approved, not a session, and every channel omitted here is one it\n * cannot reach. Its stdio is captured rather than inherited so the session that asked\n * receives the output instead of the escalated process writing over the TUI.\n */\nasync function runEscalatedCommand(options: {\n\trequest: CommandEscalationRequest;\n\tlaunch: SandboxLaunch;\n\tstateDirectory: string;\n\tspawnBwrap: typeof spawn;\n}): Promise<CommandEscalationResult> {\n\tconst { request, launch, stateDirectory } = options;\n\tconst escalatedReadOnlyPaths = launch.readOnlyPaths ?? [];\n\tconst readOnlyFileDescriptors = [\n\t\t...descriptorBackedReadOnlyPaths(escalatedReadOnlyPaths),\n\t\t...(launch.readOnlyFiles ?? []),\n\t].map((path) => openSync(path, \"r\"));\n\tconst child = options.spawnBwrap(\n\t\t\"bwrap\",\n\t\tbuildBwrapArguments({\n\t\t\tworkspace: launch.policy.workspace,\n\t\t\tadditionalWritableRoots: [...launch.policy.additionalWritableRoots, request.writableRoot],\n\t\t\treadOnlyPaths: escalatedReadOnlyPaths,\n\t\t\treadOnlyFiles: launch.readOnlyFiles ?? [],\n\t\t\treadOnlyBinaries: launch.readOnlyBinaries ?? [],\n\t\t\tsockets: [],\n\t\t\tenvironment: {\n\t\t\t\tHOME: launch.environment?.HOME ?? stateDirectory,\n\t\t\t\tTMPDIR: launch.environment?.TMPDIR ?? stateDirectory,\n\t\t\t},\n\t\t\tcommand: \"/bin/sh\",\n\t\t\targs: [\"-c\", request.command],\n\t\t}),\n\t\t{ stdio: [\"ignore\", \"pipe\", \"pipe\", ...readOnlyFileDescriptors] },\n\t);\n\tlet stdout = \"\";\n\tlet stderr = \"\";\n\ttry {\n\t\tchild.stdout?.setEncoding(\"utf8\");\n\t\tchild.stdout?.on(\"data\", (chunk: string) => {\n\t\t\tstdout += chunk;\n\t\t});\n\t\tchild.stderr?.setEncoding(\"utf8\");\n\t\tchild.stderr?.on(\"data\", (chunk: string) => {\n\t\t\tstderr += chunk;\n\t\t});\n\t\tconst code = await new Promise<number>((resolveCode) => {\n\t\t\tchild.once(\"error\", () => resolveCode(1));\n\t\t\tchild.once(\"exit\", (exitCode) => resolveCode(exitCode ?? 1));\n\t\t});\n\t\treturn { code, stdout, stderr };\n\t} finally {\n\t\tfor (const descriptor of readOnlyFileDescriptors) closeSync(descriptor);\n\t}\n}\n"]}
|
|
@@ -2,8 +2,14 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
3
|
import { closeSync, mkdirSync, openSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
|
-
import {
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { buildBwrapArguments, descriptorBackedReadOnlyPaths } from "./bwrap-arguments.js";
|
|
7
|
+
import { createCommandEscalationApprover, createCredentialReleaser, createHostApprover } from "./host-approval.js";
|
|
6
8
|
import { createSandboxNetworkProxy } from "./network-proxy.js";
|
|
9
|
+
import { COMMAND_ESCALATION_SOCKET_VARIABLE, createCommandEscalationProxy, resolveCommandEscalationChannelPaths, } from "./rpc/command-proxy.js";
|
|
10
|
+
import { fillHostGitCredential, GIT_CREDENTIAL_SOCKET_VARIABLE, resolveGitCredentialChannelPaths, writeGitCredentialHelper, } from "./rpc/git-credential-helper.js";
|
|
11
|
+
import { createGitCredentialProxy } from "./rpc/git-credential-proxy.js";
|
|
12
|
+
import { createTerminalHandoff, TERMINAL_HANDOFF_ACK_PATH_VARIABLE, TERMINAL_HANDOFF_PATH_VARIABLE, } from "./terminal-handoff.js";
|
|
7
13
|
import { publishTerminalSize, TERMINAL_SIZE_PATH_VARIABLE } from "./terminal-size.js";
|
|
8
14
|
/** AF_UNIX `sun_path` is 108 bytes on Linux, including the terminating NUL. */
|
|
9
15
|
const SUN_PATH_LIMIT = 108;
|
|
@@ -17,20 +23,6 @@ function waitForExit(child) {
|
|
|
17
23
|
child.once("exit", (code) => resolve(code ?? 1));
|
|
18
24
|
});
|
|
19
25
|
}
|
|
20
|
-
function readOnlyMountArguments(path, kind = "directory", descriptor = 3) {
|
|
21
|
-
const target = resolve(path);
|
|
22
|
-
const directory = dirname(target);
|
|
23
|
-
const ancestors = [];
|
|
24
|
-
let current = directory;
|
|
25
|
-
while (current !== "/" && current !== "/home") {
|
|
26
|
-
ancestors.push(current);
|
|
27
|
-
current = dirname(current);
|
|
28
|
-
}
|
|
29
|
-
const parentArguments = ancestors.reverse().flatMap((ancestor) => ["--dir", ancestor]);
|
|
30
|
-
return kind === "file"
|
|
31
|
-
? [...parentArguments, "--tmpfs", directory, "--perms", "0400", "--file", String(descriptor), target]
|
|
32
|
-
: [...parentArguments, "--ro-bind", directory, directory];
|
|
33
|
-
}
|
|
34
26
|
/**
|
|
35
27
|
* Where the supervisor's proxy socket lives on each side of the boundary.
|
|
36
28
|
*
|
|
@@ -86,6 +78,11 @@ export function createLinuxSandboxBackend(options) {
|
|
|
86
78
|
const hasCommand = options?.commandExists ?? commandExists;
|
|
87
79
|
const violationStore = options?.violationStore;
|
|
88
80
|
let proxy;
|
|
81
|
+
let handoff;
|
|
82
|
+
let gitCredentialProxy;
|
|
83
|
+
let gitCredentialDirectory;
|
|
84
|
+
let escalationProxy;
|
|
85
|
+
let escalationDirectory;
|
|
89
86
|
if (!hasCommand("bwrap")) {
|
|
90
87
|
return {
|
|
91
88
|
status: { kind: "unavailable", reason: "Bubblewrap (bwrap) is required for OS sandboxing." },
|
|
@@ -98,19 +95,44 @@ export function createLinuxSandboxBackend(options) {
|
|
|
98
95
|
return {
|
|
99
96
|
status: { kind: "enforced" },
|
|
100
97
|
async launch(launch) {
|
|
101
|
-
const
|
|
102
|
-
mkdirSync(
|
|
98
|
+
const supervisorStateDirectory = launch.supervisorStateDirectory;
|
|
99
|
+
mkdirSync(supervisorStateDirectory, { recursive: true, mode: 0o700 });
|
|
100
|
+
const stateDirectory = launch.policy.workspace;
|
|
101
|
+
const acknowledgementPath = join(launch.policy.workspace, ".apex-code", "sandbox-handoff-ack");
|
|
102
|
+
mkdirSync(join(launch.policy.workspace, ".apex-code"), { recursive: true });
|
|
103
103
|
const violationCountBeforeLaunch = violationStore?.totalCount ?? 0;
|
|
104
104
|
const { hostSocketPath, childSocketPath } = resolveProxySocketPaths();
|
|
105
|
+
handoff = createTerminalHandoff(supervisorStateDirectory, { acknowledgementPath });
|
|
106
|
+
const gitCredentialPaths = resolveGitCredentialChannelPaths();
|
|
107
|
+
gitCredentialDirectory = gitCredentialPaths.hostSocketDirectory;
|
|
105
108
|
proxy = await createSandboxNetworkProxy({
|
|
106
109
|
socketPath: hostSocketPath,
|
|
107
110
|
allowedHosts: launch.policy.allowedHosts,
|
|
108
111
|
violationStore,
|
|
112
|
+
// Undefined without a terminal, which leaves the proxy's own
|
|
113
|
+
// deny-without-asking path in place for headless, print, JSON, and RPC.
|
|
114
|
+
requestApproval: createHostApprover({ handoff }),
|
|
115
|
+
});
|
|
116
|
+
// After the network proxy, because reachability is its answer to give: the
|
|
117
|
+
// credential channel must never hand out a token for a host this session
|
|
118
|
+
// cannot even open a connection to.
|
|
119
|
+
gitCredentialProxy = await createGitCredentialProxy({
|
|
120
|
+
socketPath: gitCredentialPaths.hostSocketPath,
|
|
121
|
+
isHostAllowed: (host) => proxy?.isHostReachable(host) ?? false,
|
|
122
|
+
requestRelease: options?.requestGitCredentialRelease ?? createCredentialReleaser({ handoff }),
|
|
123
|
+
// The supervisor's own environment, deliberately: this runs outside the
|
|
124
|
+
// boundary and must resolve against the real host home, which is exactly
|
|
125
|
+
// what the child cannot see.
|
|
126
|
+
fillCredential: (request) => (options?.fillGitCredential ?? fillHostGitCredential)(request, { environment: process.env }),
|
|
127
|
+
violationStore,
|
|
109
128
|
});
|
|
129
|
+
writeGitCredentialHelper(supervisorStateDirectory);
|
|
130
|
+
const escalationPaths = resolveCommandEscalationChannelPaths();
|
|
131
|
+
escalationDirectory = escalationPaths.hostSocketDirectory;
|
|
110
132
|
// .cjs forces CommonJS regardless of the target workspace's package.json "type"
|
|
111
133
|
// field -- a plain .js here would be parsed as ESM under "type": "module" and
|
|
112
134
|
// crash on `require`, since Node resolves module type from the nearest package.json.
|
|
113
|
-
const relayScriptPath = join(
|
|
135
|
+
const relayScriptPath = join(supervisorStateDirectory, "relay.cjs");
|
|
114
136
|
writeFileSync(relayScriptPath, `
|
|
115
137
|
const net = require("node:net");
|
|
116
138
|
const child_process = require("node:child_process");
|
|
@@ -143,76 +165,73 @@ server.on("error", (err) => {
|
|
|
143
165
|
process.exit(1);
|
|
144
166
|
});
|
|
145
167
|
`.trim());
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
168
|
+
const primaryReadOnlyPaths = [
|
|
169
|
+
process.execPath,
|
|
170
|
+
launch.command,
|
|
171
|
+
supervisorStateDirectory,
|
|
172
|
+
...(launch.readOnlyPaths ?? []),
|
|
173
|
+
];
|
|
174
|
+
const readOnlyFileDescriptors = [
|
|
175
|
+
...descriptorBackedReadOnlyPaths(primaryReadOnlyPaths),
|
|
176
|
+
...(launch.readOnlyFiles ?? []),
|
|
177
|
+
].map((path) => openSync(path, "r"));
|
|
149
178
|
// The descriptors above are opened before the child spawns and must be closed
|
|
150
179
|
// on every exit path -- including a spawn/wait rejection -- or a crashed launch
|
|
151
180
|
// leaks an open handle onto a host-owned credential file for the process's life.
|
|
152
181
|
// The workspace is bind-mounted read-write into the sandbox, so a file
|
|
153
182
|
// here is visible at the same absolute path on both sides of it.
|
|
154
|
-
const terminalSizePath = join(
|
|
183
|
+
const terminalSizePath = join(supervisorStateDirectory, "terminal-size");
|
|
155
184
|
const stopPublishingTerminalSize = publishTerminalSize(terminalSizePath);
|
|
185
|
+
// The second child derives its argv from the same builder as the first, with one
|
|
186
|
+
// extra writable root. Nothing about the original child's namespace changes.
|
|
187
|
+
escalationProxy = await createCommandEscalationProxy({
|
|
188
|
+
socketPath: escalationPaths.hostSocketPath,
|
|
189
|
+
requestApproval: options?.requestCommandEscalation ?? createCommandEscalationApprover({ handoff }),
|
|
190
|
+
violationStore,
|
|
191
|
+
runEscalated: (request) => runEscalatedCommand({
|
|
192
|
+
request,
|
|
193
|
+
launch,
|
|
194
|
+
stateDirectory,
|
|
195
|
+
spawnBwrap: options?.spawnChild ?? spawn,
|
|
196
|
+
}),
|
|
197
|
+
});
|
|
156
198
|
try {
|
|
157
199
|
const spawnBwrap = options?.spawnChild ?? spawn;
|
|
158
|
-
const child = spawnBwrap("bwrap",
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
]
|
|
192
|
-
|
|
193
|
-
"/dev",
|
|
194
|
-
"--proc",
|
|
195
|
-
"/proc",
|
|
196
|
-
"--chdir",
|
|
197
|
-
launch.policy.workspace,
|
|
198
|
-
"--setenv",
|
|
199
|
-
"HOME",
|
|
200
|
-
launch.environment?.HOME ?? stateDirectory,
|
|
201
|
-
"--setenv",
|
|
202
|
-
"TMPDIR",
|
|
203
|
-
launch.environment?.TMPDIR ?? stateDirectory,
|
|
204
|
-
"--setenv",
|
|
205
|
-
"APEX_UDS_PATH",
|
|
206
|
-
childSocketPath,
|
|
207
|
-
"--setenv",
|
|
208
|
-
TERMINAL_SIZE_PATH_VARIABLE,
|
|
209
|
-
terminalSizePath,
|
|
210
|
-
"--",
|
|
211
|
-
process.execPath,
|
|
212
|
-
relayScriptPath,
|
|
213
|
-
launch.command,
|
|
214
|
-
...launch.args,
|
|
215
|
-
], { env: launch.environment, stdio: ["inherit", "inherit", "pipe", ...readOnlyFileDescriptors] });
|
|
200
|
+
const child = spawnBwrap("bwrap", buildBwrapArguments({
|
|
201
|
+
workspace: launch.policy.workspace,
|
|
202
|
+
additionalWritableRoots: launch.policy.additionalWritableRoots,
|
|
203
|
+
readOnlyPaths: primaryReadOnlyPaths,
|
|
204
|
+
readOnlyFiles: launch.readOnlyFiles ?? [],
|
|
205
|
+
readOnlyBinaries: launch.readOnlyBinaries ?? [],
|
|
206
|
+
sockets: [
|
|
207
|
+
{ hostPath: hostSocketPath, childPath: childSocketPath },
|
|
208
|
+
// Its child-side path is what `APEX_CREDENTIAL_PROXY_PATH` names.
|
|
209
|
+
...(launch.credentialChannel
|
|
210
|
+
? [
|
|
211
|
+
{
|
|
212
|
+
hostPath: launch.credentialChannel.hostSocketPath,
|
|
213
|
+
childPath: launch.credentialChannel.childSocketPath,
|
|
214
|
+
},
|
|
215
|
+
]
|
|
216
|
+
: []),
|
|
217
|
+
{
|
|
218
|
+
hostPath: gitCredentialPaths.hostSocketPath,
|
|
219
|
+
childPath: gitCredentialPaths.childSocketPath,
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
environment: {
|
|
223
|
+
HOME: launch.environment?.HOME ?? stateDirectory,
|
|
224
|
+
TMPDIR: launch.environment?.TMPDIR ?? stateDirectory,
|
|
225
|
+
APEX_UDS_PATH: childSocketPath,
|
|
226
|
+
[TERMINAL_SIZE_PATH_VARIABLE]: terminalSizePath,
|
|
227
|
+
[TERMINAL_HANDOFF_PATH_VARIABLE]: supervisorStateDirectory,
|
|
228
|
+
[TERMINAL_HANDOFF_ACK_PATH_VARIABLE]: acknowledgementPath,
|
|
229
|
+
[GIT_CREDENTIAL_SOCKET_VARIABLE]: gitCredentialPaths.childSocketPath,
|
|
230
|
+
[COMMAND_ESCALATION_SOCKET_VARIABLE]: escalationPaths.childSocketPath,
|
|
231
|
+
},
|
|
232
|
+
command: process.execPath,
|
|
233
|
+
args: [relayScriptPath, launch.command, ...launch.args],
|
|
234
|
+
}), { env: launch.environment, stdio: ["inherit", "inherit", "pipe", ...readOnlyFileDescriptors] });
|
|
216
235
|
let stderr = "";
|
|
217
236
|
child.stderr?.setEncoding("utf8");
|
|
218
237
|
child.stderr?.on("data", (chunk) => {
|
|
@@ -243,8 +262,67 @@ server.on("error", (err) => {
|
|
|
243
262
|
}
|
|
244
263
|
},
|
|
245
264
|
async close() {
|
|
265
|
+
handoff?.stop();
|
|
266
|
+
await escalationProxy?.close();
|
|
267
|
+
if (escalationDirectory)
|
|
268
|
+
rmSync(escalationDirectory, { force: true, recursive: true });
|
|
269
|
+
await gitCredentialProxy?.close();
|
|
270
|
+
if (gitCredentialDirectory)
|
|
271
|
+
rmSync(gitCredentialDirectory, { force: true, recursive: true });
|
|
246
272
|
await proxy?.close();
|
|
247
273
|
},
|
|
248
274
|
};
|
|
249
275
|
}
|
|
276
|
+
/**
|
|
277
|
+
* Start one approved command in its own child, with one extra writable root.
|
|
278
|
+
*
|
|
279
|
+
* Deliberately minimal compared with the session child: no network relay, no credential
|
|
280
|
+
* channels, no terminal handoff. An escalated command is a single shell invocation the
|
|
281
|
+
* human just read and approved, not a session, and every channel omitted here is one it
|
|
282
|
+
* cannot reach. Its stdio is captured rather than inherited so the session that asked
|
|
283
|
+
* receives the output instead of the escalated process writing over the TUI.
|
|
284
|
+
*/
|
|
285
|
+
async function runEscalatedCommand(options) {
|
|
286
|
+
const { request, launch, stateDirectory } = options;
|
|
287
|
+
const escalatedReadOnlyPaths = launch.readOnlyPaths ?? [];
|
|
288
|
+
const readOnlyFileDescriptors = [
|
|
289
|
+
...descriptorBackedReadOnlyPaths(escalatedReadOnlyPaths),
|
|
290
|
+
...(launch.readOnlyFiles ?? []),
|
|
291
|
+
].map((path) => openSync(path, "r"));
|
|
292
|
+
const child = options.spawnBwrap("bwrap", buildBwrapArguments({
|
|
293
|
+
workspace: launch.policy.workspace,
|
|
294
|
+
additionalWritableRoots: [...launch.policy.additionalWritableRoots, request.writableRoot],
|
|
295
|
+
readOnlyPaths: escalatedReadOnlyPaths,
|
|
296
|
+
readOnlyFiles: launch.readOnlyFiles ?? [],
|
|
297
|
+
readOnlyBinaries: launch.readOnlyBinaries ?? [],
|
|
298
|
+
sockets: [],
|
|
299
|
+
environment: {
|
|
300
|
+
HOME: launch.environment?.HOME ?? stateDirectory,
|
|
301
|
+
TMPDIR: launch.environment?.TMPDIR ?? stateDirectory,
|
|
302
|
+
},
|
|
303
|
+
command: "/bin/sh",
|
|
304
|
+
args: ["-c", request.command],
|
|
305
|
+
}), { stdio: ["ignore", "pipe", "pipe", ...readOnlyFileDescriptors] });
|
|
306
|
+
let stdout = "";
|
|
307
|
+
let stderr = "";
|
|
308
|
+
try {
|
|
309
|
+
child.stdout?.setEncoding("utf8");
|
|
310
|
+
child.stdout?.on("data", (chunk) => {
|
|
311
|
+
stdout += chunk;
|
|
312
|
+
});
|
|
313
|
+
child.stderr?.setEncoding("utf8");
|
|
314
|
+
child.stderr?.on("data", (chunk) => {
|
|
315
|
+
stderr += chunk;
|
|
316
|
+
});
|
|
317
|
+
const code = await new Promise((resolveCode) => {
|
|
318
|
+
child.once("error", () => resolveCode(1));
|
|
319
|
+
child.once("exit", (exitCode) => resolveCode(exitCode ?? 1));
|
|
320
|
+
});
|
|
321
|
+
return { code, stdout, stderr };
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
for (const descriptor of readOnlyFileDescriptors)
|
|
325
|
+
closeSync(descriptor);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
250
328
|
//# sourceMappingURL=linux-backend.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"linux-backend.js","sourceRoot":"","sources":["../../../src/core/sandbox/linux-backend.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn, spawnSync } from \"node:child_process\";\nimport { randomBytes } from \"node:crypto\";\nimport { closeSync, mkdirSync, openSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { createSandboxNetworkProxy, type SandboxNetworkProxy } from \"./network-proxy.ts\";\nimport type { SandboxBackend, SandboxLaunch } from \"./supervisor.ts\";\nimport { publishTerminalSize, TERMINAL_SIZE_PATH_VARIABLE } from \"./terminal-size.ts\";\nimport type { SandboxViolationStore } from \"./violations.ts\";\n\n/** AF_UNIX `sun_path` is 108 bytes on Linux, including the terminating NUL. */\nconst SUN_PATH_LIMIT = 108;\n\nexport interface LinuxSandboxBackendOptions {\n\t/** Injectable only for preflight tests; production uses the running platform. */\n\tplatform?: NodeJS.Platform;\n\tcommandExists?: (command: string) => boolean;\n\tviolationStore?: SandboxViolationStore;\n\t/** Injectable only to prove crash cleanup in tests; production spawns `bwrap` directly. */\n\tspawnChild?: typeof spawn;\n}\n\nfunction commandExists(command: string): boolean {\n\tconst result = spawnSync(command, [\"--version\"], { stdio: \"ignore\", timeout: 1_000 });\n\treturn result.status === 0;\n}\n\nfunction waitForExit(child: ReturnType<typeof spawn>): Promise<number> {\n\treturn new Promise((resolve, reject) => {\n\t\tchild.once(\"error\", reject);\n\t\tchild.once(\"exit\", (code) => resolve(code ?? 1));\n\t});\n}\n\nfunction readOnlyMountArguments(path: string, kind: \"directory\" | \"file\" = \"directory\", descriptor = 3): string[] {\n\tconst target = resolve(path);\n\tconst directory = dirname(target);\n\tconst ancestors: string[] = [];\n\tlet current = directory;\n\twhile (current !== \"/\" && current !== \"/home\") {\n\t\tancestors.push(current);\n\t\tcurrent = dirname(current);\n\t}\n\tconst parentArguments = ancestors.reverse().flatMap((ancestor) => [\"--dir\", ancestor]);\n\treturn kind === \"file\"\n\t\t? [...parentArguments, \"--tmpfs\", directory, \"--perms\", \"0400\", \"--file\", String(descriptor), target]\n\t\t: [...parentArguments, \"--ro-bind\", directory, directory];\n}\n\n/**\n * Where the supervisor's proxy socket lives on each side of the boundary.\n *\n * AF_UNIX caps `sun_path` at 108 bytes, so the socket cannot live under the workspace:\n * a deep-but-ordinary project directory exhausts that budget and the sandbox fails to\n * start at all with `listen EINVAL`. The host side therefore gets a short, unique path\n * under the system temp directory, and it is bind-mounted into the child.\n *\n * The child-side path has to sit under `/home`. That is the one writable mount at the\n * point the mountpoint is created — the sandbox root is a read-only bind, so bwrap\n * cannot create a mountpoint at `/run`, `/tmp`, or anywhere else on it.\n */\nexport function resolveProxySocketPaths(temporaryDirectory: string = tmpdir()): {\n\thostSocketPath: string;\n\tchildSocketPath: string;\n} {\n\tconst name = `apex-${process.pid}-${randomBytes(4).toString(\"hex\")}.sock`;\n\t// An unusually long TMPDIR would reintroduce the very limit this avoids.\n\tconst base = join(temporaryDirectory, name).length + 1 > SUN_PATH_LIMIT ? \"/tmp\" : temporaryDirectory;\n\treturn { hostSocketPath: join(base, name), childSocketPath: `/home/${name}` };\n}\n\n/**\n * Classify an unsuccessful child from what its stderr actually evidences, or return\n * undefined when nothing suggests the boundary refused anything.\n *\n * A non-zero exit is not a violation. An invalid API key, a failing test command, or a\n * script exiting 1 are the child's own business, and recording them as sandbox\n * violations blames the boundary for failures it had no part in — which is worse than\n * silence, because it sends whoever reads the output looking for a refusal that never\n * happened. Only Linux's own refusal wording earns a violation here; the proxy records\n * network refusals itself, before this fallback is consulted.\n */\nfunction classifySandboxFailure(stderr: string): \"filesystem\" | \"network\" | undefined {\n\tif (/Read-only file system|Permission denied|Operation not permitted/i.test(stderr)) return \"filesystem\";\n\tif (/Network is unreachable|ENETUNREACH|EHOSTUNREACH/i.test(stderr)) return \"network\";\n\treturn undefined;\n}\n\n/**\n * Linux's deliberately small platform adapter. The sandbox starts from a read-only\n * host root and bind-mounts only the canonical workspace as writable. `--unshare-net`\n * removes all direct network interfaces for the child and every descendant.\n */\nexport function createLinuxSandboxBackend(options?: LinuxSandboxBackendOptions): SandboxBackend {\n\tconst platform = options?.platform ?? process.platform;\n\tif (platform !== \"linux\") {\n\t\treturn {\n\t\t\tstatus: { kind: \"unavailable\", reason: \"OS sandbox is supported on Linux only.\" },\n\t\t\tasync launch() {\n\t\t\t\tthrow new Error(\"Linux sandbox backend is unavailable.\");\n\t\t\t},\n\t\t\tasync close() {},\n\t\t};\n\t}\n\tconst hasCommand = options?.commandExists ?? commandExists;\n\tconst violationStore = options?.violationStore;\n\tlet proxy: SandboxNetworkProxy | undefined;\n\n\tif (!hasCommand(\"bwrap\")) {\n\t\treturn {\n\t\t\tstatus: { kind: \"unavailable\", reason: \"Bubblewrap (bwrap) is required for OS sandboxing.\" },\n\t\t\tasync launch() {\n\t\t\t\tthrow new Error(\"Linux sandbox backend is unavailable.\");\n\t\t\t},\n\t\t\tasync close() {},\n\t\t};\n\t}\n\treturn {\n\t\tstatus: { kind: \"enforced\" },\n\t\tasync launch(launch: SandboxLaunch): Promise<number> {\n\t\t\tconst stateDirectory = join(launch.policy.workspace, \".apex-code\", \"sandbox-state\");\n\t\t\tmkdirSync(stateDirectory, { recursive: true });\n\n\t\t\tconst violationCountBeforeLaunch = violationStore?.totalCount ?? 0;\n\n\t\t\tconst { hostSocketPath, childSocketPath } = resolveProxySocketPaths();\n\t\t\tproxy = await createSandboxNetworkProxy({\n\t\t\t\tsocketPath: hostSocketPath,\n\t\t\t\tallowedHosts: launch.policy.allowedHosts,\n\t\t\t\tviolationStore,\n\t\t\t});\n\n\t\t\t// .cjs forces CommonJS regardless of the target workspace's package.json \"type\"\n\t\t\t// field -- a plain .js here would be parsed as ESM under \"type\": \"module\" and\n\t\t\t// crash on `require`, since Node resolves module type from the nearest package.json.\n\t\t\tconst relayScriptPath = join(stateDirectory, \"relay.cjs\");\n\t\t\twriteFileSync(\n\t\t\t\trelayScriptPath,\n\t\t\t\t`\nconst net = require(\"node:net\");\nconst child_process = require(\"node:child_process\");\n\nconst socketPath = process.env.APEX_UDS_PATH;\nconst server = net.createServer((c) => {\n\tconst client = net.connect(socketPath);\n\tc.pipe(client).pipe(c);\n\tc.on(\"error\", () => {});\n\tclient.on(\"error\", () => {});\n});\n\nserver.listen(0, \"127.0.0.1\", () => {\n\tconst port = server.address().port;\n\tconst env = Object.assign({}, process.env, {\n\t\tHTTP_PROXY: \\`http://127.0.0.1:\\${port}\\`,\n\t\tHTTPS_PROXY: \\`http://127.0.0.1:\\${port}\\`\n\t});\n\tconst args = process.argv.slice(2);\n\tconst child = child_process.spawn(args[0], args.slice(1), {\n\t\tstdio: \"inherit\",\n\t\tenv: env,\n\t});\n\tchild.on(\"exit\", (code, signal) => {\n\t\tprocess.exit(code ?? (signal ? 128 : 1));\n\t});\n});\nserver.on(\"error\", (err) => {\n\tconsole.error(\"Relay error:\", err);\n\tprocess.exit(1);\n});\n`.trim(),\n\t\t\t);\n\n\t\t\tconst readOnlyMounts = [process.execPath, launch.command, ...(launch.readOnlyPaths ?? [])].flatMap((path) =>\n\t\t\t\treadOnlyMountArguments(path),\n\t\t\t);\n\t\t\tconst readOnlyFileMounts = (launch.readOnlyFiles ?? []).flatMap((path, index) =>\n\t\t\t\treadOnlyMountArguments(path, \"file\", index + 3),\n\t\t\t);\n\t\t\tconst readOnlyFileDescriptors = (launch.readOnlyFiles ?? []).map((path) => openSync(path, \"r\"));\n\t\t\t// The descriptors above are opened before the child spawns and must be closed\n\t\t\t// on every exit path -- including a spawn/wait rejection -- or a crashed launch\n\t\t\t// leaks an open handle onto a host-owned credential file for the process's life.\n\t\t\t// The workspace is bind-mounted read-write into the sandbox, so a file\n\t\t\t// here is visible at the same absolute path on both sides of it.\n\t\t\tconst terminalSizePath = join(stateDirectory, \"terminal-size\");\n\t\t\tconst stopPublishingTerminalSize = publishTerminalSize(terminalSizePath);\n\t\t\ttry {\n\t\t\t\tconst spawnBwrap = options?.spawnChild ?? spawn;\n\t\t\t\tconst child = spawnBwrap(\n\t\t\t\t\t\"bwrap\",\n\t\t\t\t\t[\n\t\t\t\t\t\t\"--new-session\",\n\t\t\t\t\t\t\"--die-with-parent\",\n\t\t\t\t\t\t\"--unshare-user\",\n\t\t\t\t\t\t\"--unshare-pid\",\n\t\t\t\t\t\t\"--unshare-net\",\n\t\t\t\t\t\t\"--ro-bind\",\n\t\t\t\t\t\t\"/\",\n\t\t\t\t\t\t\"/\",\n\t\t\t\t\t\t\"--tmpfs\",\n\t\t\t\t\t\t\"/home\",\n\t\t\t\t\t\t// Immediately after the /home tmpfs: that is the only writable mount at\n\t\t\t\t\t\t// this point, so it is the only place bwrap can create the mountpoint.\n\t\t\t\t\t\t\"--bind\",\n\t\t\t\t\t\thostSocketPath,\n\t\t\t\t\t\tchildSocketPath,\n\t\t\t\t\t\t// The credential channel socket: same writable-under-/home constraint\n\t\t\t\t\t\t// and same position as the network socket above. Its child-side path\n\t\t\t\t\t\t// is what `APEX_CREDENTIAL_PROXY_PATH` names in the launch environment.\n\t\t\t\t\t\t...(launch.credentialChannel\n\t\t\t\t\t\t\t? [\"--bind\", launch.credentialChannel.hostSocketPath, launch.credentialChannel.childSocketPath]\n\t\t\t\t\t\t\t: []),\n\t\t\t\t\t\t...readOnlyMounts,\n\t\t\t\t\t\t...readOnlyFileMounts,\n\t\t\t\t\t\t\"--bind\",\n\t\t\t\t\t\tlaunch.policy.workspace,\n\t\t\t\t\t\tlaunch.policy.workspace,\n\t\t\t\t\t\t// After the workspace bind: these destinations sit inside it, and an\n\t\t\t\t\t\t// earlier mount would be masked when the workspace is bound over them.\n\t\t\t\t\t\t...(launch.readOnlyBinaries ?? []).flatMap(({ source, destination }) => [\n\t\t\t\t\t\t\t\"--ro-bind\",\n\t\t\t\t\t\t\tsource,\n\t\t\t\t\t\t\tdestination,\n\t\t\t\t\t\t]),\n\t\t\t\t\t\t\"--dev\",\n\t\t\t\t\t\t\"/dev\",\n\t\t\t\t\t\t\"--proc\",\n\t\t\t\t\t\t\"/proc\",\n\t\t\t\t\t\t\"--chdir\",\n\t\t\t\t\t\tlaunch.policy.workspace,\n\t\t\t\t\t\t\"--setenv\",\n\t\t\t\t\t\t\"HOME\",\n\t\t\t\t\t\tlaunch.environment?.HOME ?? stateDirectory,\n\t\t\t\t\t\t\"--setenv\",\n\t\t\t\t\t\t\"TMPDIR\",\n\t\t\t\t\t\tlaunch.environment?.TMPDIR ?? stateDirectory,\n\t\t\t\t\t\t\"--setenv\",\n\t\t\t\t\t\t\"APEX_UDS_PATH\",\n\t\t\t\t\t\tchildSocketPath,\n\t\t\t\t\t\t\"--setenv\",\n\t\t\t\t\t\tTERMINAL_SIZE_PATH_VARIABLE,\n\t\t\t\t\t\tterminalSizePath,\n\t\t\t\t\t\t\"--\",\n\t\t\t\t\t\tprocess.execPath,\n\t\t\t\t\t\trelayScriptPath,\n\t\t\t\t\t\tlaunch.command,\n\t\t\t\t\t\t...launch.args,\n\t\t\t\t\t],\n\t\t\t\t\t{ env: launch.environment, stdio: [\"inherit\", \"inherit\", \"pipe\", ...readOnlyFileDescriptors] },\n\t\t\t\t);\n\t\t\t\tlet stderr = \"\";\n\t\t\t\tchild.stderr?.setEncoding(\"utf8\");\n\t\t\t\tchild.stderr?.on(\"data\", (chunk: string) => {\n\t\t\t\t\tstderr += chunk;\n\t\t\t\t\tprocess.stderr.write(chunk);\n\t\t\t\t});\n\t\t\t\tconst exitCode = await waitForExit(child);\n\t\t\t\tawait proxy?.close();\n\t\t\t\tconst proxyAlreadyRecordedAViolation = (violationStore?.totalCount ?? 0) > violationCountBeforeLaunch;\n\t\t\t\tconst kind = exitCode !== 0 && !proxyAlreadyRecordedAViolation ? classifySandboxFailure(stderr) : undefined;\n\t\t\t\tif (kind) {\n\t\t\t\t\tviolationStore?.add({\n\t\t\t\t\t\tkind,\n\t\t\t\t\t\tcommand: [launch.command, ...launch.args].join(\" \"),\n\t\t\t\t\t\tdetail: stderr.trim(),\n\t\t\t\t\t\ttimestamp: new Date(),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn exitCode;\n\t\t\t} finally {\n\t\t\t\tstopPublishingTerminalSize();\n\t\t\t\tfor (const descriptor of readOnlyFileDescriptors) closeSync(descriptor);\n\t\t\t\t// The socket now lives outside the workspace, so nothing else will ever clean\n\t\t\t\t// it up; leaving it would litter the temp directory once per launch.\n\t\t\t\trmSync(hostSocketPath, { force: true });\n\t\t\t}\n\t\t},\n\t\tasync close() {\n\t\t\tawait proxy?.close();\n\t\t},\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"linux-backend.js","sourceRoot":"","sources":["../../../src/core/sandbox/linux-backend.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn, spawnSync } from \"node:child_process\";\nimport { randomBytes } from \"node:crypto\";\nimport { closeSync, mkdirSync, openSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { buildBwrapArguments, descriptorBackedReadOnlyPaths } from \"./bwrap-arguments.ts\";\nimport { createCommandEscalationApprover, createCredentialReleaser, createHostApprover } from \"./host-approval.ts\";\nimport { createSandboxNetworkProxy, type SandboxNetworkProxy } from \"./network-proxy.ts\";\nimport {\n\tCOMMAND_ESCALATION_SOCKET_VARIABLE,\n\ttype CommandEscalationRequest,\n\ttype CommandEscalationResult,\n\tcreateCommandEscalationProxy,\n\tresolveCommandEscalationChannelPaths,\n} from \"./rpc/command-proxy.ts\";\nimport {\n\tfillHostGitCredential,\n\tGIT_CREDENTIAL_SOCKET_VARIABLE,\n\tresolveGitCredentialChannelPaths,\n\twriteGitCredentialHelper,\n} from \"./rpc/git-credential-helper.ts\";\nimport { createGitCredentialProxy, type GitCredentialProxy } from \"./rpc/git-credential-proxy.ts\";\nimport type { SandboxBackend, SandboxLaunch } from \"./supervisor.ts\";\nimport {\n\tcreateTerminalHandoff,\n\tTERMINAL_HANDOFF_ACK_PATH_VARIABLE,\n\tTERMINAL_HANDOFF_PATH_VARIABLE,\n\ttype TerminalHandoff,\n} from \"./terminal-handoff.ts\";\nimport { publishTerminalSize, TERMINAL_SIZE_PATH_VARIABLE } from \"./terminal-size.ts\";\nimport type { SandboxViolationStore } from \"./violations.ts\";\n\n/** AF_UNIX `sun_path` is 108 bytes on Linux, including the terminating NUL. */\nconst SUN_PATH_LIMIT = 108;\n\nexport interface LinuxSandboxBackendOptions {\n\t/** Injectable only for preflight tests; production uses the running platform. */\n\tplatform?: NodeJS.Platform;\n\tcommandExists?: (command: string) => boolean;\n\tviolationStore?: SandboxViolationStore;\n\t/** Injectable only to prove crash cleanup in tests; production spawns `bwrap` directly. */\n\tspawnChild?: typeof spawn;\n\t/**\n\t * Injectable only for tests, which have no terminal and would otherwise be unable to\n\t * exercise a released credential at all. Production builds the releaser from the\n\t * terminal handoff, and its absence there is what makes a headless session refuse.\n\t */\n\trequestGitCredentialRelease?: (host: string) => Promise<boolean>;\n\t/** Injectable only for tests; production resolves against the real host store. */\n\tfillGitCredential?: typeof fillHostGitCredential;\n\t/** Injectable only for tests, which have no terminal to approve an escalation at. */\n\trequestCommandEscalation?: (request: CommandEscalationRequest) => Promise<boolean>;\n\t/** Retained for source compatibility; launch state is now required on each launch. */\n\tsupervisorStateDirectory?: string;\n}\n\nfunction commandExists(command: string): boolean {\n\tconst result = spawnSync(command, [\"--version\"], { stdio: \"ignore\", timeout: 1_000 });\n\treturn result.status === 0;\n}\n\nfunction waitForExit(child: ReturnType<typeof spawn>): Promise<number> {\n\treturn new Promise((resolve, reject) => {\n\t\tchild.once(\"error\", reject);\n\t\tchild.once(\"exit\", (code) => resolve(code ?? 1));\n\t});\n}\n\n/**\n * Where the supervisor's proxy socket lives on each side of the boundary.\n *\n * AF_UNIX caps `sun_path` at 108 bytes, so the socket cannot live under the workspace:\n * a deep-but-ordinary project directory exhausts that budget and the sandbox fails to\n * start at all with `listen EINVAL`. The host side therefore gets a short, unique path\n * under the system temp directory, and it is bind-mounted into the child.\n *\n * The child-side path has to sit under `/home`. That is the one writable mount at the\n * point the mountpoint is created — the sandbox root is a read-only bind, so bwrap\n * cannot create a mountpoint at `/run`, `/tmp`, or anywhere else on it.\n */\nexport function resolveProxySocketPaths(temporaryDirectory: string = tmpdir()): {\n\thostSocketPath: string;\n\tchildSocketPath: string;\n} {\n\tconst name = `apex-${process.pid}-${randomBytes(4).toString(\"hex\")}.sock`;\n\t// An unusually long TMPDIR would reintroduce the very limit this avoids.\n\tconst base = join(temporaryDirectory, name).length + 1 > SUN_PATH_LIMIT ? \"/tmp\" : temporaryDirectory;\n\treturn { hostSocketPath: join(base, name), childSocketPath: `/home/${name}` };\n}\n\n/**\n * Classify an unsuccessful child from what its stderr actually evidences, or return\n * undefined when nothing suggests the boundary refused anything.\n *\n * A non-zero exit is not a violation. An invalid API key, a failing test command, or a\n * script exiting 1 are the child's own business, and recording them as sandbox\n * violations blames the boundary for failures it had no part in — which is worse than\n * silence, because it sends whoever reads the output looking for a refusal that never\n * happened. Only Linux's own refusal wording earns a violation here; the proxy records\n * network refusals itself, before this fallback is consulted.\n */\nfunction classifySandboxFailure(stderr: string): \"filesystem\" | \"network\" | undefined {\n\tif (/Read-only file system|Permission denied|Operation not permitted/i.test(stderr)) return \"filesystem\";\n\tif (/Network is unreachable|ENETUNREACH|EHOSTUNREACH/i.test(stderr)) return \"network\";\n\treturn undefined;\n}\n\n/**\n * Linux's deliberately small platform adapter. The sandbox starts from a read-only\n * host root and bind-mounts only the canonical workspace as writable. `--unshare-net`\n * removes all direct network interfaces for the child and every descendant.\n */\nexport function createLinuxSandboxBackend(options?: LinuxSandboxBackendOptions): SandboxBackend {\n\tconst platform = options?.platform ?? process.platform;\n\tif (platform !== \"linux\") {\n\t\treturn {\n\t\t\tstatus: { kind: \"unavailable\", reason: \"OS sandbox is supported on Linux only.\" },\n\t\t\tasync launch() {\n\t\t\t\tthrow new Error(\"Linux sandbox backend is unavailable.\");\n\t\t\t},\n\t\t\tasync close() {},\n\t\t};\n\t}\n\tconst hasCommand = options?.commandExists ?? commandExists;\n\tconst violationStore = options?.violationStore;\n\tlet proxy: SandboxNetworkProxy | undefined;\n\tlet handoff: TerminalHandoff | undefined;\n\tlet gitCredentialProxy: GitCredentialProxy | undefined;\n\tlet gitCredentialDirectory: string | undefined;\n\tlet escalationProxy: Awaited<ReturnType<typeof createCommandEscalationProxy>> | undefined;\n\tlet escalationDirectory: string | undefined;\n\tif (!hasCommand(\"bwrap\")) {\n\t\treturn {\n\t\t\tstatus: { kind: \"unavailable\", reason: \"Bubblewrap (bwrap) is required for OS sandboxing.\" },\n\t\t\tasync launch() {\n\t\t\t\tthrow new Error(\"Linux sandbox backend is unavailable.\");\n\t\t\t},\n\t\t\tasync close() {},\n\t\t};\n\t}\n\treturn {\n\t\tstatus: { kind: \"enforced\" },\n\t\tasync launch(launch: SandboxLaunch): Promise<number> {\n\t\t\tconst supervisorStateDirectory = launch.supervisorStateDirectory;\n\t\t\tmkdirSync(supervisorStateDirectory, { recursive: true, mode: 0o700 });\n\t\t\tconst stateDirectory = launch.policy.workspace;\n\t\t\tconst acknowledgementPath = join(launch.policy.workspace, \".apex-code\", \"sandbox-handoff-ack\");\n\t\t\tmkdirSync(join(launch.policy.workspace, \".apex-code\"), { recursive: true });\n\n\t\t\tconst violationCountBeforeLaunch = violationStore?.totalCount ?? 0;\n\n\t\t\tconst { hostSocketPath, childSocketPath } = resolveProxySocketPaths();\n\t\t\thandoff = createTerminalHandoff(supervisorStateDirectory, { acknowledgementPath });\n\t\t\tconst gitCredentialPaths = resolveGitCredentialChannelPaths();\n\t\t\tgitCredentialDirectory = gitCredentialPaths.hostSocketDirectory;\n\t\t\tproxy = await createSandboxNetworkProxy({\n\t\t\t\tsocketPath: hostSocketPath,\n\t\t\t\tallowedHosts: launch.policy.allowedHosts,\n\t\t\t\tviolationStore,\n\t\t\t\t// Undefined without a terminal, which leaves the proxy's own\n\t\t\t\t// deny-without-asking path in place for headless, print, JSON, and RPC.\n\t\t\t\trequestApproval: createHostApprover({ handoff }),\n\t\t\t});\n\t\t\t// After the network proxy, because reachability is its answer to give: the\n\t\t\t// credential channel must never hand out a token for a host this session\n\t\t\t// cannot even open a connection to.\n\t\t\tgitCredentialProxy = await createGitCredentialProxy({\n\t\t\t\tsocketPath: gitCredentialPaths.hostSocketPath,\n\t\t\t\tisHostAllowed: (host) => proxy?.isHostReachable(host) ?? false,\n\t\t\t\trequestRelease: options?.requestGitCredentialRelease ?? createCredentialReleaser({ handoff }),\n\t\t\t\t// The supervisor's own environment, deliberately: this runs outside the\n\t\t\t\t// boundary and must resolve against the real host home, which is exactly\n\t\t\t\t// what the child cannot see.\n\t\t\t\tfillCredential: (request) =>\n\t\t\t\t\t(options?.fillGitCredential ?? fillHostGitCredential)(request, { environment: process.env }),\n\t\t\t\tviolationStore,\n\t\t\t});\n\t\t\twriteGitCredentialHelper(supervisorStateDirectory);\n\n\t\t\tconst escalationPaths = resolveCommandEscalationChannelPaths();\n\t\t\tescalationDirectory = escalationPaths.hostSocketDirectory;\n\n\t\t\t// .cjs forces CommonJS regardless of the target workspace's package.json \"type\"\n\t\t\t// field -- a plain .js here would be parsed as ESM under \"type\": \"module\" and\n\t\t\t// crash on `require`, since Node resolves module type from the nearest package.json.\n\t\t\tconst relayScriptPath = join(supervisorStateDirectory, \"relay.cjs\");\n\t\t\twriteFileSync(\n\t\t\t\trelayScriptPath,\n\t\t\t\t`\nconst net = require(\"node:net\");\nconst child_process = require(\"node:child_process\");\n\nconst socketPath = process.env.APEX_UDS_PATH;\nconst server = net.createServer((c) => {\n\tconst client = net.connect(socketPath);\n\tc.pipe(client).pipe(c);\n\tc.on(\"error\", () => {});\n\tclient.on(\"error\", () => {});\n});\n\nserver.listen(0, \"127.0.0.1\", () => {\n\tconst port = server.address().port;\n\tconst env = Object.assign({}, process.env, {\n\t\tHTTP_PROXY: \\`http://127.0.0.1:\\${port}\\`,\n\t\tHTTPS_PROXY: \\`http://127.0.0.1:\\${port}\\`\n\t});\n\tconst args = process.argv.slice(2);\n\tconst child = child_process.spawn(args[0], args.slice(1), {\n\t\tstdio: \"inherit\",\n\t\tenv: env,\n\t});\n\tchild.on(\"exit\", (code, signal) => {\n\t\tprocess.exit(code ?? (signal ? 128 : 1));\n\t});\n});\nserver.on(\"error\", (err) => {\n\tconsole.error(\"Relay error:\", err);\n\tprocess.exit(1);\n});\n`.trim(),\n\t\t\t);\n\n\t\t\tconst primaryReadOnlyPaths = [\n\t\t\t\tprocess.execPath,\n\t\t\t\tlaunch.command,\n\t\t\t\tsupervisorStateDirectory,\n\t\t\t\t...(launch.readOnlyPaths ?? []),\n\t\t\t];\n\t\t\tconst readOnlyFileDescriptors = [\n\t\t\t\t...descriptorBackedReadOnlyPaths(primaryReadOnlyPaths),\n\t\t\t\t...(launch.readOnlyFiles ?? []),\n\t\t\t].map((path) => openSync(path, \"r\"));\n\t\t\t// The descriptors above are opened before the child spawns and must be closed\n\t\t\t// on every exit path -- including a spawn/wait rejection -- or a crashed launch\n\t\t\t// leaks an open handle onto a host-owned credential file for the process's life.\n\t\t\t// The workspace is bind-mounted read-write into the sandbox, so a file\n\t\t\t// here is visible at the same absolute path on both sides of it.\n\t\t\tconst terminalSizePath = join(supervisorStateDirectory, \"terminal-size\");\n\t\t\tconst stopPublishingTerminalSize = publishTerminalSize(terminalSizePath);\n\n\t\t\t// The second child derives its argv from the same builder as the first, with one\n\t\t\t// extra writable root. Nothing about the original child's namespace changes.\n\t\t\tescalationProxy = await createCommandEscalationProxy({\n\t\t\t\tsocketPath: escalationPaths.hostSocketPath,\n\t\t\t\trequestApproval: options?.requestCommandEscalation ?? createCommandEscalationApprover({ handoff }),\n\t\t\t\tviolationStore,\n\t\t\t\trunEscalated: (request) =>\n\t\t\t\t\trunEscalatedCommand({\n\t\t\t\t\t\trequest,\n\t\t\t\t\t\tlaunch,\n\t\t\t\t\t\tstateDirectory,\n\t\t\t\t\t\tspawnBwrap: options?.spawnChild ?? spawn,\n\t\t\t\t\t}),\n\t\t\t});\n\t\t\ttry {\n\t\t\t\tconst spawnBwrap = options?.spawnChild ?? spawn;\n\t\t\t\tconst child = spawnBwrap(\n\t\t\t\t\t\"bwrap\",\n\t\t\t\t\tbuildBwrapArguments({\n\t\t\t\t\t\tworkspace: launch.policy.workspace,\n\t\t\t\t\t\tadditionalWritableRoots: launch.policy.additionalWritableRoots,\n\t\t\t\t\t\treadOnlyPaths: primaryReadOnlyPaths,\n\t\t\t\t\t\treadOnlyFiles: launch.readOnlyFiles ?? [],\n\t\t\t\t\t\treadOnlyBinaries: launch.readOnlyBinaries ?? [],\n\t\t\t\t\t\tsockets: [\n\t\t\t\t\t\t\t{ hostPath: hostSocketPath, childPath: childSocketPath },\n\t\t\t\t\t\t\t// Its child-side path is what `APEX_CREDENTIAL_PROXY_PATH` names.\n\t\t\t\t\t\t\t...(launch.credentialChannel\n\t\t\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\thostPath: launch.credentialChannel.hostSocketPath,\n\t\t\t\t\t\t\t\t\t\t\tchildPath: launch.credentialChannel.childSocketPath,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t: []),\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\thostPath: gitCredentialPaths.hostSocketPath,\n\t\t\t\t\t\t\t\tchildPath: gitCredentialPaths.childSocketPath,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t\tenvironment: {\n\t\t\t\t\t\t\tHOME: launch.environment?.HOME ?? stateDirectory,\n\t\t\t\t\t\t\tTMPDIR: launch.environment?.TMPDIR ?? stateDirectory,\n\t\t\t\t\t\t\tAPEX_UDS_PATH: childSocketPath,\n\t\t\t\t\t\t\t[TERMINAL_SIZE_PATH_VARIABLE]: terminalSizePath,\n\t\t\t\t\t\t\t[TERMINAL_HANDOFF_PATH_VARIABLE]: supervisorStateDirectory,\n\t\t\t\t\t\t\t[TERMINAL_HANDOFF_ACK_PATH_VARIABLE]: acknowledgementPath,\n\t\t\t\t\t\t\t[GIT_CREDENTIAL_SOCKET_VARIABLE]: gitCredentialPaths.childSocketPath,\n\t\t\t\t\t\t\t[COMMAND_ESCALATION_SOCKET_VARIABLE]: escalationPaths.childSocketPath,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tcommand: process.execPath,\n\t\t\t\t\t\targs: [relayScriptPath, launch.command, ...launch.args],\n\t\t\t\t\t}),\n\t\t\t\t\t{ env: launch.environment, stdio: [\"inherit\", \"inherit\", \"pipe\", ...readOnlyFileDescriptors] },\n\t\t\t\t);\n\t\t\t\tlet stderr = \"\";\n\t\t\t\tchild.stderr?.setEncoding(\"utf8\");\n\t\t\t\tchild.stderr?.on(\"data\", (chunk: string) => {\n\t\t\t\t\tstderr += chunk;\n\t\t\t\t\tprocess.stderr.write(chunk);\n\t\t\t\t});\n\t\t\t\tconst exitCode = await waitForExit(child);\n\t\t\t\tawait proxy?.close();\n\t\t\t\tconst proxyAlreadyRecordedAViolation = (violationStore?.totalCount ?? 0) > violationCountBeforeLaunch;\n\t\t\t\tconst kind = exitCode !== 0 && !proxyAlreadyRecordedAViolation ? classifySandboxFailure(stderr) : undefined;\n\t\t\t\tif (kind) {\n\t\t\t\t\tviolationStore?.add({\n\t\t\t\t\t\tkind,\n\t\t\t\t\t\tcommand: [launch.command, ...launch.args].join(\" \"),\n\t\t\t\t\t\tdetail: stderr.trim(),\n\t\t\t\t\t\ttimestamp: new Date(),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn exitCode;\n\t\t\t} finally {\n\t\t\t\tstopPublishingTerminalSize();\n\t\t\t\tfor (const descriptor of readOnlyFileDescriptors) closeSync(descriptor);\n\t\t\t\t// The socket now lives outside the workspace, so nothing else will ever clean\n\t\t\t\t// it up; leaving it would litter the temp directory once per launch.\n\t\t\t\trmSync(hostSocketPath, { force: true });\n\t\t\t}\n\t\t},\n\t\tasync close() {\n\t\t\thandoff?.stop();\n\t\t\tawait escalationProxy?.close();\n\t\t\tif (escalationDirectory) rmSync(escalationDirectory, { force: true, recursive: true });\n\t\t\tawait gitCredentialProxy?.close();\n\t\t\tif (gitCredentialDirectory) rmSync(gitCredentialDirectory, { force: true, recursive: true });\n\t\t\tawait proxy?.close();\n\t\t},\n\t};\n}\n\n/**\n * Start one approved command in its own child, with one extra writable root.\n *\n * Deliberately minimal compared with the session child: no network relay, no credential\n * channels, no terminal handoff. An escalated command is a single shell invocation the\n * human just read and approved, not a session, and every channel omitted here is one it\n * cannot reach. Its stdio is captured rather than inherited so the session that asked\n * receives the output instead of the escalated process writing over the TUI.\n */\nasync function runEscalatedCommand(options: {\n\trequest: CommandEscalationRequest;\n\tlaunch: SandboxLaunch;\n\tstateDirectory: string;\n\tspawnBwrap: typeof spawn;\n}): Promise<CommandEscalationResult> {\n\tconst { request, launch, stateDirectory } = options;\n\tconst escalatedReadOnlyPaths = launch.readOnlyPaths ?? [];\n\tconst readOnlyFileDescriptors = [\n\t\t...descriptorBackedReadOnlyPaths(escalatedReadOnlyPaths),\n\t\t...(launch.readOnlyFiles ?? []),\n\t].map((path) => openSync(path, \"r\"));\n\tconst child = options.spawnBwrap(\n\t\t\"bwrap\",\n\t\tbuildBwrapArguments({\n\t\t\tworkspace: launch.policy.workspace,\n\t\t\tadditionalWritableRoots: [...launch.policy.additionalWritableRoots, request.writableRoot],\n\t\t\treadOnlyPaths: escalatedReadOnlyPaths,\n\t\t\treadOnlyFiles: launch.readOnlyFiles ?? [],\n\t\t\treadOnlyBinaries: launch.readOnlyBinaries ?? [],\n\t\t\tsockets: [],\n\t\t\tenvironment: {\n\t\t\t\tHOME: launch.environment?.HOME ?? stateDirectory,\n\t\t\t\tTMPDIR: launch.environment?.TMPDIR ?? stateDirectory,\n\t\t\t},\n\t\t\tcommand: \"/bin/sh\",\n\t\t\targs: [\"-c\", request.command],\n\t\t}),\n\t\t{ stdio: [\"ignore\", \"pipe\", \"pipe\", ...readOnlyFileDescriptors] },\n\t);\n\tlet stdout = \"\";\n\tlet stderr = \"\";\n\ttry {\n\t\tchild.stdout?.setEncoding(\"utf8\");\n\t\tchild.stdout?.on(\"data\", (chunk: string) => {\n\t\t\tstdout += chunk;\n\t\t});\n\t\tchild.stderr?.setEncoding(\"utf8\");\n\t\tchild.stderr?.on(\"data\", (chunk: string) => {\n\t\t\tstderr += chunk;\n\t\t});\n\t\tconst code = await new Promise<number>((resolveCode) => {\n\t\t\tchild.once(\"error\", () => resolveCode(1));\n\t\t\tchild.once(\"exit\", (exitCode) => resolveCode(exitCode ?? 1));\n\t\t});\n\t\treturn { code, stdout, stderr };\n\t} finally {\n\t\tfor (const descriptor of readOnlyFileDescriptors) closeSync(descriptor);\n\t}\n}\n"]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { type CommandEscalationRequest } from "./rpc/command-proxy.ts";
|
|
2
3
|
import type { SandboxBackend } from "./supervisor.ts";
|
|
3
4
|
import type { SandboxViolationStore } from "./violations.ts";
|
|
4
5
|
export interface MacosSandboxBackendOptions {
|
|
@@ -12,6 +13,10 @@ export interface MacosSandboxBackendOptions {
|
|
|
12
13
|
* violation would be verifiable only on a macOS host.
|
|
13
14
|
*/
|
|
14
15
|
spawnChild?: typeof spawn;
|
|
16
|
+
/** Injectable only for tests, which have no terminal to approve an escalation at. */
|
|
17
|
+
requestCommandEscalation?: (request: CommandEscalationRequest) => Promise<boolean>;
|
|
18
|
+
/** Retained for source compatibility; launch state is now required on each launch. */
|
|
19
|
+
supervisorStateDirectory?: string;
|
|
15
20
|
}
|
|
16
21
|
/**
|
|
17
22
|
* macOS's platform adapter. Seatbelt (`sandbox-exec`) allows reads broadly except
|