apex-code 0.0.1-alpha.9 → 0.0.5
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 +150 -2
- package/README.md +9 -8
- package/dist/cli/args.d.ts +25 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +43 -1
- package/dist/cli/args.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.map +1 -1
- package/dist/config.js +24 -5
- 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 +157 -8
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +565 -22
- package/dist/core/agent-session.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 +4 -3
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +5 -0
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +19 -5
- package/dist/core/compaction/compaction.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/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 +1 -1
- package/dist/core/extensions/index.d.ts.map +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 +8 -2
- 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 +22 -2
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.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/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/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/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/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 +22 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +53 -1
- package/dist/core/sdk.js.map +1 -1
- 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 +32 -5
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +163 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +107 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +2 -0
- package/dist/core/slash-commands.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 +30 -15
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +228 -12
- 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 -0
- package/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/core/tools/edit-diff.js +215 -3
- 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 +53 -7
- 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 +10 -1
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +3 -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/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/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/provider.d.ts +3 -1
- package/dist/extensions/llama/provider.d.ts.map +1 -1
- package/dist/extensions/llama/provider.js +27 -6
- package/dist/extensions/llama/provider.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +3 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +41 -9
- package/dist/main.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.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +6 -6
- 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 +2 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +10 -0
- package/dist/modes/interactive/components/settings-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/interactive-mode.d.ts +34 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +231 -26
- 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 +15 -5
- package/dist/modes/interactive/theme/theme.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.map +1 -1
- package/dist/package-manager-cli.js +0 -6
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +6 -4
- package/dist/utils/shell.js.map +1 -1
- 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 +4 -0
- package/docs/custom-provider.md +7 -7
- package/docs/environment-variables.md +1 -1
- package/docs/extensions.md +18 -0
- package/docs/keybindings.md +1 -1
- package/docs/quickstart.md +1 -1
- package/docs/rpc.md +23 -0
- package/docs/sdk.md +25 -0
- package/docs/settings.md +12 -4
- package/docs/terminal-setup.md +12 -0
- package/docs/usage.md +4 -3
- 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/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/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +194 -46
- package/package.json +14 -12
- 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
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { supervisorTempDirectory } from "../supervisor-temp.js";
|
|
4
|
+
import { parseGitCredentialRequest } from "./git-credential-proxy.js";
|
|
5
|
+
/** Env var naming the child-side socket the helper talks to. */
|
|
6
|
+
export const GIT_CREDENTIAL_SOCKET_VARIABLE = "APEX_GIT_CREDENTIAL_PATH";
|
|
7
|
+
/**
|
|
8
|
+
* The credential helper git runs inside the sandbox.
|
|
9
|
+
*
|
|
10
|
+
* `.cjs` for the same reason the network relay is: a plain `.js` under a workspace whose
|
|
11
|
+
* package.json declares `"type": "module"` is parsed as ESM and dies on `require`.
|
|
12
|
+
*
|
|
13
|
+
* It answers `get` and nothing else. `store` and `erase` exit silently and successfully,
|
|
14
|
+
* which is what tells git the credential was handled and stops it falling through to
|
|
15
|
+
* another helper. Writing is deliberately not served: ADR 0015 keeps credential mutation
|
|
16
|
+
* an explicit host operation, and a channel that let the child rewrite the host's store
|
|
17
|
+
* would be a way out of the boundary rather than a way to work inside it.
|
|
18
|
+
*
|
|
19
|
+
* Any failure exits 0 having printed nothing, which git reads as "this helper has no
|
|
20
|
+
* credential" and handles normally. Exiting non-zero would surface as a git error that
|
|
21
|
+
* looks like a repository problem rather than a refused release.
|
|
22
|
+
*/
|
|
23
|
+
const HELPER_SOURCE = `
|
|
24
|
+
const net = require("node:net");
|
|
25
|
+
|
|
26
|
+
const operation = process.argv[2];
|
|
27
|
+
if (operation !== "get") process.exit(0);
|
|
28
|
+
|
|
29
|
+
let input = "";
|
|
30
|
+
process.stdin.setEncoding("utf8");
|
|
31
|
+
process.stdin.on("data", (chunk) => {
|
|
32
|
+
input += chunk;
|
|
33
|
+
});
|
|
34
|
+
process.stdin.on("end", () => {
|
|
35
|
+
const request = {};
|
|
36
|
+
for (const line of input.split("\\n")) {
|
|
37
|
+
const separator = line.indexOf("=");
|
|
38
|
+
if (separator > 0) request[line.slice(0, separator)] = line.slice(separator + 1).trim();
|
|
39
|
+
}
|
|
40
|
+
const socketPath = process.env.${GIT_CREDENTIAL_SOCKET_VARIABLE};
|
|
41
|
+
if (!socketPath || !request.host) process.exit(0);
|
|
42
|
+
|
|
43
|
+
const socket = net.connect(socketPath);
|
|
44
|
+
let buffer = "";
|
|
45
|
+
const giveUp = () => {
|
|
46
|
+
socket.destroy();
|
|
47
|
+
process.exit(0);
|
|
48
|
+
};
|
|
49
|
+
socket.on("connect", () => {
|
|
50
|
+
socket.write(JSON.stringify({ op: "get", protocol: request.protocol || "https", host: request.host }) + "\\n");
|
|
51
|
+
});
|
|
52
|
+
socket.on("data", (chunk) => {
|
|
53
|
+
buffer += chunk.toString();
|
|
54
|
+
const newline = buffer.indexOf("\\n");
|
|
55
|
+
if (newline < 0) return;
|
|
56
|
+
let response;
|
|
57
|
+
try {
|
|
58
|
+
response = JSON.parse(buffer.slice(0, newline));
|
|
59
|
+
} catch {
|
|
60
|
+
giveUp();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (response && response.ok) {
|
|
64
|
+
process.stdout.write("username=" + response.username + "\\n");
|
|
65
|
+
process.stdout.write("password=" + response.password + "\\n");
|
|
66
|
+
}
|
|
67
|
+
socket.destroy();
|
|
68
|
+
process.exit(0);
|
|
69
|
+
});
|
|
70
|
+
socket.on("error", giveUp);
|
|
71
|
+
});
|
|
72
|
+
`.trim();
|
|
73
|
+
/** AF_UNIX `sun_path` is 108 bytes on Linux, including the terminating NUL. */
|
|
74
|
+
const SUN_PATH_LIMIT = 108;
|
|
75
|
+
/**
|
|
76
|
+
* Where the git credential socket lives on each side, mirroring the credential channel's
|
|
77
|
+
* own resolution. TMPDIR is deliberately not honoured: it can point inside the workspace
|
|
78
|
+
* on macOS, which the sandbox may write. The child-side path sits under `/home` on Linux
|
|
79
|
+
* because the `--tmpfs /home` is the only writable mount when bwrap creates the mountpoint.
|
|
80
|
+
*/
|
|
81
|
+
export function resolveGitCredentialChannelPaths() {
|
|
82
|
+
const hostSocketDirectory = mkdtempSync(join(supervisorTempDirectory(), `apex-gitcred-${process.pid}-`), {
|
|
83
|
+
encoding: "utf8",
|
|
84
|
+
});
|
|
85
|
+
chmodSync(hostSocketDirectory, 0o700);
|
|
86
|
+
const hostSocketPath = join(hostSocketDirectory, "channel.sock");
|
|
87
|
+
if (Buffer.byteLength(hostSocketPath) + 1 > SUN_PATH_LIMIT) {
|
|
88
|
+
throw new Error("Git credential channel path exceeds the Unix socket path limit.");
|
|
89
|
+
}
|
|
90
|
+
const childSocketPath = process.platform === "linux" ? "/home/apex-git-credential-channel.sock" : hostSocketPath;
|
|
91
|
+
return { hostSocketDirectory, hostSocketPath, childSocketPath };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Where the helper lives, derived rather than passed.
|
|
95
|
+
*
|
|
96
|
+
* The supervisor writes `credential.helper` into the projected git config before the
|
|
97
|
+
* platform backend exists, and the backend writes the file itself. Deriving both from the
|
|
98
|
+
* state directory is what keeps the config from naming a path nothing ever created.
|
|
99
|
+
*/
|
|
100
|
+
export function gitCredentialHelperPath(stateDirectory) {
|
|
101
|
+
return join(stateDirectory, "git-credential-apex.cjs");
|
|
102
|
+
}
|
|
103
|
+
/** The `credential.helper` value naming that helper, run under the supervisor's own node. */
|
|
104
|
+
export function gitCredentialHelperCommand(stateDirectory, nodePath = process.execPath) {
|
|
105
|
+
return `!"${nodePath}" "${gitCredentialHelperPath(stateDirectory)}"`;
|
|
106
|
+
}
|
|
107
|
+
/** Write the helper into a directory the child can read, returning its path. */
|
|
108
|
+
export function writeGitCredentialHelper(directory) {
|
|
109
|
+
const path = gitCredentialHelperPath(directory);
|
|
110
|
+
writeFileSync(path, HELPER_SOURCE);
|
|
111
|
+
return path;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The environment variables that decide which repository git is standing in, and which
|
|
115
|
+
* config it reads.
|
|
116
|
+
*
|
|
117
|
+
* `credential.helper` is a shell command git runs on whichever machine resolves the
|
|
118
|
+
* credential -- the host, here. Any of these four lets a repository the agent cloned choose
|
|
119
|
+
* that command, so a credential fill that inherits them is a host execution primitive with
|
|
120
|
+
* a socket in front of it.
|
|
121
|
+
*
|
|
122
|
+
* `HOME` and the global and system config are deliberately kept. They are how `gh`,
|
|
123
|
+
* libsecret, the macOS keychain and a plain `credential.helper` in `--global` are found,
|
|
124
|
+
* and they belong to the human running the supervisor rather than to any repository.
|
|
125
|
+
*/
|
|
126
|
+
const REPOSITORY_DISCOVERY_VARIABLES = ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_CONFIG"];
|
|
127
|
+
function createPrivateFillDirectory() {
|
|
128
|
+
const base = mkdtempSync(join(supervisorTempDirectory(), `apex-gitcred-fill-${process.pid}-`), { encoding: "utf8" });
|
|
129
|
+
chmodSync(base, 0o700);
|
|
130
|
+
const cwd = join(base, "empty");
|
|
131
|
+
mkdirSync(cwd, { mode: 0o700 });
|
|
132
|
+
// Resolved, because git ignores a ceiling entry that would need symlink resolution and
|
|
133
|
+
// `/tmp` is a symlink on macOS.
|
|
134
|
+
let ceiling = base;
|
|
135
|
+
try {
|
|
136
|
+
ceiling = realpathSync(base);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// A ceiling that does not resolve is a weaker second guard, not a broken fill; the
|
|
140
|
+
// empty cwd is what does the work.
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
cwd,
|
|
144
|
+
ceiling,
|
|
145
|
+
dispose: () => {
|
|
146
|
+
try {
|
|
147
|
+
rmSync(base, { force: true, recursive: true });
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// A leftover empty directory under the supervisor's own temp root is not worth
|
|
151
|
+
// failing a credential fill over.
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Resolve a credential on the host, through the host's own git configuration.
|
|
158
|
+
*
|
|
159
|
+
* `git credential fill` rather than reading any particular store, because the answer may
|
|
160
|
+
* come from `gh`'s helper, libsecret, the macOS keychain, or a plain file, and only git
|
|
161
|
+
* knows which of those this host is configured to ask. Reimplementing that resolution
|
|
162
|
+
* would be a second, quietly diverging copy of it.
|
|
163
|
+
*
|
|
164
|
+
* Two things about *where* it runs are part of the boundary rather than housekeeping. It
|
|
165
|
+
* runs in a private empty directory, never in whatever directory the supervisor happened to
|
|
166
|
+
* be started from, because a repository's own `credential.helper` is a command git would
|
|
167
|
+
* otherwise execute on the host. And it runs without the four variables that would point it
|
|
168
|
+
* back at such a repository. The request is re-parsed here too: this function serializes
|
|
169
|
+
* into git's line protocol, so it refuses anything that could rewrite the identity it was
|
|
170
|
+
* asked for, whether or not the proxy already checked.
|
|
171
|
+
*/
|
|
172
|
+
export async function fillHostGitCredential(request, options) {
|
|
173
|
+
const parsed = parseGitCredentialRequest({ protocol: request.protocol, host: request.host });
|
|
174
|
+
if (!parsed.ok)
|
|
175
|
+
return undefined;
|
|
176
|
+
const identity = parsed.request;
|
|
177
|
+
const { spawn } = await import("node:child_process");
|
|
178
|
+
const directory = createPrivateFillDirectory();
|
|
179
|
+
const environment = { ...(options?.environment ?? process.env) };
|
|
180
|
+
for (const variable of REPOSITORY_DISCOVERY_VARIABLES)
|
|
181
|
+
delete environment[variable];
|
|
182
|
+
environment.GIT_CEILING_DIRECTORIES = directory.ceiling;
|
|
183
|
+
return new Promise((resolve) => {
|
|
184
|
+
let settled = false;
|
|
185
|
+
const finish = (credential) => {
|
|
186
|
+
if (settled)
|
|
187
|
+
return;
|
|
188
|
+
settled = true;
|
|
189
|
+
directory.dispose();
|
|
190
|
+
resolve(credential);
|
|
191
|
+
};
|
|
192
|
+
const child = spawn("git", ["credential", "fill"], {
|
|
193
|
+
cwd: directory.cwd,
|
|
194
|
+
env: environment,
|
|
195
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
196
|
+
});
|
|
197
|
+
let stdout = "";
|
|
198
|
+
child.stdout.setEncoding("utf8");
|
|
199
|
+
child.stdout.on("data", (chunk) => {
|
|
200
|
+
stdout += chunk;
|
|
201
|
+
});
|
|
202
|
+
child.on("error", () => finish(undefined));
|
|
203
|
+
child.on("close", (code) => {
|
|
204
|
+
if (code !== 0) {
|
|
205
|
+
finish(undefined);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const fields = new Map();
|
|
209
|
+
for (const line of stdout.split("\n")) {
|
|
210
|
+
const separator = line.indexOf("=");
|
|
211
|
+
if (separator > 0)
|
|
212
|
+
fields.set(line.slice(0, separator), line.slice(separator + 1).trim());
|
|
213
|
+
}
|
|
214
|
+
const username = fields.get("username");
|
|
215
|
+
const password = fields.get("password");
|
|
216
|
+
finish(username && password ? { username, password } : undefined);
|
|
217
|
+
});
|
|
218
|
+
child.stdin.on("error", () => finish(undefined));
|
|
219
|
+
child.stdin.end(`protocol=${identity.protocol}\nhost=${identity.host}\n\n`);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=git-credential-helper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-credential-helper.js","sourceRoot":"","sources":["../../../../src/core/sandbox/rpc/git-credential-helper.ts"],"names":[],"mappings":"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","sourcesContent":["import { chmodSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { supervisorTempDirectory } from \"../supervisor-temp.ts\";\nimport { type GitCredential, type GitCredentialRequest, parseGitCredentialRequest } from \"./git-credential-proxy.ts\";\n\n/** Env var naming the child-side socket the helper talks to. */\nexport const GIT_CREDENTIAL_SOCKET_VARIABLE = \"APEX_GIT_CREDENTIAL_PATH\";\n\n/**\n * The credential helper git runs inside the sandbox.\n *\n * `.cjs` for the same reason the network relay is: a plain `.js` under a workspace whose\n * package.json declares `\"type\": \"module\"` is parsed as ESM and dies on `require`.\n *\n * It answers `get` and nothing else. `store` and `erase` exit silently and successfully,\n * which is what tells git the credential was handled and stops it falling through to\n * another helper. Writing is deliberately not served: ADR 0015 keeps credential mutation\n * an explicit host operation, and a channel that let the child rewrite the host's store\n * would be a way out of the boundary rather than a way to work inside it.\n *\n * Any failure exits 0 having printed nothing, which git reads as \"this helper has no\n * credential\" and handles normally. Exiting non-zero would surface as a git error that\n * looks like a repository problem rather than a refused release.\n */\nconst HELPER_SOURCE = `\nconst net = require(\"node:net\");\n\nconst operation = process.argv[2];\nif (operation !== \"get\") process.exit(0);\n\nlet input = \"\";\nprocess.stdin.setEncoding(\"utf8\");\nprocess.stdin.on(\"data\", (chunk) => {\n\tinput += chunk;\n});\nprocess.stdin.on(\"end\", () => {\n\tconst request = {};\n\tfor (const line of input.split(\"\\\\n\")) {\n\t\tconst separator = line.indexOf(\"=\");\n\t\tif (separator > 0) request[line.slice(0, separator)] = line.slice(separator + 1).trim();\n\t}\n\tconst socketPath = process.env.${GIT_CREDENTIAL_SOCKET_VARIABLE};\n\tif (!socketPath || !request.host) process.exit(0);\n\n\tconst socket = net.connect(socketPath);\n\tlet buffer = \"\";\n\tconst giveUp = () => {\n\t\tsocket.destroy();\n\t\tprocess.exit(0);\n\t};\n\tsocket.on(\"connect\", () => {\n\t\tsocket.write(JSON.stringify({ op: \"get\", protocol: request.protocol || \"https\", host: request.host }) + \"\\\\n\");\n\t});\n\tsocket.on(\"data\", (chunk) => {\n\t\tbuffer += chunk.toString();\n\t\tconst newline = buffer.indexOf(\"\\\\n\");\n\t\tif (newline < 0) return;\n\t\tlet response;\n\t\ttry {\n\t\t\tresponse = JSON.parse(buffer.slice(0, newline));\n\t\t} catch {\n\t\t\tgiveUp();\n\t\t\treturn;\n\t\t}\n\t\tif (response && response.ok) {\n\t\t\tprocess.stdout.write(\"username=\" + response.username + \"\\\\n\");\n\t\t\tprocess.stdout.write(\"password=\" + response.password + \"\\\\n\");\n\t\t}\n\t\tsocket.destroy();\n\t\tprocess.exit(0);\n\t});\n\tsocket.on(\"error\", giveUp);\n});\n`.trim();\n\n/** AF_UNIX `sun_path` is 108 bytes on Linux, including the terminating NUL. */\nconst SUN_PATH_LIMIT = 108;\n\nexport interface GitCredentialChannelPaths {\n\treadonly hostSocketDirectory: string;\n\treadonly hostSocketPath: string;\n\treadonly childSocketPath: string;\n}\n\n/**\n * Where the git credential socket lives on each side, mirroring the credential channel's\n * own resolution. TMPDIR is deliberately not honoured: it can point inside the workspace\n * on macOS, which the sandbox may write. The child-side path sits under `/home` on Linux\n * because the `--tmpfs /home` is the only writable mount when bwrap creates the mountpoint.\n */\nexport function resolveGitCredentialChannelPaths(): GitCredentialChannelPaths {\n\tconst hostSocketDirectory = mkdtempSync(join(supervisorTempDirectory(), `apex-gitcred-${process.pid}-`), {\n\t\tencoding: \"utf8\",\n\t});\n\tchmodSync(hostSocketDirectory, 0o700);\n\tconst hostSocketPath = join(hostSocketDirectory, \"channel.sock\");\n\tif (Buffer.byteLength(hostSocketPath) + 1 > SUN_PATH_LIMIT) {\n\t\tthrow new Error(\"Git credential channel path exceeds the Unix socket path limit.\");\n\t}\n\tconst childSocketPath = process.platform === \"linux\" ? \"/home/apex-git-credential-channel.sock\" : hostSocketPath;\n\treturn { hostSocketDirectory, hostSocketPath, childSocketPath };\n}\n\n/**\n * Where the helper lives, derived rather than passed.\n *\n * The supervisor writes `credential.helper` into the projected git config before the\n * platform backend exists, and the backend writes the file itself. Deriving both from the\n * state directory is what keeps the config from naming a path nothing ever created.\n */\nexport function gitCredentialHelperPath(stateDirectory: string): string {\n\treturn join(stateDirectory, \"git-credential-apex.cjs\");\n}\n\n/** The `credential.helper` value naming that helper, run under the supervisor's own node. */\nexport function gitCredentialHelperCommand(stateDirectory: string, nodePath = process.execPath): string {\n\treturn `!\"${nodePath}\" \"${gitCredentialHelperPath(stateDirectory)}\"`;\n}\n\n/** Write the helper into a directory the child can read, returning its path. */\nexport function writeGitCredentialHelper(directory: string): string {\n\tconst path = gitCredentialHelperPath(directory);\n\twriteFileSync(path, HELPER_SOURCE);\n\treturn path;\n}\n\n/**\n * The environment variables that decide which repository git is standing in, and which\n * config it reads.\n *\n * `credential.helper` is a shell command git runs on whichever machine resolves the\n * credential -- the host, here. Any of these four lets a repository the agent cloned choose\n * that command, so a credential fill that inherits them is a host execution primitive with\n * a socket in front of it.\n *\n * `HOME` and the global and system config are deliberately kept. They are how `gh`,\n * libsecret, the macOS keychain and a plain `credential.helper` in `--global` are found,\n * and they belong to the human running the supervisor rather than to any repository.\n */\nconst REPOSITORY_DISCOVERY_VARIABLES = [\"GIT_DIR\", \"GIT_WORK_TREE\", \"GIT_INDEX_FILE\", \"GIT_CONFIG\"] as const;\n\ninterface PrivateFillDirectory {\n\t/** Where git runs: empty, 0700, and inside no repository. */\n\treadonly cwd: string;\n\t/** Its parent, so git's upward search for a repository stops before leaving it. */\n\treadonly ceiling: string;\n\treadonly dispose: () => void;\n}\n\nfunction createPrivateFillDirectory(): PrivateFillDirectory {\n\tconst base = mkdtempSync(join(supervisorTempDirectory(), `apex-gitcred-fill-${process.pid}-`), { encoding: \"utf8\" });\n\tchmodSync(base, 0o700);\n\tconst cwd = join(base, \"empty\");\n\tmkdirSync(cwd, { mode: 0o700 });\n\t// Resolved, because git ignores a ceiling entry that would need symlink resolution and\n\t// `/tmp` is a symlink on macOS.\n\tlet ceiling = base;\n\ttry {\n\t\tceiling = realpathSync(base);\n\t} catch {\n\t\t// A ceiling that does not resolve is a weaker second guard, not a broken fill; the\n\t\t// empty cwd is what does the work.\n\t}\n\treturn {\n\t\tcwd,\n\t\tceiling,\n\t\tdispose: () => {\n\t\t\ttry {\n\t\t\t\trmSync(base, { force: true, recursive: true });\n\t\t\t} catch {\n\t\t\t\t// A leftover empty directory under the supervisor's own temp root is not worth\n\t\t\t\t// failing a credential fill over.\n\t\t\t}\n\t\t},\n\t};\n}\n\n/**\n * Resolve a credential on the host, through the host's own git configuration.\n *\n * `git credential fill` rather than reading any particular store, because the answer may\n * come from `gh`'s helper, libsecret, the macOS keychain, or a plain file, and only git\n * knows which of those this host is configured to ask. Reimplementing that resolution\n * would be a second, quietly diverging copy of it.\n *\n * Two things about *where* it runs are part of the boundary rather than housekeeping. It\n * runs in a private empty directory, never in whatever directory the supervisor happened to\n * be started from, because a repository's own `credential.helper` is a command git would\n * otherwise execute on the host. And it runs without the four variables that would point it\n * back at such a repository. The request is re-parsed here too: this function serializes\n * into git's line protocol, so it refuses anything that could rewrite the identity it was\n * asked for, whether or not the proxy already checked.\n */\nexport async function fillHostGitCredential(\n\trequest: GitCredentialRequest,\n\toptions?: { environment?: NodeJS.ProcessEnv },\n): Promise<GitCredential | undefined> {\n\tconst parsed = parseGitCredentialRequest({ protocol: request.protocol, host: request.host });\n\tif (!parsed.ok) return undefined;\n\tconst identity = parsed.request;\n\n\tconst { spawn } = await import(\"node:child_process\");\n\tconst directory = createPrivateFillDirectory();\n\tconst environment: NodeJS.ProcessEnv = { ...(options?.environment ?? process.env) };\n\tfor (const variable of REPOSITORY_DISCOVERY_VARIABLES) delete environment[variable];\n\tenvironment.GIT_CEILING_DIRECTORIES = directory.ceiling;\n\n\treturn new Promise((resolve) => {\n\t\tlet settled = false;\n\t\tconst finish = (credential: GitCredential | undefined): void => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tdirectory.dispose();\n\t\t\tresolve(credential);\n\t\t};\n\t\tconst child = spawn(\"git\", [\"credential\", \"fill\"], {\n\t\t\tcwd: directory.cwd,\n\t\t\tenv: environment,\n\t\t\tstdio: [\"pipe\", \"pipe\", \"ignore\"],\n\t\t});\n\t\tlet stdout = \"\";\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.on(\"error\", () => finish(undefined));\n\t\tchild.on(\"close\", (code) => {\n\t\t\tif (code !== 0) {\n\t\t\t\tfinish(undefined);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst fields = new Map<string, string>();\n\t\t\tfor (const line of stdout.split(\"\\n\")) {\n\t\t\t\tconst separator = line.indexOf(\"=\");\n\t\t\t\tif (separator > 0) fields.set(line.slice(0, separator), line.slice(separator + 1).trim());\n\t\t\t}\n\t\t\tconst username = fields.get(\"username\");\n\t\t\tconst password = fields.get(\"password\");\n\t\t\tfinish(username && password ? { username, password } : undefined);\n\t\t});\n\t\tchild.stdin.on(\"error\", () => finish(undefined));\n\t\tchild.stdin.end(`protocol=${identity.protocol}\\nhost=${identity.host}\\n\\n`);\n\t});\n}\n"]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { SandboxViolationStore } from "../violations.ts";
|
|
2
|
+
export interface GitCredentialRequest {
|
|
3
|
+
readonly protocol: string;
|
|
4
|
+
readonly host: string;
|
|
5
|
+
}
|
|
6
|
+
export interface GitCredential {
|
|
7
|
+
readonly username: string;
|
|
8
|
+
readonly password: string;
|
|
9
|
+
}
|
|
10
|
+
export interface GitCredentialProxy {
|
|
11
|
+
close(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
/** A request that survived validation, or the reason it did not. */
|
|
14
|
+
export type GitCredentialRequestParse = {
|
|
15
|
+
readonly ok: true;
|
|
16
|
+
readonly request: GitCredentialRequest;
|
|
17
|
+
} | {
|
|
18
|
+
readonly ok: false;
|
|
19
|
+
readonly error: string;
|
|
20
|
+
readonly audit: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Parse one credential request into the single structured identity everything downstream
|
|
24
|
+
* uses.
|
|
25
|
+
*
|
|
26
|
+
* The caller authorizes, releases, and serializes *this* value and never the raw fields,
|
|
27
|
+
* because the defect this closes is precisely that the string checked against the release
|
|
28
|
+
* was not the string handed to git.
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseGitCredentialRequest(request: Record<string, unknown>): GitCredentialRequestParse;
|
|
31
|
+
/**
|
|
32
|
+
* Serve git credentials to the sandboxed child without ever putting one inside it.
|
|
33
|
+
*
|
|
34
|
+
* ADR 0015 keeps credentials host-owned and hands the child a read-only projection. A git
|
|
35
|
+
* credential cannot be projected the same way: it is not one file with a stable shape, it
|
|
36
|
+
* is whatever the host's own helper produces per host. So the credential stays on the host
|
|
37
|
+
* and only the answer to one question crosses, for one host, once it has been released.
|
|
38
|
+
*
|
|
39
|
+
* Two gates, and the first is the one that matters most.
|
|
40
|
+
*
|
|
41
|
+
* The host must be reachable by this session. git only asks for a credential after a
|
|
42
|
+
* server challenged it, so in the ordinary flow the host was already allowed or approved
|
|
43
|
+
* at the network layer. A request for a host the session cannot reach is therefore not git
|
|
44
|
+
* doing its job; it is something that went looking for a token. Refusing it costs nothing
|
|
45
|
+
* real and removes the case where this channel is more useful to an attacker than to git.
|
|
46
|
+
*
|
|
47
|
+
* The human must release it. Per ADR 0023 that decision is the supervisor's, because this
|
|
48
|
+
* socket has no peer authentication -- every descendant in the child's namespace can reach
|
|
49
|
+
* it, which is precisely the code the boundary exists to contain. Without a releaser the
|
|
50
|
+
* channel refuses outright, so a headless session cannot hand out a credential at all.
|
|
51
|
+
*
|
|
52
|
+
* A release covers one host for the session. It is never persisted and never widened to a
|
|
53
|
+
* second host, which is what keeps an approval for a push from also releasing a token to
|
|
54
|
+
* whatever else the session later contacts.
|
|
55
|
+
*/
|
|
56
|
+
export declare function createGitCredentialProxy(options: {
|
|
57
|
+
socketPath: string;
|
|
58
|
+
/** Whether this session may reach the host at all. */
|
|
59
|
+
isHostAllowed: (host: string) => boolean;
|
|
60
|
+
/** Ask a human to release the host's credential. Absent means refuse without asking. */
|
|
61
|
+
requestRelease?: (host: string) => Promise<boolean>;
|
|
62
|
+
/** Resolve the credential on the host, using the host's own configuration. */
|
|
63
|
+
fillCredential: (request: GitCredentialRequest) => Promise<GitCredential | undefined>;
|
|
64
|
+
violationStore?: SandboxViolationStore;
|
|
65
|
+
}): Promise<GitCredentialProxy>;
|
|
66
|
+
//# sourceMappingURL=git-credential-proxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-credential-proxy.d.ts","sourceRoot":"","sources":["../../../../src/core/sandbox/rpc/git-credential-proxy.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAK9D,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IAClC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,oEAAoE;AACpE,MAAM,MAAM,yBAAyB,GAClC;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,oBAAoB,CAAA;CAAE,GAC7D;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAsB1E;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,yBAAyB,CAoCrG;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IACzC,wFAAwF;IACxF,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACpD,8EAA8E;IAC9E,cAAc,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAAC;IACtF,cAAc,CAAC,EAAE,qBAAqB,CAAC;CACvC,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAkH9B","sourcesContent":["import { chmodSync, existsSync, unlinkSync } from \"node:fs\";\nimport * as net from \"node:net\";\nimport type { SandboxViolationStore } from \"../violations.ts\";\nimport { FrameReader, isRequestObject, writeFrame } from \"./framing.ts\";\n\nconst MAX_CONNECTIONS = 32;\n\nexport interface GitCredentialRequest {\n\treadonly protocol: string;\n\treadonly host: string;\n}\n\nexport interface GitCredential {\n\treadonly username: string;\n\treadonly password: string;\n}\n\nexport interface GitCredentialProxy {\n\tclose(): Promise<void>;\n}\n\n/** A request that survived validation, or the reason it did not. */\nexport type GitCredentialRequestParse =\n\t| { readonly ok: true; readonly request: GitCredentialRequest }\n\t| { readonly ok: false; readonly error: string; readonly audit: string };\n\n/**\n * git's credential protocol is `key=value` lines terminated by a blank line, so any byte\n * that can end a line or a field can rewrite the request downstream of this point. A\n * `protocol` of `https\\nhost=other.invalid\\n\\n` is authorized as one identity and served as\n * another; the same is true of a carriage return, a NUL, or a bare space.\n */\nconst LINE_OR_FIELD_BREAKING = /[\\s\\u0000-\\u001f\\u007f-\\u009f]/u;\n\n/**\n * Structural, not an allowlist. Hardcoding `https` would refuse `ssh` and every custom\n * scheme a host helper answers for, which is a working feature rather than an attack. What\n * is refused is anything that is not a scheme: RFC 3986's shape, lowercased first so that\n * the released identity and the served one cannot differ by case.\n */\nconst SCHEME_SHAPE = /^[a-z][a-z0-9+.-]*$/;\n\n/** Bounds, so a frame that is within the reader's byte limit is still not an essay. */\nconst MAX_HOST_LENGTH = 255;\nconst MAX_PROTOCOL_LENGTH = 32;\n\n/**\n * Parse one credential request into the single structured identity everything downstream\n * uses.\n *\n * The caller authorizes, releases, and serializes *this* value and never the raw fields,\n * because the defect this closes is precisely that the string checked against the release\n * was not the string handed to git.\n */\nexport function parseGitCredentialRequest(request: Record<string, unknown>): GitCredentialRequestParse {\n\tconst rawProtocol = request.protocol;\n\t// git's own helper always sends one; an absent field means \"whatever git defaults to\",\n\t// and https is what this channel has always assumed. A present non-string is a client\n\t// that is not git.\n\tif (rawProtocol !== undefined && typeof rawProtocol !== \"string\") {\n\t\treturn { ok: false, error: \"Request protocol was not a string.\", audit: \"protocol field was not a string\" };\n\t}\n\tif (typeof request.host !== \"string\") {\n\t\treturn { ok: false, error: \"Request named no host.\", audit: \"host field was absent or not a string\" };\n\t}\n\n\tconst protocol = (rawProtocol ?? \"https\").toLowerCase();\n\tconst host = request.host;\n\tif (!host) return { ok: false, error: \"Request named no host.\", audit: \"host field was empty\" };\n\tif (host.length > MAX_HOST_LENGTH) {\n\t\treturn { ok: false, error: \"Request host is too long.\", audit: \"host field exceeded its length bound\" };\n\t}\n\tif (LINE_OR_FIELD_BREAKING.test(host)) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: \"Request host contains a control or whitespace character.\",\n\t\t\taudit: \"host field carried a control or whitespace character\",\n\t\t};\n\t}\n\tif (protocol.length > MAX_PROTOCOL_LENGTH) {\n\t\treturn { ok: false, error: \"Request protocol is too long.\", audit: \"protocol field exceeded its length bound\" };\n\t}\n\tif (!SCHEME_SHAPE.test(protocol)) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: \"Request protocol is not a scheme.\",\n\t\t\taudit: \"protocol field was not a scheme\",\n\t\t};\n\t}\n\treturn { ok: true, request: { protocol, host } };\n}\n\n/**\n * Serve git credentials to the sandboxed child without ever putting one inside it.\n *\n * ADR 0015 keeps credentials host-owned and hands the child a read-only projection. A git\n * credential cannot be projected the same way: it is not one file with a stable shape, it\n * is whatever the host's own helper produces per host. So the credential stays on the host\n * and only the answer to one question crosses, for one host, once it has been released.\n *\n * Two gates, and the first is the one that matters most.\n *\n * The host must be reachable by this session. git only asks for a credential after a\n * server challenged it, so in the ordinary flow the host was already allowed or approved\n * at the network layer. A request for a host the session cannot reach is therefore not git\n * doing its job; it is something that went looking for a token. Refusing it costs nothing\n * real and removes the case where this channel is more useful to an attacker than to git.\n *\n * The human must release it. Per ADR 0023 that decision is the supervisor's, because this\n * socket has no peer authentication -- every descendant in the child's namespace can reach\n * it, which is precisely the code the boundary exists to contain. Without a releaser the\n * channel refuses outright, so a headless session cannot hand out a credential at all.\n *\n * A release covers one host for the session. It is never persisted and never widened to a\n * second host, which is what keeps an approval for a push from also releasing a token to\n * whatever else the session later contacts.\n */\nexport function createGitCredentialProxy(options: {\n\tsocketPath: string;\n\t/** Whether this session may reach the host at all. */\n\tisHostAllowed: (host: string) => boolean;\n\t/** Ask a human to release the host's credential. Absent means refuse without asking. */\n\trequestRelease?: (host: string) => Promise<boolean>;\n\t/** Resolve the credential on the host, using the host's own configuration. */\n\tfillCredential: (request: GitCredentialRequest) => Promise<GitCredential | undefined>;\n\tviolationStore?: SandboxViolationStore;\n}): Promise<GitCredentialProxy> {\n\t/** Hosts released this session. Never persisted, never widened. */\n\tconst released = new Set<string>();\n\tconst pending = new Map<string, Promise<boolean>>();\n\n\tfunction audit(host: string, detail: string): void {\n\t\toptions.violationStore?.add({\n\t\t\tkind: \"network\",\n\t\t\tcommand: `git credential ${host}`,\n\t\t\tdetail,\n\t\t\ttimestamp: new Date(),\n\t\t});\n\t}\n\n\tasync function release(host: string): Promise<boolean> {\n\t\tif (released.has(host)) return true;\n\t\tif (!options.requestRelease) return false;\n\t\tconst inFlight = pending.get(host);\n\t\tif (inFlight) return inFlight;\n\t\tconst question = (async () => {\n\t\t\ttry {\n\t\t\t\tconst approved = await (options.requestRelease as (h: string) => Promise<boolean>)(host);\n\t\t\t\tif (approved) released.add(host);\n\t\t\t\treturn approved;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t} finally {\n\t\t\t\tpending.delete(host);\n\t\t\t}\n\t\t})();\n\t\tpending.set(host, question);\n\t\treturn question;\n\t}\n\n\tasync function answer(request: unknown): Promise<object> {\n\t\tif (!isRequestObject(request)) return { ok: false, error: \"Malformed request.\" };\n\t\tif (request.op !== \"get\") {\n\t\t\t// Only `get` crosses. `store` and `erase` would let the child rewrite the host's\n\t\t\t// credential store, which ADR 0015 keeps as an explicit host operation.\n\t\t\treturn { ok: false, error: \"Only credential reads are served over this channel.\" };\n\t\t}\n\t\tconst parsed = parseGitCredentialRequest(request);\n\t\tif (!parsed.ok) {\n\t\t\t// The refused bytes are deliberately not echoed. This tail is read by a human and\n\t\t\t// rendered in a terminal, and the whole point of the refused field is that it\n\t\t\t// carries line breaks and control characters.\n\t\t\taudit(\"(protocol)\", `Refused a credential request whose ${parsed.audit}.`);\n\t\t\treturn { ok: false, error: parsed.error };\n\t\t}\n\t\tconst { host, protocol } = parsed.request;\n\n\t\tif (!options.isHostAllowed(host)) {\n\t\t\taudit(host, `Refused a credential for ${host}, which this session may not reach.`);\n\t\t\treturn { ok: false, error: \"Host is not reachable by this session.\" };\n\t\t}\n\t\tif (!(await release(host))) {\n\t\t\taudit(host, `Credential for ${host} was not released.`);\n\t\t\treturn { ok: false, error: \"Credential was not released.\" };\n\t\t}\n\t\tconst credential = await options.fillCredential({ host, protocol });\n\t\tif (!credential) {\n\t\t\taudit(host, `The host credential store had nothing for ${host}.`);\n\t\t\treturn { ok: false, error: \"No credential is configured for that host.\" };\n\t\t}\n\t\treturn { ok: true, username: credential.username, password: credential.password };\n\t}\n\n\treturn new Promise((resolve, reject) => {\n\t\t// A prior launch that never closed leaves a stale socket file that listen() refuses\n\t\t// to bind over, exactly as the network proxy handles.\n\t\tif (existsSync(options.socketPath)) unlinkSync(options.socketPath);\n\n\t\tconst sockets = new Set<net.Socket>();\n\t\tconst server = net.createServer((socket) => {\n\t\t\tif (sockets.size >= MAX_CONNECTIONS) {\n\t\t\t\tsocket.destroy();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsockets.add(socket);\n\t\t\tsocket.on(\"close\", () => sockets.delete(socket));\n\t\t\tsocket.on(\"error\", () => socket.destroy());\n\t\t\tconst reader = new FrameReader(socket, \"Git credential channel\", (detail) =>\n\t\t\t\taudit(\"(protocol)\", `Refused: ${detail}`),\n\t\t\t);\n\t\t\tvoid (async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst response = await answer(await reader.next());\n\t\t\t\t\twriteFrame(socket, response);\n\t\t\t\t} catch {\n\t\t\t\t\t// A disconnected or malformed client is the reader's business; there is\n\t\t\t\t\t// nothing to answer and nothing to record beyond what it already did.\n\t\t\t\t} finally {\n\t\t\t\t\tsocket.end();\n\t\t\t\t}\n\t\t\t})();\n\t\t});\n\n\t\tserver.on(\"error\", reject);\n\t\tserver.listen(options.socketPath, () => {\n\t\t\tserver.off(\"error\", reject);\n\t\t\ttry {\n\t\t\t\tchmodSync(options.socketPath, 0o600);\n\t\t\t} catch {\n\t\t\t\t// The containing directory is already 0700; the mode here is defence in depth.\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tclose: () =>\n\t\t\t\t\tnew Promise((closed) => {\n\t\t\t\t\t\tfor (const socket of sockets) socket.destroy();\n\t\t\t\t\t\tserver.close(() => closed());\n\t\t\t\t\t}),\n\t\t\t});\n\t\t});\n\t});\n}\n"]}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { chmodSync, existsSync, unlinkSync } from "node:fs";
|
|
2
|
+
import * as net from "node:net";
|
|
3
|
+
import { FrameReader, isRequestObject, writeFrame } from "./framing.js";
|
|
4
|
+
const MAX_CONNECTIONS = 32;
|
|
5
|
+
/**
|
|
6
|
+
* git's credential protocol is `key=value` lines terminated by a blank line, so any byte
|
|
7
|
+
* that can end a line or a field can rewrite the request downstream of this point. A
|
|
8
|
+
* `protocol` of `https\nhost=other.invalid\n\n` is authorized as one identity and served as
|
|
9
|
+
* another; the same is true of a carriage return, a NUL, or a bare space.
|
|
10
|
+
*/
|
|
11
|
+
const LINE_OR_FIELD_BREAKING = /[\s\u0000-\u001f\u007f-\u009f]/u;
|
|
12
|
+
/**
|
|
13
|
+
* Structural, not an allowlist. Hardcoding `https` would refuse `ssh` and every custom
|
|
14
|
+
* scheme a host helper answers for, which is a working feature rather than an attack. What
|
|
15
|
+
* is refused is anything that is not a scheme: RFC 3986's shape, lowercased first so that
|
|
16
|
+
* the released identity and the served one cannot differ by case.
|
|
17
|
+
*/
|
|
18
|
+
const SCHEME_SHAPE = /^[a-z][a-z0-9+.-]*$/;
|
|
19
|
+
/** Bounds, so a frame that is within the reader's byte limit is still not an essay. */
|
|
20
|
+
const MAX_HOST_LENGTH = 255;
|
|
21
|
+
const MAX_PROTOCOL_LENGTH = 32;
|
|
22
|
+
/**
|
|
23
|
+
* Parse one credential request into the single structured identity everything downstream
|
|
24
|
+
* uses.
|
|
25
|
+
*
|
|
26
|
+
* The caller authorizes, releases, and serializes *this* value and never the raw fields,
|
|
27
|
+
* because the defect this closes is precisely that the string checked against the release
|
|
28
|
+
* was not the string handed to git.
|
|
29
|
+
*/
|
|
30
|
+
export function parseGitCredentialRequest(request) {
|
|
31
|
+
const rawProtocol = request.protocol;
|
|
32
|
+
// git's own helper always sends one; an absent field means "whatever git defaults to",
|
|
33
|
+
// and https is what this channel has always assumed. A present non-string is a client
|
|
34
|
+
// that is not git.
|
|
35
|
+
if (rawProtocol !== undefined && typeof rawProtocol !== "string") {
|
|
36
|
+
return { ok: false, error: "Request protocol was not a string.", audit: "protocol field was not a string" };
|
|
37
|
+
}
|
|
38
|
+
if (typeof request.host !== "string") {
|
|
39
|
+
return { ok: false, error: "Request named no host.", audit: "host field was absent or not a string" };
|
|
40
|
+
}
|
|
41
|
+
const protocol = (rawProtocol ?? "https").toLowerCase();
|
|
42
|
+
const host = request.host;
|
|
43
|
+
if (!host)
|
|
44
|
+
return { ok: false, error: "Request named no host.", audit: "host field was empty" };
|
|
45
|
+
if (host.length > MAX_HOST_LENGTH) {
|
|
46
|
+
return { ok: false, error: "Request host is too long.", audit: "host field exceeded its length bound" };
|
|
47
|
+
}
|
|
48
|
+
if (LINE_OR_FIELD_BREAKING.test(host)) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
error: "Request host contains a control or whitespace character.",
|
|
52
|
+
audit: "host field carried a control or whitespace character",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (protocol.length > MAX_PROTOCOL_LENGTH) {
|
|
56
|
+
return { ok: false, error: "Request protocol is too long.", audit: "protocol field exceeded its length bound" };
|
|
57
|
+
}
|
|
58
|
+
if (!SCHEME_SHAPE.test(protocol)) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
error: "Request protocol is not a scheme.",
|
|
62
|
+
audit: "protocol field was not a scheme",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return { ok: true, request: { protocol, host } };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Serve git credentials to the sandboxed child without ever putting one inside it.
|
|
69
|
+
*
|
|
70
|
+
* ADR 0015 keeps credentials host-owned and hands the child a read-only projection. A git
|
|
71
|
+
* credential cannot be projected the same way: it is not one file with a stable shape, it
|
|
72
|
+
* is whatever the host's own helper produces per host. So the credential stays on the host
|
|
73
|
+
* and only the answer to one question crosses, for one host, once it has been released.
|
|
74
|
+
*
|
|
75
|
+
* Two gates, and the first is the one that matters most.
|
|
76
|
+
*
|
|
77
|
+
* The host must be reachable by this session. git only asks for a credential after a
|
|
78
|
+
* server challenged it, so in the ordinary flow the host was already allowed or approved
|
|
79
|
+
* at the network layer. A request for a host the session cannot reach is therefore not git
|
|
80
|
+
* doing its job; it is something that went looking for a token. Refusing it costs nothing
|
|
81
|
+
* real and removes the case where this channel is more useful to an attacker than to git.
|
|
82
|
+
*
|
|
83
|
+
* The human must release it. Per ADR 0023 that decision is the supervisor's, because this
|
|
84
|
+
* socket has no peer authentication -- every descendant in the child's namespace can reach
|
|
85
|
+
* it, which is precisely the code the boundary exists to contain. Without a releaser the
|
|
86
|
+
* channel refuses outright, so a headless session cannot hand out a credential at all.
|
|
87
|
+
*
|
|
88
|
+
* A release covers one host for the session. It is never persisted and never widened to a
|
|
89
|
+
* second host, which is what keeps an approval for a push from also releasing a token to
|
|
90
|
+
* whatever else the session later contacts.
|
|
91
|
+
*/
|
|
92
|
+
export function createGitCredentialProxy(options) {
|
|
93
|
+
/** Hosts released this session. Never persisted, never widened. */
|
|
94
|
+
const released = new Set();
|
|
95
|
+
const pending = new Map();
|
|
96
|
+
function audit(host, detail) {
|
|
97
|
+
options.violationStore?.add({
|
|
98
|
+
kind: "network",
|
|
99
|
+
command: `git credential ${host}`,
|
|
100
|
+
detail,
|
|
101
|
+
timestamp: new Date(),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
async function release(host) {
|
|
105
|
+
if (released.has(host))
|
|
106
|
+
return true;
|
|
107
|
+
if (!options.requestRelease)
|
|
108
|
+
return false;
|
|
109
|
+
const inFlight = pending.get(host);
|
|
110
|
+
if (inFlight)
|
|
111
|
+
return inFlight;
|
|
112
|
+
const question = (async () => {
|
|
113
|
+
try {
|
|
114
|
+
const approved = await options.requestRelease(host);
|
|
115
|
+
if (approved)
|
|
116
|
+
released.add(host);
|
|
117
|
+
return approved;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
pending.delete(host);
|
|
124
|
+
}
|
|
125
|
+
})();
|
|
126
|
+
pending.set(host, question);
|
|
127
|
+
return question;
|
|
128
|
+
}
|
|
129
|
+
async function answer(request) {
|
|
130
|
+
if (!isRequestObject(request))
|
|
131
|
+
return { ok: false, error: "Malformed request." };
|
|
132
|
+
if (request.op !== "get") {
|
|
133
|
+
// Only `get` crosses. `store` and `erase` would let the child rewrite the host's
|
|
134
|
+
// credential store, which ADR 0015 keeps as an explicit host operation.
|
|
135
|
+
return { ok: false, error: "Only credential reads are served over this channel." };
|
|
136
|
+
}
|
|
137
|
+
const parsed = parseGitCredentialRequest(request);
|
|
138
|
+
if (!parsed.ok) {
|
|
139
|
+
// The refused bytes are deliberately not echoed. This tail is read by a human and
|
|
140
|
+
// rendered in a terminal, and the whole point of the refused field is that it
|
|
141
|
+
// carries line breaks and control characters.
|
|
142
|
+
audit("(protocol)", `Refused a credential request whose ${parsed.audit}.`);
|
|
143
|
+
return { ok: false, error: parsed.error };
|
|
144
|
+
}
|
|
145
|
+
const { host, protocol } = parsed.request;
|
|
146
|
+
if (!options.isHostAllowed(host)) {
|
|
147
|
+
audit(host, `Refused a credential for ${host}, which this session may not reach.`);
|
|
148
|
+
return { ok: false, error: "Host is not reachable by this session." };
|
|
149
|
+
}
|
|
150
|
+
if (!(await release(host))) {
|
|
151
|
+
audit(host, `Credential for ${host} was not released.`);
|
|
152
|
+
return { ok: false, error: "Credential was not released." };
|
|
153
|
+
}
|
|
154
|
+
const credential = await options.fillCredential({ host, protocol });
|
|
155
|
+
if (!credential) {
|
|
156
|
+
audit(host, `The host credential store had nothing for ${host}.`);
|
|
157
|
+
return { ok: false, error: "No credential is configured for that host." };
|
|
158
|
+
}
|
|
159
|
+
return { ok: true, username: credential.username, password: credential.password };
|
|
160
|
+
}
|
|
161
|
+
return new Promise((resolve, reject) => {
|
|
162
|
+
// A prior launch that never closed leaves a stale socket file that listen() refuses
|
|
163
|
+
// to bind over, exactly as the network proxy handles.
|
|
164
|
+
if (existsSync(options.socketPath))
|
|
165
|
+
unlinkSync(options.socketPath);
|
|
166
|
+
const sockets = new Set();
|
|
167
|
+
const server = net.createServer((socket) => {
|
|
168
|
+
if (sockets.size >= MAX_CONNECTIONS) {
|
|
169
|
+
socket.destroy();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
sockets.add(socket);
|
|
173
|
+
socket.on("close", () => sockets.delete(socket));
|
|
174
|
+
socket.on("error", () => socket.destroy());
|
|
175
|
+
const reader = new FrameReader(socket, "Git credential channel", (detail) => audit("(protocol)", `Refused: ${detail}`));
|
|
176
|
+
void (async () => {
|
|
177
|
+
try {
|
|
178
|
+
const response = await answer(await reader.next());
|
|
179
|
+
writeFrame(socket, response);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// A disconnected or malformed client is the reader's business; there is
|
|
183
|
+
// nothing to answer and nothing to record beyond what it already did.
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
socket.end();
|
|
187
|
+
}
|
|
188
|
+
})();
|
|
189
|
+
});
|
|
190
|
+
server.on("error", reject);
|
|
191
|
+
server.listen(options.socketPath, () => {
|
|
192
|
+
server.off("error", reject);
|
|
193
|
+
try {
|
|
194
|
+
chmodSync(options.socketPath, 0o600);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// The containing directory is already 0700; the mode here is defence in depth.
|
|
198
|
+
}
|
|
199
|
+
resolve({
|
|
200
|
+
close: () => new Promise((closed) => {
|
|
201
|
+
for (const socket of sockets)
|
|
202
|
+
socket.destroy();
|
|
203
|
+
server.close(() => closed());
|
|
204
|
+
}),
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=git-credential-proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-credential-proxy.js","sourceRoot":"","sources":["../../../../src/core/sandbox/rpc/git-credential-proxy.ts"],"names":[],"mappings":"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","sourcesContent":["import { chmodSync, existsSync, unlinkSync } from \"node:fs\";\nimport * as net from \"node:net\";\nimport type { SandboxViolationStore } from \"../violations.ts\";\nimport { FrameReader, isRequestObject, writeFrame } from \"./framing.ts\";\n\nconst MAX_CONNECTIONS = 32;\n\nexport interface GitCredentialRequest {\n\treadonly protocol: string;\n\treadonly host: string;\n}\n\nexport interface GitCredential {\n\treadonly username: string;\n\treadonly password: string;\n}\n\nexport interface GitCredentialProxy {\n\tclose(): Promise<void>;\n}\n\n/** A request that survived validation, or the reason it did not. */\nexport type GitCredentialRequestParse =\n\t| { readonly ok: true; readonly request: GitCredentialRequest }\n\t| { readonly ok: false; readonly error: string; readonly audit: string };\n\n/**\n * git's credential protocol is `key=value` lines terminated by a blank line, so any byte\n * that can end a line or a field can rewrite the request downstream of this point. A\n * `protocol` of `https\\nhost=other.invalid\\n\\n` is authorized as one identity and served as\n * another; the same is true of a carriage return, a NUL, or a bare space.\n */\nconst LINE_OR_FIELD_BREAKING = /[\\s\\u0000-\\u001f\\u007f-\\u009f]/u;\n\n/**\n * Structural, not an allowlist. Hardcoding `https` would refuse `ssh` and every custom\n * scheme a host helper answers for, which is a working feature rather than an attack. What\n * is refused is anything that is not a scheme: RFC 3986's shape, lowercased first so that\n * the released identity and the served one cannot differ by case.\n */\nconst SCHEME_SHAPE = /^[a-z][a-z0-9+.-]*$/;\n\n/** Bounds, so a frame that is within the reader's byte limit is still not an essay. */\nconst MAX_HOST_LENGTH = 255;\nconst MAX_PROTOCOL_LENGTH = 32;\n\n/**\n * Parse one credential request into the single structured identity everything downstream\n * uses.\n *\n * The caller authorizes, releases, and serializes *this* value and never the raw fields,\n * because the defect this closes is precisely that the string checked against the release\n * was not the string handed to git.\n */\nexport function parseGitCredentialRequest(request: Record<string, unknown>): GitCredentialRequestParse {\n\tconst rawProtocol = request.protocol;\n\t// git's own helper always sends one; an absent field means \"whatever git defaults to\",\n\t// and https is what this channel has always assumed. A present non-string is a client\n\t// that is not git.\n\tif (rawProtocol !== undefined && typeof rawProtocol !== \"string\") {\n\t\treturn { ok: false, error: \"Request protocol was not a string.\", audit: \"protocol field was not a string\" };\n\t}\n\tif (typeof request.host !== \"string\") {\n\t\treturn { ok: false, error: \"Request named no host.\", audit: \"host field was absent or not a string\" };\n\t}\n\n\tconst protocol = (rawProtocol ?? \"https\").toLowerCase();\n\tconst host = request.host;\n\tif (!host) return { ok: false, error: \"Request named no host.\", audit: \"host field was empty\" };\n\tif (host.length > MAX_HOST_LENGTH) {\n\t\treturn { ok: false, error: \"Request host is too long.\", audit: \"host field exceeded its length bound\" };\n\t}\n\tif (LINE_OR_FIELD_BREAKING.test(host)) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: \"Request host contains a control or whitespace character.\",\n\t\t\taudit: \"host field carried a control or whitespace character\",\n\t\t};\n\t}\n\tif (protocol.length > MAX_PROTOCOL_LENGTH) {\n\t\treturn { ok: false, error: \"Request protocol is too long.\", audit: \"protocol field exceeded its length bound\" };\n\t}\n\tif (!SCHEME_SHAPE.test(protocol)) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: \"Request protocol is not a scheme.\",\n\t\t\taudit: \"protocol field was not a scheme\",\n\t\t};\n\t}\n\treturn { ok: true, request: { protocol, host } };\n}\n\n/**\n * Serve git credentials to the sandboxed child without ever putting one inside it.\n *\n * ADR 0015 keeps credentials host-owned and hands the child a read-only projection. A git\n * credential cannot be projected the same way: it is not one file with a stable shape, it\n * is whatever the host's own helper produces per host. So the credential stays on the host\n * and only the answer to one question crosses, for one host, once it has been released.\n *\n * Two gates, and the first is the one that matters most.\n *\n * The host must be reachable by this session. git only asks for a credential after a\n * server challenged it, so in the ordinary flow the host was already allowed or approved\n * at the network layer. A request for a host the session cannot reach is therefore not git\n * doing its job; it is something that went looking for a token. Refusing it costs nothing\n * real and removes the case where this channel is more useful to an attacker than to git.\n *\n * The human must release it. Per ADR 0023 that decision is the supervisor's, because this\n * socket has no peer authentication -- every descendant in the child's namespace can reach\n * it, which is precisely the code the boundary exists to contain. Without a releaser the\n * channel refuses outright, so a headless session cannot hand out a credential at all.\n *\n * A release covers one host for the session. It is never persisted and never widened to a\n * second host, which is what keeps an approval for a push from also releasing a token to\n * whatever else the session later contacts.\n */\nexport function createGitCredentialProxy(options: {\n\tsocketPath: string;\n\t/** Whether this session may reach the host at all. */\n\tisHostAllowed: (host: string) => boolean;\n\t/** Ask a human to release the host's credential. Absent means refuse without asking. */\n\trequestRelease?: (host: string) => Promise<boolean>;\n\t/** Resolve the credential on the host, using the host's own configuration. */\n\tfillCredential: (request: GitCredentialRequest) => Promise<GitCredential | undefined>;\n\tviolationStore?: SandboxViolationStore;\n}): Promise<GitCredentialProxy> {\n\t/** Hosts released this session. Never persisted, never widened. */\n\tconst released = new Set<string>();\n\tconst pending = new Map<string, Promise<boolean>>();\n\n\tfunction audit(host: string, detail: string): void {\n\t\toptions.violationStore?.add({\n\t\t\tkind: \"network\",\n\t\t\tcommand: `git credential ${host}`,\n\t\t\tdetail,\n\t\t\ttimestamp: new Date(),\n\t\t});\n\t}\n\n\tasync function release(host: string): Promise<boolean> {\n\t\tif (released.has(host)) return true;\n\t\tif (!options.requestRelease) return false;\n\t\tconst inFlight = pending.get(host);\n\t\tif (inFlight) return inFlight;\n\t\tconst question = (async () => {\n\t\t\ttry {\n\t\t\t\tconst approved = await (options.requestRelease as (h: string) => Promise<boolean>)(host);\n\t\t\t\tif (approved) released.add(host);\n\t\t\t\treturn approved;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t} finally {\n\t\t\t\tpending.delete(host);\n\t\t\t}\n\t\t})();\n\t\tpending.set(host, question);\n\t\treturn question;\n\t}\n\n\tasync function answer(request: unknown): Promise<object> {\n\t\tif (!isRequestObject(request)) return { ok: false, error: \"Malformed request.\" };\n\t\tif (request.op !== \"get\") {\n\t\t\t// Only `get` crosses. `store` and `erase` would let the child rewrite the host's\n\t\t\t// credential store, which ADR 0015 keeps as an explicit host operation.\n\t\t\treturn { ok: false, error: \"Only credential reads are served over this channel.\" };\n\t\t}\n\t\tconst parsed = parseGitCredentialRequest(request);\n\t\tif (!parsed.ok) {\n\t\t\t// The refused bytes are deliberately not echoed. This tail is read by a human and\n\t\t\t// rendered in a terminal, and the whole point of the refused field is that it\n\t\t\t// carries line breaks and control characters.\n\t\t\taudit(\"(protocol)\", `Refused a credential request whose ${parsed.audit}.`);\n\t\t\treturn { ok: false, error: parsed.error };\n\t\t}\n\t\tconst { host, protocol } = parsed.request;\n\n\t\tif (!options.isHostAllowed(host)) {\n\t\t\taudit(host, `Refused a credential for ${host}, which this session may not reach.`);\n\t\t\treturn { ok: false, error: \"Host is not reachable by this session.\" };\n\t\t}\n\t\tif (!(await release(host))) {\n\t\t\taudit(host, `Credential for ${host} was not released.`);\n\t\t\treturn { ok: false, error: \"Credential was not released.\" };\n\t\t}\n\t\tconst credential = await options.fillCredential({ host, protocol });\n\t\tif (!credential) {\n\t\t\taudit(host, `The host credential store had nothing for ${host}.`);\n\t\t\treturn { ok: false, error: \"No credential is configured for that host.\" };\n\t\t}\n\t\treturn { ok: true, username: credential.username, password: credential.password };\n\t}\n\n\treturn new Promise((resolve, reject) => {\n\t\t// A prior launch that never closed leaves a stale socket file that listen() refuses\n\t\t// to bind over, exactly as the network proxy handles.\n\t\tif (existsSync(options.socketPath)) unlinkSync(options.socketPath);\n\n\t\tconst sockets = new Set<net.Socket>();\n\t\tconst server = net.createServer((socket) => {\n\t\t\tif (sockets.size >= MAX_CONNECTIONS) {\n\t\t\t\tsocket.destroy();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsockets.add(socket);\n\t\t\tsocket.on(\"close\", () => sockets.delete(socket));\n\t\t\tsocket.on(\"error\", () => socket.destroy());\n\t\t\tconst reader = new FrameReader(socket, \"Git credential channel\", (detail) =>\n\t\t\t\taudit(\"(protocol)\", `Refused: ${detail}`),\n\t\t\t);\n\t\t\tvoid (async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst response = await answer(await reader.next());\n\t\t\t\t\twriteFrame(socket, response);\n\t\t\t\t} catch {\n\t\t\t\t\t// A disconnected or malformed client is the reader's business; there is\n\t\t\t\t\t// nothing to answer and nothing to record beyond what it already did.\n\t\t\t\t} finally {\n\t\t\t\t\tsocket.end();\n\t\t\t\t}\n\t\t\t})();\n\t\t});\n\n\t\tserver.on(\"error\", reject);\n\t\tserver.listen(options.socketPath, () => {\n\t\t\tserver.off(\"error\", reject);\n\t\t\ttry {\n\t\t\t\tchmodSync(options.socketPath, 0o600);\n\t\t\t} catch {\n\t\t\t\t// The containing directory is already 0700; the mode here is defence in depth.\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tclose: () =>\n\t\t\t\t\tnew Promise((closed) => {\n\t\t\t\t\t\tfor (const socket of sockets) socket.destroy();\n\t\t\t\t\t\tserver.close(() => closed());\n\t\t\t\t\t}),\n\t\t\t});\n\t\t});\n\t});\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The directory the supervisor puts its own private state in.
|
|
3
|
+
*
|
|
4
|
+
* `/tmp` on POSIX rather than `TMPDIR`, deliberately, for two reasons the channel code
|
|
5
|
+
* depends on. `TMPDIR` can point inside the workspace on macOS, which the sandbox may
|
|
6
|
+
* write, and a socket has to fit inside `AF_UNIX`'s 108-byte `sun_path`, which an
|
|
7
|
+
* unusually long `TMPDIR` exhausts.
|
|
8
|
+
*
|
|
9
|
+
* Windows has neither `/tmp` nor Unix sockets. Nothing here is reachable in a Windows
|
|
10
|
+
* session, because ADR 0005 leaves that platform unsupported and both backends report
|
|
11
|
+
* `unavailable` before a launch gets this far. It still has to resolve to something real:
|
|
12
|
+
* these helpers are called before the backend's platform check in one path, and off-platform
|
|
13
|
+
* in the tests that exercise violation attribution without a macOS host. Returning
|
|
14
|
+
* `os.tmpdir()` there turns a confusing `ENOENT: mkdtemp '/tmp/...'` into the accurate
|
|
15
|
+
* "OS sandbox is supported on macOS only."
|
|
16
|
+
*/
|
|
17
|
+
export declare function supervisorTempDirectory(): string;
|
|
18
|
+
//# sourceMappingURL=supervisor-temp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supervisor-temp.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/supervisor-temp.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAEhD","sourcesContent":["import { tmpdir } from \"node:os\";\n\n/**\n * The directory the supervisor puts its own private state in.\n *\n * `/tmp` on POSIX rather than `TMPDIR`, deliberately, for two reasons the channel code\n * depends on. `TMPDIR` can point inside the workspace on macOS, which the sandbox may\n * write, and a socket has to fit inside `AF_UNIX`'s 108-byte `sun_path`, which an\n * unusually long `TMPDIR` exhausts.\n *\n * Windows has neither `/tmp` nor Unix sockets. Nothing here is reachable in a Windows\n * session, because ADR 0005 leaves that platform unsupported and both backends report\n * `unavailable` before a launch gets this far. It still has to resolve to something real:\n * these helpers are called before the backend's platform check in one path, and off-platform\n * in the tests that exercise violation attribution without a macOS host. Returning\n * `os.tmpdir()` there turns a confusing `ENOENT: mkdtemp '/tmp/...'` into the accurate\n * \"OS sandbox is supported on macOS only.\"\n */\nexport function supervisorTempDirectory(): string {\n\treturn process.platform === \"win32\" ? tmpdir() : \"/tmp\";\n}\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { tmpdir } from "node:os";
|
|
2
|
+
/**
|
|
3
|
+
* The directory the supervisor puts its own private state in.
|
|
4
|
+
*
|
|
5
|
+
* `/tmp` on POSIX rather than `TMPDIR`, deliberately, for two reasons the channel code
|
|
6
|
+
* depends on. `TMPDIR` can point inside the workspace on macOS, which the sandbox may
|
|
7
|
+
* write, and a socket has to fit inside `AF_UNIX`'s 108-byte `sun_path`, which an
|
|
8
|
+
* unusually long `TMPDIR` exhausts.
|
|
9
|
+
*
|
|
10
|
+
* Windows has neither `/tmp` nor Unix sockets. Nothing here is reachable in a Windows
|
|
11
|
+
* session, because ADR 0005 leaves that platform unsupported and both backends report
|
|
12
|
+
* `unavailable` before a launch gets this far. It still has to resolve to something real:
|
|
13
|
+
* these helpers are called before the backend's platform check in one path, and off-platform
|
|
14
|
+
* in the tests that exercise violation attribution without a macOS host. Returning
|
|
15
|
+
* `os.tmpdir()` there turns a confusing `ENOENT: mkdtemp '/tmp/...'` into the accurate
|
|
16
|
+
* "OS sandbox is supported on macOS only."
|
|
17
|
+
*/
|
|
18
|
+
export function supervisorTempDirectory() {
|
|
19
|
+
return process.platform === "win32" ? tmpdir() : "/tmp";
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=supervisor-temp.js.map
|