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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdtempSync, unlinkSync } from "node:fs";
|
|
2
|
+
import * as net from "node:net";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { supervisorTempDirectory } from "../supervisor-temp.js";
|
|
5
|
+
import { FrameReader, isRequestObject, writeFrame } from "./framing.js";
|
|
6
|
+
/** AF_UNIX `sun_path` is 108 bytes on Linux, including the terminating NUL. */
|
|
7
|
+
const SUN_PATH_LIMIT = 108;
|
|
8
|
+
const MAX_CONNECTIONS = 8;
|
|
9
|
+
/** Env var naming the child-side socket. */
|
|
10
|
+
export const COMMAND_ESCALATION_SOCKET_VARIABLE = "APEX_COMMAND_ESCALATION_PATH";
|
|
11
|
+
export function resolveCommandEscalationChannelPaths() {
|
|
12
|
+
const hostSocketDirectory = mkdtempSync(join(supervisorTempDirectory(), `apex-escalate-${process.pid}-`), {
|
|
13
|
+
encoding: "utf8",
|
|
14
|
+
});
|
|
15
|
+
chmodSync(hostSocketDirectory, 0o700);
|
|
16
|
+
const hostSocketPath = join(hostSocketDirectory, "channel.sock");
|
|
17
|
+
if (Buffer.byteLength(hostSocketPath) + 1 > SUN_PATH_LIMIT) {
|
|
18
|
+
throw new Error("Command escalation channel path exceeds the Unix socket path limit.");
|
|
19
|
+
}
|
|
20
|
+
const childSocketPath = process.platform === "linux" ? "/home/apex-command-escalation.sock" : hostSocketPath;
|
|
21
|
+
return { hostSocketDirectory, hostSocketPath, childSocketPath };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Run one approved command in a second, differently-mounted child.
|
|
25
|
+
*
|
|
26
|
+
* A refused write is refused by the kernel inside a namespace whose mounts are fixed for
|
|
27
|
+
* its lifetime. There is no in-place equivalent of holding a CONNECT open while a human
|
|
28
|
+
* decides: the syscall has already failed, and nothing inside the namespace can widen it.
|
|
29
|
+
* So the escalation happens where the authority is, outside the boundary, by starting a
|
|
30
|
+
* separate child for that one command.
|
|
31
|
+
*
|
|
32
|
+
* The original child's namespace is never modified, which is what makes this sound rather
|
|
33
|
+
* than a hole. Approving one command grants nothing to the session it came from; the
|
|
34
|
+
* session's own next attempt at the same operation is refused exactly as before.
|
|
35
|
+
*
|
|
36
|
+
* Approval is the supervisor's, per ADR 0023, for the reason that ADR gives: this socket
|
|
37
|
+
* has no peer authentication, so a request arriving on it may have come from anything
|
|
38
|
+
* inside the boundary. Without an approver the channel refuses, which is what keeps
|
|
39
|
+
* headless, print, JSON, and RPC modes at ADR 0005's deny.
|
|
40
|
+
*/
|
|
41
|
+
export function createCommandEscalationProxy(options) {
|
|
42
|
+
function audit(detail, command) {
|
|
43
|
+
options.violationStore?.add({ kind: "filesystem", command, detail, timestamp: new Date() });
|
|
44
|
+
}
|
|
45
|
+
async function answer(request) {
|
|
46
|
+
if (!isRequestObject(request))
|
|
47
|
+
return { ok: false, error: "Malformed request." };
|
|
48
|
+
if (request.op !== "run")
|
|
49
|
+
return { ok: false, error: "Only command escalation is served here." };
|
|
50
|
+
const command = typeof request.command === "string" ? request.command : "";
|
|
51
|
+
const writableRoot = typeof request.writableRoot === "string" ? request.writableRoot : "";
|
|
52
|
+
if (!command || !writableRoot)
|
|
53
|
+
return { ok: false, error: "Request named no command or no root." };
|
|
54
|
+
// Every request is asked about individually. There is no session-scoped grant here,
|
|
55
|
+
// unlike a host or a credential: a command is not a stable subject to remember, and
|
|
56
|
+
// remembering one would silently cover the next command that named the same root.
|
|
57
|
+
if (!options.requestApproval) {
|
|
58
|
+
audit(`Refused an escalation with no way to ask: ${command}`, command);
|
|
59
|
+
return { ok: false, error: "Escalation is unavailable without a terminal." };
|
|
60
|
+
}
|
|
61
|
+
let approved = false;
|
|
62
|
+
try {
|
|
63
|
+
approved = await options.requestApproval({ command, writableRoot });
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
approved = false;
|
|
67
|
+
}
|
|
68
|
+
if (!approved) {
|
|
69
|
+
audit(`Escalation refused for ${writableRoot}`, command);
|
|
70
|
+
return { ok: false, error: "Escalation was refused." };
|
|
71
|
+
}
|
|
72
|
+
const result = await options.runEscalated({ command, writableRoot });
|
|
73
|
+
audit(`Escalated with ${writableRoot} writable, exit ${result.code}`, command);
|
|
74
|
+
return { ok: true, code: result.code, stdout: result.stdout, stderr: result.stderr };
|
|
75
|
+
}
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
if (existsSync(options.socketPath))
|
|
78
|
+
unlinkSync(options.socketPath);
|
|
79
|
+
const sockets = new Set();
|
|
80
|
+
const server = net.createServer((socket) => {
|
|
81
|
+
if (sockets.size >= MAX_CONNECTIONS) {
|
|
82
|
+
socket.destroy();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
sockets.add(socket);
|
|
86
|
+
socket.on("close", () => sockets.delete(socket));
|
|
87
|
+
socket.on("error", () => socket.destroy());
|
|
88
|
+
const reader = new FrameReader(socket, "Command escalation channel", (detail) => audit(`Refused: ${detail}`, "(protocol)"));
|
|
89
|
+
void (async () => {
|
|
90
|
+
try {
|
|
91
|
+
writeFrame(socket, await answer(await reader.next()));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// A disconnected or malformed client is the reader's business.
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
socket.end();
|
|
98
|
+
}
|
|
99
|
+
})();
|
|
100
|
+
});
|
|
101
|
+
server.on("error", reject);
|
|
102
|
+
server.listen(options.socketPath, () => {
|
|
103
|
+
server.off("error", reject);
|
|
104
|
+
try {
|
|
105
|
+
chmodSync(options.socketPath, 0o600);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// The containing directory is already 0700; this is defence in depth.
|
|
109
|
+
}
|
|
110
|
+
resolve({
|
|
111
|
+
close: () => new Promise((closed) => {
|
|
112
|
+
for (const socket of sockets)
|
|
113
|
+
socket.destroy();
|
|
114
|
+
server.close(() => closed());
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=command-proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-proxy.js","sourceRoot":"","sources":["../../../../src/core/sandbox/rpc/command-proxy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACzE,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAExE,+EAA+E;AAC/E,MAAM,cAAc,GAAG,GAAG,CAAC;AAC3B,MAAM,eAAe,GAAG,CAAC,CAAC;AAE1B,4CAA4C;AAC5C,MAAM,CAAC,MAAM,kCAAkC,GAAG,8BAA8B,CAAC;AAoBjF,MAAM,UAAU,oCAAoC,GAAkC;IACrF,MAAM,mBAAmB,GAAG,WAAW,CAAC,IAAI,CAAC,uBAAuB,EAAE,EAAE,iBAAiB,OAAO,CAAC,GAAG,GAAG,CAAC,EAAE;QACzG,QAAQ,EAAE,MAAM;KAChB,CAAC,CAAC;IACH,SAAS,CAAC,mBAAmB,EAAE,KAAK,CAAC,CAAC;IACtC,MAAM,cAAc,GAAG,IAAI,CAAC,mBAAmB,EAAE,cAAc,CAAC,CAAC;IACjE,IAAI,MAAM,CAAC,UAAU,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,cAAc,EAAE,CAAC;QAC5D,MAAM,IAAI,KAAK,CAAC,qEAAqE,CAAC,CAAC;IACxF,CAAC;IACD,MAAM,eAAe,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,oCAAoC,CAAC,CAAC,CAAC,cAAc,CAAC;IAC7G,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,eAAe,EAAE,CAAC;AAAA,CAChE;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,4BAA4B,CAAC,OAO5C,EAAuC;IACvC,SAAS,KAAK,CAAC,MAAc,EAAE,OAAe,EAAQ;QACrD,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC;IAAA,CAC5F;IAED,KAAK,UAAU,MAAM,CAAC,OAAgB,EAAmB;QACxD,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,oBAAoB,EAAE,CAAC;QACjF,IAAI,OAAO,CAAC,EAAE,KAAK,KAAK;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,yCAAyC,EAAE,CAAC;QACjG,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3E,MAAM,YAAY,GAAG,OAAO,OAAO,CAAC,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1F,IAAI,CAAC,OAAO,IAAI,CAAC,YAAY;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,sCAAsC,EAAE,CAAC;QAEnG,oFAAoF;QACpF,oFAAoF;QACpF,kFAAkF;QAClF,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,CAAC;YAC9B,KAAK,CAAC,6CAA6C,OAAO,EAAE,EAAE,OAAO,CAAC,CAAC;YACvE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,+CAA+C,EAAE,CAAC;QAC9E,CAAC;QACD,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,IAAI,CAAC;YACJ,QAAQ,GAAG,MAAM,OAAO,CAAC,eAAe,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,CAAC;QACrE,CAAC;QAAC,MAAM,CAAC;YACR,QAAQ,GAAG,KAAK,CAAC;QAClB,CAAC;QACD,IAAI,CAAC,QAAQ,EAAE,CAAC;YACf,KAAK,CAAC,0BAA0B,YAAY,EAAE,EAAE,OAAO,CAAC,CAAC;YACzD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAC;QACxD,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,CAAC;QACrE,KAAK,CAAC,kBAAkB,YAAY,mBAAmB,MAAM,CAAC,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC;QAC/E,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC;IAAA,CACrF;IAED,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;QACvC,IAAI,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC;YAAE,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACnE,MAAM,OAAO,GAAG,IAAI,GAAG,EAAc,CAAC;QACtC,MAAM,MAAM,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;YAC3C,IAAI,OAAO,CAAC,IAAI,IAAI,eAAe,EAAE,CAAC;gBACrC,MAAM,CAAC,OAAO,EAAE,CAAC;gBACjB,OAAO;YACR,CAAC;YACD,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACpB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;YACjD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;YAC3C,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC,MAAM,EAAE,4BAA4B,EAAE,CAAC,MAAM,EAAE,EAAE,CAC/E,KAAK,CAAC,YAAY,MAAM,EAAE,EAAE,YAAY,CAAC,CACzC,CAAC;YACF,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC;gBACjB,IAAI,CAAC;oBACJ,UAAU,CAAC,MAAM,EAAE,MAAM,MAAM,CAAC,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;gBACvD,CAAC;gBAAC,MAAM,CAAC;oBACR,+DAA+D;gBAChE,CAAC;wBAAS,CAAC;oBACV,MAAM,CAAC,GAAG,EAAE,CAAC;gBACd,CAAC;YAAA,CACD,CAAC,EAAE,CAAC;QAAA,CACL,CAAC,CAAC;QACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC3B,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE,GAAG,EAAE,CAAC;YACvC,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAC5B,IAAI,CAAC;gBACJ,SAAS,CAAC,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;YACtC,CAAC;YAAC,MAAM,CAAC;gBACR,sEAAsE;YACvE,CAAC;YACD,OAAO,CAAC;gBACP,KAAK,EAAE,GAAG,EAAE,CACX,IAAI,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;oBACvB,KAAK,MAAM,MAAM,IAAI,OAAO;wBAAE,MAAM,CAAC,OAAO,EAAE,CAAC;oBAC/C,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC;gBAAA,CAC7B,CAAC;aACH,CAAC,CAAC;QAAA,CACH,CAAC,CAAC;IAAA,CACH,CAAC,CAAC;AAAA,CACH","sourcesContent":["import { chmodSync, existsSync, mkdtempSync, unlinkSync } from \"node:fs\";\nimport * as net from \"node:net\";\nimport { join } from \"node:path\";\nimport { supervisorTempDirectory } from \"../supervisor-temp.ts\";\nimport type { SandboxViolationStore } from \"../violations.ts\";\nimport { FrameReader, isRequestObject, writeFrame } from \"./framing.ts\";\n\n/** AF_UNIX `sun_path` is 108 bytes on Linux, including the terminating NUL. */\nconst SUN_PATH_LIMIT = 108;\nconst MAX_CONNECTIONS = 8;\n\n/** Env var naming the child-side socket. */\nexport const COMMAND_ESCALATION_SOCKET_VARIABLE = \"APEX_COMMAND_ESCALATION_PATH\";\n\nexport interface CommandEscalationRequest {\n\treadonly command: string;\n\t/** The one directory the command needs writable that it does not already have. */\n\treadonly writableRoot: string;\n}\n\nexport interface CommandEscalationResult {\n\treadonly code: number;\n\treadonly stdout: string;\n\treadonly stderr: string;\n}\n\nexport interface CommandEscalationChannelPaths {\n\treadonly hostSocketDirectory: string;\n\treadonly hostSocketPath: string;\n\treadonly childSocketPath: string;\n}\n\nexport function resolveCommandEscalationChannelPaths(): CommandEscalationChannelPaths {\n\tconst hostSocketDirectory = mkdtempSync(join(supervisorTempDirectory(), `apex-escalate-${process.pid}-`), {\n\t\tencoding: \"utf8\",\n\t});\n\tchmodSync(hostSocketDirectory, 0o700);\n\tconst hostSocketPath = join(hostSocketDirectory, \"channel.sock\");\n\tif (Buffer.byteLength(hostSocketPath) + 1 > SUN_PATH_LIMIT) {\n\t\tthrow new Error(\"Command escalation channel path exceeds the Unix socket path limit.\");\n\t}\n\tconst childSocketPath = process.platform === \"linux\" ? \"/home/apex-command-escalation.sock\" : hostSocketPath;\n\treturn { hostSocketDirectory, hostSocketPath, childSocketPath };\n}\n\n/**\n * Run one approved command in a second, differently-mounted child.\n *\n * A refused write is refused by the kernel inside a namespace whose mounts are fixed for\n * its lifetime. There is no in-place equivalent of holding a CONNECT open while a human\n * decides: the syscall has already failed, and nothing inside the namespace can widen it.\n * So the escalation happens where the authority is, outside the boundary, by starting a\n * separate child for that one command.\n *\n * The original child's namespace is never modified, which is what makes this sound rather\n * than a hole. Approving one command grants nothing to the session it came from; the\n * session's own next attempt at the same operation is refused exactly as before.\n *\n * Approval is the supervisor's, per ADR 0023, for the reason that ADR gives: this socket\n * has no peer authentication, so a request arriving on it may have come from anything\n * inside the boundary. Without an approver the channel refuses, which is what keeps\n * headless, print, JSON, and RPC modes at ADR 0005's deny.\n */\nexport function createCommandEscalationProxy(options: {\n\tsocketPath: string;\n\t/** Ask a human. Absent means refuse without asking. */\n\trequestApproval?: (request: CommandEscalationRequest) => Promise<boolean>;\n\t/** Start the second child. Supplied by the platform backend, which owns mount shape. */\n\trunEscalated: (request: CommandEscalationRequest) => Promise<CommandEscalationResult>;\n\tviolationStore?: SandboxViolationStore;\n}): Promise<{ close(): Promise<void> }> {\n\tfunction audit(detail: string, command: string): void {\n\t\toptions.violationStore?.add({ kind: \"filesystem\", command, detail, timestamp: new Date() });\n\t}\n\n\tasync function answer(request: unknown): Promise<object> {\n\t\tif (!isRequestObject(request)) return { ok: false, error: \"Malformed request.\" };\n\t\tif (request.op !== \"run\") return { ok: false, error: \"Only command escalation is served here.\" };\n\t\tconst command = typeof request.command === \"string\" ? request.command : \"\";\n\t\tconst writableRoot = typeof request.writableRoot === \"string\" ? request.writableRoot : \"\";\n\t\tif (!command || !writableRoot) return { ok: false, error: \"Request named no command or no root.\" };\n\n\t\t// Every request is asked about individually. There is no session-scoped grant here,\n\t\t// unlike a host or a credential: a command is not a stable subject to remember, and\n\t\t// remembering one would silently cover the next command that named the same root.\n\t\tif (!options.requestApproval) {\n\t\t\taudit(`Refused an escalation with no way to ask: ${command}`, command);\n\t\t\treturn { ok: false, error: \"Escalation is unavailable without a terminal.\" };\n\t\t}\n\t\tlet approved = false;\n\t\ttry {\n\t\t\tapproved = await options.requestApproval({ command, writableRoot });\n\t\t} catch {\n\t\t\tapproved = false;\n\t\t}\n\t\tif (!approved) {\n\t\t\taudit(`Escalation refused for ${writableRoot}`, command);\n\t\t\treturn { ok: false, error: \"Escalation was refused.\" };\n\t\t}\n\t\tconst result = await options.runEscalated({ command, writableRoot });\n\t\taudit(`Escalated with ${writableRoot} writable, exit ${result.code}`, command);\n\t\treturn { ok: true, code: result.code, stdout: result.stdout, stderr: result.stderr };\n\t}\n\n\treturn new Promise((resolve, reject) => {\n\t\tif (existsSync(options.socketPath)) unlinkSync(options.socketPath);\n\t\tconst sockets = new Set<net.Socket>();\n\t\tconst server = net.createServer((socket) => {\n\t\t\tif (sockets.size >= MAX_CONNECTIONS) {\n\t\t\t\tsocket.destroy();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsockets.add(socket);\n\t\t\tsocket.on(\"close\", () => sockets.delete(socket));\n\t\t\tsocket.on(\"error\", () => socket.destroy());\n\t\t\tconst reader = new FrameReader(socket, \"Command escalation channel\", (detail) =>\n\t\t\t\taudit(`Refused: ${detail}`, \"(protocol)\"),\n\t\t\t);\n\t\t\tvoid (async () => {\n\t\t\t\ttry {\n\t\t\t\t\twriteFrame(socket, await answer(await reader.next()));\n\t\t\t\t} catch {\n\t\t\t\t\t// A disconnected or malformed client is the reader's business.\n\t\t\t\t} finally {\n\t\t\t\t\tsocket.end();\n\t\t\t\t}\n\t\t\t})();\n\t\t});\n\t\tserver.on(\"error\", reject);\n\t\tserver.listen(options.socketPath, () => {\n\t\t\tserver.off(\"error\", reject);\n\t\t\ttry {\n\t\t\t\tchmodSync(options.socketPath, 0o600);\n\t\t\t} catch {\n\t\t\t\t// The containing directory is already 0700; this is defence in depth.\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tclose: () =>\n\t\t\t\t\tnew Promise((closed) => {\n\t\t\t\t\t\tfor (const socket of sockets) socket.destroy();\n\t\t\t\t\t\tserver.close(() => closed());\n\t\t\t\t\t}),\n\t\t\t});\n\t\t});\n\t});\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"credential-proxy.d.ts","sourceRoot":"","sources":["../../../../src/core/sandbox/rpc/credential-proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAGxD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AA0C9D,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CACjC;AAED;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,IAAI,sBAAsB,CAWtE;AAED,MAAM,WAAW,eAAe;IAC/B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,MAAM,WAAW,yBAAyB;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,aAAa,CAAC;CACzC;AAED,2FAA2F;AAC3F,wBAAgB,6BAA6B,CAAC,UAAU,EAAE,OAAO,GAAG,yBAAyB,GAAG,SAAS,CA2BxG;AAWD,kEAAkE;AAClE,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAgBhE;AAyHD,wEAAwE;AACxE,wBAAgB,qBAAqB,CAAC,OAAO,EAAE;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,qBAAqB,CAAC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,sFAAsF;IACtF,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B,GAAG,OAAO,CAAC,eAAe,CAAC,CAkK3B","sourcesContent":["/**\n * Supervisor-owned credential mutation service for sandboxed sessions.\n *\n * Reads stay on the read-only auth.json projection. Mutations cross this Unix socket\n * and execute under the host AuthStorage lock. The child can therefore implement the\n * CredentialStore serialized read-modify-write contract without gaining filesystem\n * write access to the credential file.\n */\n\nimport { chmodSync, lstatSync, mkdtempSync, readdirSync, rmSync } from \"node:fs\";\nimport * as net from \"node:net\";\nimport { join } from \"node:path\";\nimport type { Credential } from \"@earendil-works/pi-ai\";\nimport { AuthStorage } from \"../../auth-storage.ts\";\nimport { getConfigValueEnvVarNames, isCommandConfigValue } from \"../../resolve-config-value.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/** Credentials are small; reject larger frames before UTF-8 decoding or JSON parsing. */\nconst MAX_FRAME_BYTES = 64 * 1024;\nconst MAX_CONNECTIONS = 32;\nconst CONNECTION_IDLE_TIMEOUT_MS = 30_000;\n\nfunction processIsAlive(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (error) {\n\t\treturn (error as NodeJS.ErrnoException).code === \"EPERM\";\n\t}\n}\n\n/** Reclaim only same-user directories whose encoded supervisor PID is dead. */\nfunction reclaimStaleCredentialChannelDirectories(): void {\n\tlet entries: string[];\n\ttry {\n\t\tentries = readdirSync(\"/tmp\");\n\t} catch {\n\t\treturn;\n\t}\n\tfor (const entry of entries) {\n\t\tconst match = /^apex-cred-(\\d+)-[A-Za-z0-9_-]+$/.exec(entry);\n\t\tif (!match) continue;\n\t\tconst pid = Number(match[1]);\n\t\tif (!Number.isSafeInteger(pid) || pid < 1 || processIsAlive(pid)) continue;\n\t\tconst path = join(\"/tmp\", entry);\n\t\ttry {\n\t\t\tconst stat = lstatSync(path);\n\t\t\tif (!stat.isDirectory() || stat.uid !== process.getuid?.()) continue;\n\t\t\trmSync(path, { force: true, recursive: true });\n\t\t} catch {\n\t\t\t// Another launch may have reclaimed it first.\n\t\t}\n\t}\n}\n\nexport interface CredentialChannelPaths {\n\treadonly hostSocketDirectory: string;\n\treadonly hostSocketPath: string;\n\treadonly childSocketPath: string;\n}\n\n/**\n * Allocate the endpoint beneath a private fixed-root directory.\n *\n * Do not honor TMPDIR here. It can point inside the workspace on macOS, where the\n * sandbox may write it. A fresh 0700 directory under /tmp also removes the\n * listen-before-chmod authorization race: no other account can reach the socket while\n * Node applies its initial umask-derived mode.\n */\nexport function resolveCredentialChannelPaths(): CredentialChannelPaths {\n\treclaimStaleCredentialChannelDirectories();\n\tconst hostSocketDirectory = mkdtempSync(`/tmp/apex-cred-${process.pid}-`, { encoding: \"utf8\" });\n\tchmodSync(hostSocketDirectory, 0o700);\n\tconst hostSocketPath = join(hostSocketDirectory, \"channel.sock\");\n\tif (Buffer.byteLength(hostSocketPath) + 1 > SUN_PATH_LIMIT) {\n\t\trmSync(hostSocketDirectory, { force: true, recursive: true });\n\t\tthrow new Error(\"Credential channel path exceeds the Unix socket path limit.\");\n\t}\n\tconst childSocketPath = process.platform === \"linux\" ? \"/home/apex-credential-channel.sock\" : hostSocketPath;\n\treturn { hostSocketDirectory, hostSocketPath, childSocketPath };\n}\n\nexport interface CredentialProxy {\n\tclose(): Promise<void>;\n}\n\nexport interface CredentialConfigReference {\n\treadonly field: string;\n\treadonly kind: \"command\" | \"environment\";\n}\n\n/** Find a config-value reference without recursively walking attacker-controlled input. */\nexport function findCredentialConfigReference(credential: unknown): CredentialConfigReference | undefined {\n\tconst pending: Array<{ value: unknown; field: string }> = [{ value: credential, field: \"\" }];\n\twhile (pending.length > 0) {\n\t\tconst current = pending.pop();\n\t\tif (!current) break;\n\t\tif (typeof current.value === \"string\") {\n\t\t\tif (isCommandConfigValue(current.value)) return { field: current.field, kind: \"command\" };\n\t\t\tif (getConfigValueEnvVarNames(current.value).length > 0) {\n\t\t\t\treturn { field: current.field, kind: \"environment\" };\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (Array.isArray(current.value)) {\n\t\t\tfor (let index = current.value.length - 1; index >= 0; index--) {\n\t\t\t\tpending.push({ value: current.value[index], field: `${current.field}[${index}]` });\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (current.value !== null && typeof current.value === \"object\") {\n\t\t\tconst entries = Object.entries(current.value);\n\t\t\tfor (let index = entries.length - 1; index >= 0; index--) {\n\t\t\t\tconst [key, value] = entries[index]!;\n\t\t\t\tpending.push({ value, field: current.field ? `${current.field}.${key}` : key });\n\t\t\t}\n\t\t}\n\t}\n\treturn undefined;\n}\n\nfunction isStringRecord(value: unknown): value is Record<string, string> {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\t!Array.isArray(value) &&\n\t\tObject.values(value).every((entry) => typeof entry === \"string\")\n\t);\n}\n\n/** Runtime validation at the untrusted child-to-host boundary. */\nexport function isCredential(value: unknown): value is Credential {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) return false;\n\tconst credential = value as Record<string, unknown>;\n\tif (credential.type === \"api_key\") {\n\t\treturn (\n\t\t\t(credential.key === undefined || typeof credential.key === \"string\") &&\n\t\t\t(credential.env === undefined || isStringRecord(credential.env))\n\t\t);\n\t}\n\treturn (\n\t\tcredential.type === \"oauth\" &&\n\t\ttypeof credential.access === \"string\" &&\n\t\ttypeof credential.refresh === \"string\" &&\n\t\ttypeof credential.expires === \"number\" &&\n\t\tNumber.isFinite(credential.expires)\n\t);\n}\n\nfunction referenceRefusal(reference: CredentialConfigReference): string {\n\treturn (\n\t\t`Credential values written from a sandboxed session must be literal secrets: field ` +\n\t\t`\"${reference.field}\" contains a ${reference.kind} reference. To store a reference, ` +\n\t\t`edit the credential file on the host directly or use the provider environment variable.`\n\t);\n}\n\ninterface StartRequest {\n\treadonly action?: unknown;\n\treadonly providerId?: unknown;\n}\n\ninterface CommitRequest {\n\treadonly action?: unknown;\n\treadonly credential?: unknown;\n}\n\nfunction writeFrame(socket: net.Socket, response: object): void {\n\tif (socket.destroyed) return;\n\tconst frame = Buffer.from(`${JSON.stringify(response)}\\n`, \"utf8\");\n\tif (frame.length > MAX_FRAME_BYTES) {\n\t\tthrow new Error(\"Credential channel response exceeded its byte limit.\");\n\t}\n\tsocket.write(frame);\n}\n\nclass FrameReader {\n\tprivate readonly socket: net.Socket;\n\tprivate readonly onProtocolRefusal: (detail: string) => void;\n\tprivate buffer = Buffer.alloc(0);\n\tprivate readonly queued: unknown[] = [];\n\tprivate readonly waiting: Array<{ resolve(value: unknown): void; reject(error: Error): void }> = [];\n\tprivate terminalError: Error | undefined;\n\n\tconstructor(socket: net.Socket, onProtocolRefusal: (detail: string) => void) {\n\t\tthis.socket = socket;\n\t\tthis.onProtocolRefusal = onProtocolRefusal;\n\t\tsocket.on(\"data\", (chunk: Buffer) => this.push(chunk));\n\t\tsocket.on(\"close\", () => this.fail(new Error(\"Credential channel client disconnected.\")));\n\t\tsocket.on(\"error\", (error) => this.fail(error));\n\t}\n\n\tnext(): Promise<unknown> {\n\t\tconst queued = this.queued.shift();\n\t\tif (queued !== undefined) return Promise.resolve(queued);\n\t\tif (this.terminalError) return Promise.reject(this.terminalError);\n\t\treturn new Promise((resolve, reject) => this.waiting.push({ resolve, reject }));\n\t}\n\n\tprivate push(chunk: Buffer): void {\n\t\tif (this.terminalError) return;\n\t\tthis.buffer = Buffer.concat([this.buffer, chunk]);\n\t\twhile (true) {\n\t\t\tconst newline = this.buffer.indexOf(0x0a);\n\t\t\tif (newline < 0) {\n\t\t\t\tif (this.buffer.length > MAX_FRAME_BYTES) this.rejectOversizedFrame();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (newline > MAX_FRAME_BYTES) {\n\t\t\t\tthis.rejectOversizedFrame();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst frame = this.buffer.subarray(0, newline);\n\t\t\tthis.buffer = this.buffer.subarray(newline + 1);\n\t\t\tif (frame.length === 0) continue;\n\t\t\tlet value: unknown;\n\t\t\ttry {\n\t\t\t\tvalue = JSON.parse(frame.toString(\"utf8\")) as unknown;\n\t\t\t} catch {\n\t\t\t\tthis.onProtocolRefusal(\"Invalid request frame: not JSON.\");\n\t\t\t\twriteFrame(this.socket, { ok: false, error: \"Invalid request frame: not JSON.\" });\n\t\t\t\tthis.fail(new Error(\"Invalid credential channel JSON frame.\"));\n\t\t\t\tthis.socket.end();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst waiter = this.waiting.shift();\n\t\t\tif (waiter) {\n\t\t\t\twaiter.resolve(value);\n\t\t\t} else if (this.queued.length === 0) {\n\t\t\t\tthis.queued.push(value);\n\t\t\t} else {\n\t\t\t\tthis.onProtocolRefusal(\"Unexpected pipelined request frame.\");\n\t\t\t\twriteFrame(this.socket, { ok: false, error: \"Unexpected pipelined request frame.\" });\n\t\t\t\tthis.fail(new Error(\"Credential channel received an unexpected pipelined frame.\"));\n\t\t\t\tthis.socket.end();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate rejectOversizedFrame(): void {\n\t\tthis.onProtocolRefusal(\"Request frame exceeded the 64 KiB byte limit.\");\n\t\twriteFrame(this.socket, { ok: false, error: \"Request frame is too large.\" });\n\t\tthis.fail(new Error(\"Credential channel frame exceeded its byte limit.\"));\n\t\tthis.socket.end();\n\t}\n\n\tprivate fail(error: Error): void {\n\t\tif (this.terminalError) return;\n\t\tthis.terminalError = error;\n\t\tfor (const waiter of this.waiting.splice(0)) waiter.reject(error);\n\t}\n}\n\nclass ChannelRefusal extends Error {\n\treadonly command: string;\n\n\tconstructor(command: string, message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"ChannelRefusal\";\n\t\tthis.command = command;\n\t}\n}\n\nfunction isRequestObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Serve serialized credential mutations on one private Unix socket. */\nexport function createCredentialProxy(options: {\n\tauthPath: string;\n\tviolationStore?: SandboxViolationStore;\n\tsocketPath: string;\n\t/** Private directory allocated by resolveCredentialChannelPaths; removed on close. */\n\tcleanupDirectory?: string;\n}): Promise<CredentialProxy> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst authStorage = AuthStorage.create(options.authPath);\n\t\tconst sockets = new Set<net.Socket>();\n\t\tconst operations = new Set<Promise<void>>();\n\t\tlet settled = false;\n\t\tlet closing: Promise<void> | undefined;\n\n\t\tconst audit = (command: string, detail: string): void => {\n\t\t\toptions.violationStore?.add({ kind: \"unknown\", command, detail, timestamp: new Date() });\n\t\t};\n\t\tconst refuse = (socket: net.Socket, command: string, error: string, detail = error): void => {\n\t\t\taudit(command, `Refused: ${detail}`);\n\t\t\twriteFrame(socket, { ok: false, error });\n\t\t};\n\n\t\tconst handleConnection = async (socket: net.Socket, reader: FrameReader, signal: AbortSignal): Promise<void> => {\n\t\t\tconst raw = await reader.next();\n\t\t\tif (!isRequestObject(raw)) {\n\t\t\t\trefuse(socket, \"credential-channel\", \"Invalid request: expected an object.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst request = raw as StartRequest;\n\t\t\tconst providerId = request.providerId;\n\t\t\tif (\n\t\t\t\ttypeof providerId !== \"string\" ||\n\t\t\t\tproviderId.length === 0 ||\n\t\t\t\tBuffer.byteLength(providerId, \"utf8\") > 256 ||\n\t\t\t\t/[\\u0000-\\u001f\\u007f-\\u009f\\u202a-\\u202e\\u2066-\\u2069]/u.test(providerId)\n\t\t\t) {\n\t\t\t\trefuse(\n\t\t\t\t\tsocket,\n\t\t\t\t\t\"credential-channel\",\n\t\t\t\t\t\"Invalid request: providerId contains unsafe characters or is too long.\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (request.action === \"delete\") {\n\t\t\t\tawait authStorage.delete(providerId, { signal });\n\t\t\t\taudit(`credential-delete ${providerId}`, \"Accepted: credential deleted through the sandbox channel.\");\n\t\t\t\twriteFrame(socket, { ok: true });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (request.action !== \"modify\") {\n\t\t\t\trefuse(socket, \"credential-channel\", 'Invalid request: action must be \"modify\" or \"delete\".');\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet changed = false;\n\t\t\tconst result = await authStorage.modify(\n\t\t\t\tproviderId,\n\t\t\t\tasync (current) => {\n\t\t\t\t\twriteFrame(socket, { type: \"current\", ...(current === undefined ? {} : { credential: current }) });\n\t\t\t\t\tconst rawCommit = await reader.next();\n\t\t\t\t\tif (!isRequestObject(rawCommit)) {\n\t\t\t\t\t\tthrow new ChannelRefusal(\n\t\t\t\t\t\t\t`credential-write ${providerId}`,\n\t\t\t\t\t\t\t\"Invalid modify response: expected an object.\",\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst commit = rawCommit as CommitRequest;\n\t\t\t\t\tif (commit.action === \"abort\") {\n\t\t\t\t\t\tthrow new ChannelRefusal(\n\t\t\t\t\t\t\t`credential-write ${providerId}`,\n\t\t\t\t\t\t\t\"Credential modification was aborted by the client.\",\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tif (commit.action === \"no_change\") return undefined;\n\t\t\t\t\tif (commit.action !== \"commit\") {\n\t\t\t\t\t\tthrow new ChannelRefusal(`credential-write ${providerId}`, \"Invalid modify response action.\");\n\t\t\t\t\t}\n\t\t\t\t\tif (!isCredential(commit.credential)) {\n\t\t\t\t\t\tthrow new ChannelRefusal(`credential-write ${providerId}`, \"Invalid credential shape.\");\n\t\t\t\t\t}\n\t\t\t\t\tconst reference = findCredentialConfigReference(commit.credential);\n\t\t\t\t\tif (reference) {\n\t\t\t\t\t\tthrow new ChannelRefusal(`credential-write ${providerId}`, referenceRefusal(reference));\n\t\t\t\t\t}\n\t\t\t\t\tchanged = true;\n\t\t\t\t\treturn commit.credential;\n\t\t\t\t},\n\t\t\t\t{ signal },\n\t\t\t);\n\t\t\tif (changed) {\n\t\t\t\taudit(`credential-write ${providerId}`, \"Accepted: credential written through the sandbox channel.\");\n\t\t\t}\n\t\t\twriteFrame(socket, { ok: true, ...(result === undefined ? {} : { credential: result }) });\n\t\t};\n\n\t\tconst server = net.createServer((socket) => {\n\t\t\tif (sockets.size >= MAX_CONNECTIONS) {\n\t\t\t\trefuse(socket, \"credential-channel\", \"Credential channel connection limit reached.\");\n\t\t\t\tsocket.end();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsockets.add(socket);\n\t\t\tconst controller = new AbortController();\n\t\t\tsocket.setTimeout(CONNECTION_IDLE_TIMEOUT_MS, () => {\n\t\t\t\taudit(\"credential-channel\", \"Refused: credential channel connection timed out.\");\n\t\t\t\tsocket.destroy(new Error(\"Credential channel connection timed out.\"));\n\t\t\t});\n\t\t\tsocket.on(\"close\", () => {\n\t\t\t\tsockets.delete(socket);\n\t\t\t\tcontroller.abort();\n\t\t\t});\n\t\t\tconst reader = new FrameReader(socket, (detail) => audit(\"credential-channel\", `Refused: ${detail}`));\n\t\t\tconst operation = handleConnection(socket, reader, controller.signal)\n\t\t\t\t.catch((error: unknown) => {\n\t\t\t\t\tif (error instanceof ChannelRefusal) {\n\t\t\t\t\t\trefuse(socket, error.command, error.message);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\trefuse(\n\t\t\t\t\t\tsocket,\n\t\t\t\t\t\t\"credential-channel\",\n\t\t\t\t\t\t\"The host credential operation failed.\",\n\t\t\t\t\t\t\"Host credential operation failed without exposing credential data.\",\n\t\t\t\t\t);\n\t\t\t\t})\n\t\t\t\t.finally(() => {\n\t\t\t\t\toperations.delete(operation);\n\t\t\t\t\tsocket.end();\n\t\t\t\t});\n\t\t\toperations.add(operation);\n\t\t});\n\n\t\tconst cleanup = (): void => {\n\t\t\trmSync(options.socketPath, { force: true });\n\t\t\tif (options.cleanupDirectory) rmSync(options.cleanupDirectory, { force: true, recursive: true });\n\t\t};\n\t\tconst failStartup = (error: Error): void => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tserver.close();\n\t\t\tfor (const socket of sockets) socket.destroy();\n\t\t\tcleanup();\n\t\t\treject(error);\n\t\t};\n\t\tserver.once(\"error\", failStartup);\n\t\tserver.listen(options.socketPath, () => {\n\t\t\ttry {\n\t\t\t\tchmodSync(options.socketPath, 0o600);\n\t\t\t} catch (error) {\n\t\t\t\tfailStartup(error instanceof Error ? error : new Error(String(error)));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsettled = true;\n\t\t\tserver.off(\"error\", failStartup);\n\t\t\tresolve({\n\t\t\t\tclose: () => {\n\t\t\t\t\tif (closing) return closing;\n\t\t\t\t\tclosing = (async () => {\n\t\t\t\t\t\tfor (const socket of sockets) socket.destroy();\n\t\t\t\t\t\tawait new Promise<void>((resolveClose) => server.close(() => resolveClose()));\n\t\t\t\t\t\tawait Promise.allSettled([...operations]);\n\t\t\t\t\t\tcleanup();\n\t\t\t\t\t})();\n\t\t\t\t\treturn closing;\n\t\t\t\t},\n\t\t\t});\n\t\t});\n\t});\n}\n"]}
|
|
1
|
+
{"version":3,"file":"credential-proxy.d.ts","sourceRoot":"","sources":["../../../../src/core/sandbox/rpc/credential-proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAGxD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AA0C9D,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CACjC;AAED;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,IAAI,sBAAsB,CAWtE;AAED,MAAM,WAAW,eAAe;IAC/B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,MAAM,WAAW,yBAAyB;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,aAAa,CAAC;CACzC;AAED,2FAA2F;AAC3F,wBAAgB,6BAA6B,CAAC,UAAU,EAAE,OAAO,GAAG,yBAAyB,GAAG,SAAS,CA2BxG;AAWD,kEAAkE;AAClE,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAgBhE;AA8BD,wEAAwE;AACxE,wBAAgB,qBAAqB,CAAC,OAAO,EAAE;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,qBAAqB,CAAC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,sFAAsF;IACtF,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B,GAAG,OAAO,CAAC,eAAe,CAAC,CAoK3B","sourcesContent":["/**\n * Supervisor-owned credential mutation service for sandboxed sessions.\n *\n * Reads stay on the read-only auth.json projection. Mutations cross this Unix socket\n * and execute under the host AuthStorage lock. The child can therefore implement the\n * CredentialStore serialized read-modify-write contract without gaining filesystem\n * write access to the credential file.\n */\n\nimport { chmodSync, lstatSync, mkdtempSync, readdirSync, rmSync } from \"node:fs\";\nimport * as net from \"node:net\";\nimport { join } from \"node:path\";\nimport type { Credential } from \"@earendil-works/pi-ai\";\nimport { AuthStorage } from \"../../auth-storage.ts\";\nimport { getConfigValueEnvVarNames, isCommandConfigValue } from \"../../resolve-config-value.ts\";\nimport type { SandboxViolationStore } from \"../violations.ts\";\nimport { FrameReader, isRequestObject, writeFrame } from \"./framing.ts\";\n\n/** AF_UNIX `sun_path` is 108 bytes on Linux, including the terminating NUL. */\nconst SUN_PATH_LIMIT = 108;\n/** Credentials are small; reject larger frames before UTF-8 decoding or JSON parsing. */\nconst MAX_CONNECTIONS = 32;\nconst CONNECTION_IDLE_TIMEOUT_MS = 30_000;\n\nfunction processIsAlive(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (error) {\n\t\treturn (error as NodeJS.ErrnoException).code === \"EPERM\";\n\t}\n}\n\n/** Reclaim only same-user directories whose encoded supervisor PID is dead. */\nfunction reclaimStaleCredentialChannelDirectories(): void {\n\tlet entries: string[];\n\ttry {\n\t\tentries = readdirSync(\"/tmp\");\n\t} catch {\n\t\treturn;\n\t}\n\tfor (const entry of entries) {\n\t\tconst match = /^apex-cred-(\\d+)-[A-Za-z0-9_-]+$/.exec(entry);\n\t\tif (!match) continue;\n\t\tconst pid = Number(match[1]);\n\t\tif (!Number.isSafeInteger(pid) || pid < 1 || processIsAlive(pid)) continue;\n\t\tconst path = join(\"/tmp\", entry);\n\t\ttry {\n\t\t\tconst stat = lstatSync(path);\n\t\t\tif (!stat.isDirectory() || stat.uid !== process.getuid?.()) continue;\n\t\t\trmSync(path, { force: true, recursive: true });\n\t\t} catch {\n\t\t\t// Another launch may have reclaimed it first.\n\t\t}\n\t}\n}\n\nexport interface CredentialChannelPaths {\n\treadonly hostSocketDirectory: string;\n\treadonly hostSocketPath: string;\n\treadonly childSocketPath: string;\n}\n\n/**\n * Allocate the endpoint beneath a private fixed-root directory.\n *\n * Do not honor TMPDIR here. It can point inside the workspace on macOS, where the\n * sandbox may write it. A fresh 0700 directory under /tmp also removes the\n * listen-before-chmod authorization race: no other account can reach the socket while\n * Node applies its initial umask-derived mode.\n */\nexport function resolveCredentialChannelPaths(): CredentialChannelPaths {\n\treclaimStaleCredentialChannelDirectories();\n\tconst hostSocketDirectory = mkdtempSync(`/tmp/apex-cred-${process.pid}-`, { encoding: \"utf8\" });\n\tchmodSync(hostSocketDirectory, 0o700);\n\tconst hostSocketPath = join(hostSocketDirectory, \"channel.sock\");\n\tif (Buffer.byteLength(hostSocketPath) + 1 > SUN_PATH_LIMIT) {\n\t\trmSync(hostSocketDirectory, { force: true, recursive: true });\n\t\tthrow new Error(\"Credential channel path exceeds the Unix socket path limit.\");\n\t}\n\tconst childSocketPath = process.platform === \"linux\" ? \"/home/apex-credential-channel.sock\" : hostSocketPath;\n\treturn { hostSocketDirectory, hostSocketPath, childSocketPath };\n}\n\nexport interface CredentialProxy {\n\tclose(): Promise<void>;\n}\n\nexport interface CredentialConfigReference {\n\treadonly field: string;\n\treadonly kind: \"command\" | \"environment\";\n}\n\n/** Find a config-value reference without recursively walking attacker-controlled input. */\nexport function findCredentialConfigReference(credential: unknown): CredentialConfigReference | undefined {\n\tconst pending: Array<{ value: unknown; field: string }> = [{ value: credential, field: \"\" }];\n\twhile (pending.length > 0) {\n\t\tconst current = pending.pop();\n\t\tif (!current) break;\n\t\tif (typeof current.value === \"string\") {\n\t\t\tif (isCommandConfigValue(current.value)) return { field: current.field, kind: \"command\" };\n\t\t\tif (getConfigValueEnvVarNames(current.value).length > 0) {\n\t\t\t\treturn { field: current.field, kind: \"environment\" };\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (Array.isArray(current.value)) {\n\t\t\tfor (let index = current.value.length - 1; index >= 0; index--) {\n\t\t\t\tpending.push({ value: current.value[index], field: `${current.field}[${index}]` });\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (current.value !== null && typeof current.value === \"object\") {\n\t\t\tconst entries = Object.entries(current.value);\n\t\t\tfor (let index = entries.length - 1; index >= 0; index--) {\n\t\t\t\tconst [key, value] = entries[index]!;\n\t\t\t\tpending.push({ value, field: current.field ? `${current.field}.${key}` : key });\n\t\t\t}\n\t\t}\n\t}\n\treturn undefined;\n}\n\nfunction isStringRecord(value: unknown): value is Record<string, string> {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\t!Array.isArray(value) &&\n\t\tObject.values(value).every((entry) => typeof entry === \"string\")\n\t);\n}\n\n/** Runtime validation at the untrusted child-to-host boundary. */\nexport function isCredential(value: unknown): value is Credential {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) return false;\n\tconst credential = value as Record<string, unknown>;\n\tif (credential.type === \"api_key\") {\n\t\treturn (\n\t\t\t(credential.key === undefined || typeof credential.key === \"string\") &&\n\t\t\t(credential.env === undefined || isStringRecord(credential.env))\n\t\t);\n\t}\n\treturn (\n\t\tcredential.type === \"oauth\" &&\n\t\ttypeof credential.access === \"string\" &&\n\t\ttypeof credential.refresh === \"string\" &&\n\t\ttypeof credential.expires === \"number\" &&\n\t\tNumber.isFinite(credential.expires)\n\t);\n}\n\nfunction referenceRefusal(reference: CredentialConfigReference): string {\n\treturn (\n\t\t`Credential values written from a sandboxed session must be literal secrets: field ` +\n\t\t`\"${reference.field}\" contains a ${reference.kind} reference. To store a reference, ` +\n\t\t`edit the credential file on the host directly or use the provider environment variable.`\n\t);\n}\n\ninterface StartRequest {\n\treadonly action?: unknown;\n\treadonly providerId?: unknown;\n}\n\ninterface CommitRequest {\n\treadonly action?: unknown;\n\treadonly credential?: unknown;\n}\n\nclass ChannelRefusal extends Error {\n\treadonly command: string;\n\n\tconstructor(command: string, message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"ChannelRefusal\";\n\t\tthis.command = command;\n\t}\n}\n\n/** Serve serialized credential mutations on one private Unix socket. */\nexport function createCredentialProxy(options: {\n\tauthPath: string;\n\tviolationStore?: SandboxViolationStore;\n\tsocketPath: string;\n\t/** Private directory allocated by resolveCredentialChannelPaths; removed on close. */\n\tcleanupDirectory?: string;\n}): Promise<CredentialProxy> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst authStorage = AuthStorage.create(options.authPath);\n\t\tconst sockets = new Set<net.Socket>();\n\t\tconst operations = new Set<Promise<void>>();\n\t\tlet settled = false;\n\t\tlet closing: Promise<void> | undefined;\n\n\t\tconst audit = (command: string, detail: string): void => {\n\t\t\toptions.violationStore?.add({ kind: \"unknown\", command, detail, timestamp: new Date() });\n\t\t};\n\t\tconst refuse = (socket: net.Socket, command: string, error: string, detail = error): void => {\n\t\t\taudit(command, `Refused: ${detail}`);\n\t\t\twriteFrame(socket, { ok: false, error });\n\t\t};\n\n\t\tconst handleConnection = async (socket: net.Socket, reader: FrameReader, signal: AbortSignal): Promise<void> => {\n\t\t\tconst raw = await reader.next();\n\t\t\tif (!isRequestObject(raw)) {\n\t\t\t\trefuse(socket, \"credential-channel\", \"Invalid request: expected an object.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst request = raw as StartRequest;\n\t\t\tconst providerId = request.providerId;\n\t\t\tif (\n\t\t\t\ttypeof providerId !== \"string\" ||\n\t\t\t\tproviderId.length === 0 ||\n\t\t\t\tBuffer.byteLength(providerId, \"utf8\") > 256 ||\n\t\t\t\t/[\\u0000-\\u001f\\u007f-\\u009f\\u202a-\\u202e\\u2066-\\u2069]/u.test(providerId)\n\t\t\t) {\n\t\t\t\trefuse(\n\t\t\t\t\tsocket,\n\t\t\t\t\t\"credential-channel\",\n\t\t\t\t\t\"Invalid request: providerId contains unsafe characters or is too long.\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (request.action === \"delete\") {\n\t\t\t\tawait authStorage.delete(providerId, { signal });\n\t\t\t\taudit(`credential-delete ${providerId}`, \"Accepted: credential deleted through the sandbox channel.\");\n\t\t\t\twriteFrame(socket, { ok: true });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (request.action !== \"modify\") {\n\t\t\t\trefuse(socket, \"credential-channel\", 'Invalid request: action must be \"modify\" or \"delete\".');\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet changed = false;\n\t\t\tconst result = await authStorage.modify(\n\t\t\t\tproviderId,\n\t\t\t\tasync (current) => {\n\t\t\t\t\twriteFrame(socket, { type: \"current\", ...(current === undefined ? {} : { credential: current }) });\n\t\t\t\t\tconst rawCommit = await reader.next();\n\t\t\t\t\tif (!isRequestObject(rawCommit)) {\n\t\t\t\t\t\tthrow new ChannelRefusal(\n\t\t\t\t\t\t\t`credential-write ${providerId}`,\n\t\t\t\t\t\t\t\"Invalid modify response: expected an object.\",\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst commit = rawCommit as CommitRequest;\n\t\t\t\t\tif (commit.action === \"abort\") {\n\t\t\t\t\t\tthrow new ChannelRefusal(\n\t\t\t\t\t\t\t`credential-write ${providerId}`,\n\t\t\t\t\t\t\t\"Credential modification was aborted by the client.\",\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tif (commit.action === \"no_change\") return undefined;\n\t\t\t\t\tif (commit.action !== \"commit\") {\n\t\t\t\t\t\tthrow new ChannelRefusal(`credential-write ${providerId}`, \"Invalid modify response action.\");\n\t\t\t\t\t}\n\t\t\t\t\tif (!isCredential(commit.credential)) {\n\t\t\t\t\t\tthrow new ChannelRefusal(`credential-write ${providerId}`, \"Invalid credential shape.\");\n\t\t\t\t\t}\n\t\t\t\t\tconst reference = findCredentialConfigReference(commit.credential);\n\t\t\t\t\tif (reference) {\n\t\t\t\t\t\tthrow new ChannelRefusal(`credential-write ${providerId}`, referenceRefusal(reference));\n\t\t\t\t\t}\n\t\t\t\t\tchanged = true;\n\t\t\t\t\treturn commit.credential;\n\t\t\t\t},\n\t\t\t\t{ signal },\n\t\t\t);\n\t\t\tif (changed) {\n\t\t\t\taudit(`credential-write ${providerId}`, \"Accepted: credential written through the sandbox channel.\");\n\t\t\t}\n\t\t\twriteFrame(socket, { ok: true, ...(result === undefined ? {} : { credential: result }) });\n\t\t};\n\n\t\tconst server = net.createServer((socket) => {\n\t\t\tif (sockets.size >= MAX_CONNECTIONS) {\n\t\t\t\trefuse(socket, \"credential-channel\", \"Credential channel connection limit reached.\");\n\t\t\t\tsocket.end();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsockets.add(socket);\n\t\t\tconst controller = new AbortController();\n\t\t\tsocket.setTimeout(CONNECTION_IDLE_TIMEOUT_MS, () => {\n\t\t\t\taudit(\"credential-channel\", \"Refused: credential channel connection timed out.\");\n\t\t\t\tsocket.destroy(new Error(\"Credential channel connection timed out.\"));\n\t\t\t});\n\t\t\tsocket.on(\"close\", () => {\n\t\t\t\tsockets.delete(socket);\n\t\t\t\tcontroller.abort();\n\t\t\t});\n\t\t\tconst reader = new FrameReader(socket, \"Credential channel\", (detail) =>\n\t\t\t\taudit(\"credential-channel\", `Refused: ${detail}`),\n\t\t\t);\n\t\t\tconst operation = handleConnection(socket, reader, controller.signal)\n\t\t\t\t.catch((error: unknown) => {\n\t\t\t\t\tif (error instanceof ChannelRefusal) {\n\t\t\t\t\t\trefuse(socket, error.command, error.message);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\trefuse(\n\t\t\t\t\t\tsocket,\n\t\t\t\t\t\t\"credential-channel\",\n\t\t\t\t\t\t\"The host credential operation failed.\",\n\t\t\t\t\t\t\"Host credential operation failed without exposing credential data.\",\n\t\t\t\t\t);\n\t\t\t\t})\n\t\t\t\t.finally(() => {\n\t\t\t\t\toperations.delete(operation);\n\t\t\t\t\tsocket.end();\n\t\t\t\t});\n\t\t\toperations.add(operation);\n\t\t});\n\n\t\tconst cleanup = (): void => {\n\t\t\trmSync(options.socketPath, { force: true });\n\t\t\tif (options.cleanupDirectory) rmSync(options.cleanupDirectory, { force: true, recursive: true });\n\t\t};\n\t\tconst failStartup = (error: Error): void => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tserver.close();\n\t\t\tfor (const socket of sockets) socket.destroy();\n\t\t\tcleanup();\n\t\t\treject(error);\n\t\t};\n\t\tserver.once(\"error\", failStartup);\n\t\tserver.listen(options.socketPath, () => {\n\t\t\ttry {\n\t\t\t\tchmodSync(options.socketPath, 0o600);\n\t\t\t} catch (error) {\n\t\t\t\tfailStartup(error instanceof Error ? error : new Error(String(error)));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsettled = true;\n\t\t\tserver.off(\"error\", failStartup);\n\t\t\tresolve({\n\t\t\t\tclose: () => {\n\t\t\t\t\tif (closing) return closing;\n\t\t\t\t\tclosing = (async () => {\n\t\t\t\t\t\tfor (const socket of sockets) socket.destroy();\n\t\t\t\t\t\tawait new Promise<void>((resolveClose) => server.close(() => resolveClose()));\n\t\t\t\t\t\tawait Promise.allSettled([...operations]);\n\t\t\t\t\t\tcleanup();\n\t\t\t\t\t})();\n\t\t\t\t\treturn closing;\n\t\t\t\t},\n\t\t\t});\n\t\t});\n\t});\n}\n"]}
|
|
@@ -11,10 +11,10 @@ import * as net from "node:net";
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { AuthStorage } from "../../auth-storage.js";
|
|
13
13
|
import { getConfigValueEnvVarNames, isCommandConfigValue } from "../../resolve-config-value.js";
|
|
14
|
+
import { FrameReader, isRequestObject, writeFrame } from "./framing.js";
|
|
14
15
|
/** AF_UNIX `sun_path` is 108 bytes on Linux, including the terminating NUL. */
|
|
15
16
|
const SUN_PATH_LIMIT = 108;
|
|
16
17
|
/** Credentials are small; reject larger frames before UTF-8 decoding or JSON parsing. */
|
|
17
|
-
const MAX_FRAME_BYTES = 64 * 1024;
|
|
18
18
|
const MAX_CONNECTIONS = 32;
|
|
19
19
|
const CONNECTION_IDLE_TIMEOUT_MS = 30_000;
|
|
20
20
|
function processIsAlive(pid) {
|
|
@@ -131,97 +131,6 @@ function referenceRefusal(reference) {
|
|
|
131
131
|
`"${reference.field}" contains a ${reference.kind} reference. To store a reference, ` +
|
|
132
132
|
`edit the credential file on the host directly or use the provider environment variable.`);
|
|
133
133
|
}
|
|
134
|
-
function writeFrame(socket, response) {
|
|
135
|
-
if (socket.destroyed)
|
|
136
|
-
return;
|
|
137
|
-
const frame = Buffer.from(`${JSON.stringify(response)}\n`, "utf8");
|
|
138
|
-
if (frame.length > MAX_FRAME_BYTES) {
|
|
139
|
-
throw new Error("Credential channel response exceeded its byte limit.");
|
|
140
|
-
}
|
|
141
|
-
socket.write(frame);
|
|
142
|
-
}
|
|
143
|
-
class FrameReader {
|
|
144
|
-
socket;
|
|
145
|
-
onProtocolRefusal;
|
|
146
|
-
buffer = Buffer.alloc(0);
|
|
147
|
-
queued = [];
|
|
148
|
-
waiting = [];
|
|
149
|
-
terminalError;
|
|
150
|
-
constructor(socket, onProtocolRefusal) {
|
|
151
|
-
this.socket = socket;
|
|
152
|
-
this.onProtocolRefusal = onProtocolRefusal;
|
|
153
|
-
socket.on("data", (chunk) => this.push(chunk));
|
|
154
|
-
socket.on("close", () => this.fail(new Error("Credential channel client disconnected.")));
|
|
155
|
-
socket.on("error", (error) => this.fail(error));
|
|
156
|
-
}
|
|
157
|
-
next() {
|
|
158
|
-
const queued = this.queued.shift();
|
|
159
|
-
if (queued !== undefined)
|
|
160
|
-
return Promise.resolve(queued);
|
|
161
|
-
if (this.terminalError)
|
|
162
|
-
return Promise.reject(this.terminalError);
|
|
163
|
-
return new Promise((resolve, reject) => this.waiting.push({ resolve, reject }));
|
|
164
|
-
}
|
|
165
|
-
push(chunk) {
|
|
166
|
-
if (this.terminalError)
|
|
167
|
-
return;
|
|
168
|
-
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
169
|
-
while (true) {
|
|
170
|
-
const newline = this.buffer.indexOf(0x0a);
|
|
171
|
-
if (newline < 0) {
|
|
172
|
-
if (this.buffer.length > MAX_FRAME_BYTES)
|
|
173
|
-
this.rejectOversizedFrame();
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
if (newline > MAX_FRAME_BYTES) {
|
|
177
|
-
this.rejectOversizedFrame();
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
const frame = this.buffer.subarray(0, newline);
|
|
181
|
-
this.buffer = this.buffer.subarray(newline + 1);
|
|
182
|
-
if (frame.length === 0)
|
|
183
|
-
continue;
|
|
184
|
-
let value;
|
|
185
|
-
try {
|
|
186
|
-
value = JSON.parse(frame.toString("utf8"));
|
|
187
|
-
}
|
|
188
|
-
catch {
|
|
189
|
-
this.onProtocolRefusal("Invalid request frame: not JSON.");
|
|
190
|
-
writeFrame(this.socket, { ok: false, error: "Invalid request frame: not JSON." });
|
|
191
|
-
this.fail(new Error("Invalid credential channel JSON frame."));
|
|
192
|
-
this.socket.end();
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
const waiter = this.waiting.shift();
|
|
196
|
-
if (waiter) {
|
|
197
|
-
waiter.resolve(value);
|
|
198
|
-
}
|
|
199
|
-
else if (this.queued.length === 0) {
|
|
200
|
-
this.queued.push(value);
|
|
201
|
-
}
|
|
202
|
-
else {
|
|
203
|
-
this.onProtocolRefusal("Unexpected pipelined request frame.");
|
|
204
|
-
writeFrame(this.socket, { ok: false, error: "Unexpected pipelined request frame." });
|
|
205
|
-
this.fail(new Error("Credential channel received an unexpected pipelined frame."));
|
|
206
|
-
this.socket.end();
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
rejectOversizedFrame() {
|
|
212
|
-
this.onProtocolRefusal("Request frame exceeded the 64 KiB byte limit.");
|
|
213
|
-
writeFrame(this.socket, { ok: false, error: "Request frame is too large." });
|
|
214
|
-
this.fail(new Error("Credential channel frame exceeded its byte limit."));
|
|
215
|
-
this.socket.end();
|
|
216
|
-
}
|
|
217
|
-
fail(error) {
|
|
218
|
-
if (this.terminalError)
|
|
219
|
-
return;
|
|
220
|
-
this.terminalError = error;
|
|
221
|
-
for (const waiter of this.waiting.splice(0))
|
|
222
|
-
waiter.reject(error);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
134
|
class ChannelRefusal extends Error {
|
|
226
135
|
command;
|
|
227
136
|
constructor(command, message) {
|
|
@@ -230,9 +139,6 @@ class ChannelRefusal extends Error {
|
|
|
230
139
|
this.command = command;
|
|
231
140
|
}
|
|
232
141
|
}
|
|
233
|
-
function isRequestObject(value) {
|
|
234
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
235
|
-
}
|
|
236
142
|
/** Serve serialized credential mutations on one private Unix socket. */
|
|
237
143
|
export function createCredentialProxy(options) {
|
|
238
144
|
return new Promise((resolve, reject) => {
|
|
@@ -320,7 +226,7 @@ export function createCredentialProxy(options) {
|
|
|
320
226
|
sockets.delete(socket);
|
|
321
227
|
controller.abort();
|
|
322
228
|
});
|
|
323
|
-
const reader = new FrameReader(socket, (detail) => audit("credential-channel", `Refused: ${detail}`));
|
|
229
|
+
const reader = new FrameReader(socket, "Credential channel", (detail) => audit("credential-channel", `Refused: ${detail}`));
|
|
324
230
|
const operation = handleConnection(socket, reader, controller.signal)
|
|
325
231
|
.catch((error) => {
|
|
326
232
|
if (error instanceof ChannelRefusal) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"credential-proxy.js","sourceRoot":"","sources":["../../../../src/core/sandbox/rpc/credential-proxy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Supervisor-owned credential mutation service for sandboxed sessions.\n *\n * Reads stay on the read-only auth.json projection. Mutations cross this Unix socket\n * and execute under the host AuthStorage lock. The child can therefore implement the\n * CredentialStore serialized read-modify-write contract without gaining filesystem\n * write access to the credential file.\n */\n\nimport { chmodSync, lstatSync, mkdtempSync, readdirSync, rmSync } from \"node:fs\";\nimport * as net from \"node:net\";\nimport { join } from \"node:path\";\nimport type { Credential } from \"@earendil-works/pi-ai\";\nimport { AuthStorage } from \"../../auth-storage.ts\";\nimport { getConfigValueEnvVarNames, isCommandConfigValue } from \"../../resolve-config-value.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/** Credentials are small; reject larger frames before UTF-8 decoding or JSON parsing. */\nconst MAX_FRAME_BYTES = 64 * 1024;\nconst MAX_CONNECTIONS = 32;\nconst CONNECTION_IDLE_TIMEOUT_MS = 30_000;\n\nfunction processIsAlive(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (error) {\n\t\treturn (error as NodeJS.ErrnoException).code === \"EPERM\";\n\t}\n}\n\n/** Reclaim only same-user directories whose encoded supervisor PID is dead. */\nfunction reclaimStaleCredentialChannelDirectories(): void {\n\tlet entries: string[];\n\ttry {\n\t\tentries = readdirSync(\"/tmp\");\n\t} catch {\n\t\treturn;\n\t}\n\tfor (const entry of entries) {\n\t\tconst match = /^apex-cred-(\\d+)-[A-Za-z0-9_-]+$/.exec(entry);\n\t\tif (!match) continue;\n\t\tconst pid = Number(match[1]);\n\t\tif (!Number.isSafeInteger(pid) || pid < 1 || processIsAlive(pid)) continue;\n\t\tconst path = join(\"/tmp\", entry);\n\t\ttry {\n\t\t\tconst stat = lstatSync(path);\n\t\t\tif (!stat.isDirectory() || stat.uid !== process.getuid?.()) continue;\n\t\t\trmSync(path, { force: true, recursive: true });\n\t\t} catch {\n\t\t\t// Another launch may have reclaimed it first.\n\t\t}\n\t}\n}\n\nexport interface CredentialChannelPaths {\n\treadonly hostSocketDirectory: string;\n\treadonly hostSocketPath: string;\n\treadonly childSocketPath: string;\n}\n\n/**\n * Allocate the endpoint beneath a private fixed-root directory.\n *\n * Do not honor TMPDIR here. It can point inside the workspace on macOS, where the\n * sandbox may write it. A fresh 0700 directory under /tmp also removes the\n * listen-before-chmod authorization race: no other account can reach the socket while\n * Node applies its initial umask-derived mode.\n */\nexport function resolveCredentialChannelPaths(): CredentialChannelPaths {\n\treclaimStaleCredentialChannelDirectories();\n\tconst hostSocketDirectory = mkdtempSync(`/tmp/apex-cred-${process.pid}-`, { encoding: \"utf8\" });\n\tchmodSync(hostSocketDirectory, 0o700);\n\tconst hostSocketPath = join(hostSocketDirectory, \"channel.sock\");\n\tif (Buffer.byteLength(hostSocketPath) + 1 > SUN_PATH_LIMIT) {\n\t\trmSync(hostSocketDirectory, { force: true, recursive: true });\n\t\tthrow new Error(\"Credential channel path exceeds the Unix socket path limit.\");\n\t}\n\tconst childSocketPath = process.platform === \"linux\" ? \"/home/apex-credential-channel.sock\" : hostSocketPath;\n\treturn { hostSocketDirectory, hostSocketPath, childSocketPath };\n}\n\nexport interface CredentialProxy {\n\tclose(): Promise<void>;\n}\n\nexport interface CredentialConfigReference {\n\treadonly field: string;\n\treadonly kind: \"command\" | \"environment\";\n}\n\n/** Find a config-value reference without recursively walking attacker-controlled input. */\nexport function findCredentialConfigReference(credential: unknown): CredentialConfigReference | undefined {\n\tconst pending: Array<{ value: unknown; field: string }> = [{ value: credential, field: \"\" }];\n\twhile (pending.length > 0) {\n\t\tconst current = pending.pop();\n\t\tif (!current) break;\n\t\tif (typeof current.value === \"string\") {\n\t\t\tif (isCommandConfigValue(current.value)) return { field: current.field, kind: \"command\" };\n\t\t\tif (getConfigValueEnvVarNames(current.value).length > 0) {\n\t\t\t\treturn { field: current.field, kind: \"environment\" };\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (Array.isArray(current.value)) {\n\t\t\tfor (let index = current.value.length - 1; index >= 0; index--) {\n\t\t\t\tpending.push({ value: current.value[index], field: `${current.field}[${index}]` });\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (current.value !== null && typeof current.value === \"object\") {\n\t\t\tconst entries = Object.entries(current.value);\n\t\t\tfor (let index = entries.length - 1; index >= 0; index--) {\n\t\t\t\tconst [key, value] = entries[index]!;\n\t\t\t\tpending.push({ value, field: current.field ? `${current.field}.${key}` : key });\n\t\t\t}\n\t\t}\n\t}\n\treturn undefined;\n}\n\nfunction isStringRecord(value: unknown): value is Record<string, string> {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\t!Array.isArray(value) &&\n\t\tObject.values(value).every((entry) => typeof entry === \"string\")\n\t);\n}\n\n/** Runtime validation at the untrusted child-to-host boundary. */\nexport function isCredential(value: unknown): value is Credential {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) return false;\n\tconst credential = value as Record<string, unknown>;\n\tif (credential.type === \"api_key\") {\n\t\treturn (\n\t\t\t(credential.key === undefined || typeof credential.key === \"string\") &&\n\t\t\t(credential.env === undefined || isStringRecord(credential.env))\n\t\t);\n\t}\n\treturn (\n\t\tcredential.type === \"oauth\" &&\n\t\ttypeof credential.access === \"string\" &&\n\t\ttypeof credential.refresh === \"string\" &&\n\t\ttypeof credential.expires === \"number\" &&\n\t\tNumber.isFinite(credential.expires)\n\t);\n}\n\nfunction referenceRefusal(reference: CredentialConfigReference): string {\n\treturn (\n\t\t`Credential values written from a sandboxed session must be literal secrets: field ` +\n\t\t`\"${reference.field}\" contains a ${reference.kind} reference. To store a reference, ` +\n\t\t`edit the credential file on the host directly or use the provider environment variable.`\n\t);\n}\n\ninterface StartRequest {\n\treadonly action?: unknown;\n\treadonly providerId?: unknown;\n}\n\ninterface CommitRequest {\n\treadonly action?: unknown;\n\treadonly credential?: unknown;\n}\n\nfunction writeFrame(socket: net.Socket, response: object): void {\n\tif (socket.destroyed) return;\n\tconst frame = Buffer.from(`${JSON.stringify(response)}\\n`, \"utf8\");\n\tif (frame.length > MAX_FRAME_BYTES) {\n\t\tthrow new Error(\"Credential channel response exceeded its byte limit.\");\n\t}\n\tsocket.write(frame);\n}\n\nclass FrameReader {\n\tprivate readonly socket: net.Socket;\n\tprivate readonly onProtocolRefusal: (detail: string) => void;\n\tprivate buffer = Buffer.alloc(0);\n\tprivate readonly queued: unknown[] = [];\n\tprivate readonly waiting: Array<{ resolve(value: unknown): void; reject(error: Error): void }> = [];\n\tprivate terminalError: Error | undefined;\n\n\tconstructor(socket: net.Socket, onProtocolRefusal: (detail: string) => void) {\n\t\tthis.socket = socket;\n\t\tthis.onProtocolRefusal = onProtocolRefusal;\n\t\tsocket.on(\"data\", (chunk: Buffer) => this.push(chunk));\n\t\tsocket.on(\"close\", () => this.fail(new Error(\"Credential channel client disconnected.\")));\n\t\tsocket.on(\"error\", (error) => this.fail(error));\n\t}\n\n\tnext(): Promise<unknown> {\n\t\tconst queued = this.queued.shift();\n\t\tif (queued !== undefined) return Promise.resolve(queued);\n\t\tif (this.terminalError) return Promise.reject(this.terminalError);\n\t\treturn new Promise((resolve, reject) => this.waiting.push({ resolve, reject }));\n\t}\n\n\tprivate push(chunk: Buffer): void {\n\t\tif (this.terminalError) return;\n\t\tthis.buffer = Buffer.concat([this.buffer, chunk]);\n\t\twhile (true) {\n\t\t\tconst newline = this.buffer.indexOf(0x0a);\n\t\t\tif (newline < 0) {\n\t\t\t\tif (this.buffer.length > MAX_FRAME_BYTES) this.rejectOversizedFrame();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (newline > MAX_FRAME_BYTES) {\n\t\t\t\tthis.rejectOversizedFrame();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst frame = this.buffer.subarray(0, newline);\n\t\t\tthis.buffer = this.buffer.subarray(newline + 1);\n\t\t\tif (frame.length === 0) continue;\n\t\t\tlet value: unknown;\n\t\t\ttry {\n\t\t\t\tvalue = JSON.parse(frame.toString(\"utf8\")) as unknown;\n\t\t\t} catch {\n\t\t\t\tthis.onProtocolRefusal(\"Invalid request frame: not JSON.\");\n\t\t\t\twriteFrame(this.socket, { ok: false, error: \"Invalid request frame: not JSON.\" });\n\t\t\t\tthis.fail(new Error(\"Invalid credential channel JSON frame.\"));\n\t\t\t\tthis.socket.end();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst waiter = this.waiting.shift();\n\t\t\tif (waiter) {\n\t\t\t\twaiter.resolve(value);\n\t\t\t} else if (this.queued.length === 0) {\n\t\t\t\tthis.queued.push(value);\n\t\t\t} else {\n\t\t\t\tthis.onProtocolRefusal(\"Unexpected pipelined request frame.\");\n\t\t\t\twriteFrame(this.socket, { ok: false, error: \"Unexpected pipelined request frame.\" });\n\t\t\t\tthis.fail(new Error(\"Credential channel received an unexpected pipelined frame.\"));\n\t\t\t\tthis.socket.end();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate rejectOversizedFrame(): void {\n\t\tthis.onProtocolRefusal(\"Request frame exceeded the 64 KiB byte limit.\");\n\t\twriteFrame(this.socket, { ok: false, error: \"Request frame is too large.\" });\n\t\tthis.fail(new Error(\"Credential channel frame exceeded its byte limit.\"));\n\t\tthis.socket.end();\n\t}\n\n\tprivate fail(error: Error): void {\n\t\tif (this.terminalError) return;\n\t\tthis.terminalError = error;\n\t\tfor (const waiter of this.waiting.splice(0)) waiter.reject(error);\n\t}\n}\n\nclass ChannelRefusal extends Error {\n\treadonly command: string;\n\n\tconstructor(command: string, message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"ChannelRefusal\";\n\t\tthis.command = command;\n\t}\n}\n\nfunction isRequestObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Serve serialized credential mutations on one private Unix socket. */\nexport function createCredentialProxy(options: {\n\tauthPath: string;\n\tviolationStore?: SandboxViolationStore;\n\tsocketPath: string;\n\t/** Private directory allocated by resolveCredentialChannelPaths; removed on close. */\n\tcleanupDirectory?: string;\n}): Promise<CredentialProxy> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst authStorage = AuthStorage.create(options.authPath);\n\t\tconst sockets = new Set<net.Socket>();\n\t\tconst operations = new Set<Promise<void>>();\n\t\tlet settled = false;\n\t\tlet closing: Promise<void> | undefined;\n\n\t\tconst audit = (command: string, detail: string): void => {\n\t\t\toptions.violationStore?.add({ kind: \"unknown\", command, detail, timestamp: new Date() });\n\t\t};\n\t\tconst refuse = (socket: net.Socket, command: string, error: string, detail = error): void => {\n\t\t\taudit(command, `Refused: ${detail}`);\n\t\t\twriteFrame(socket, { ok: false, error });\n\t\t};\n\n\t\tconst handleConnection = async (socket: net.Socket, reader: FrameReader, signal: AbortSignal): Promise<void> => {\n\t\t\tconst raw = await reader.next();\n\t\t\tif (!isRequestObject(raw)) {\n\t\t\t\trefuse(socket, \"credential-channel\", \"Invalid request: expected an object.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst request = raw as StartRequest;\n\t\t\tconst providerId = request.providerId;\n\t\t\tif (\n\t\t\t\ttypeof providerId !== \"string\" ||\n\t\t\t\tproviderId.length === 0 ||\n\t\t\t\tBuffer.byteLength(providerId, \"utf8\") > 256 ||\n\t\t\t\t/[\\u0000-\\u001f\\u007f-\\u009f\\u202a-\\u202e\\u2066-\\u2069]/u.test(providerId)\n\t\t\t) {\n\t\t\t\trefuse(\n\t\t\t\t\tsocket,\n\t\t\t\t\t\"credential-channel\",\n\t\t\t\t\t\"Invalid request: providerId contains unsafe characters or is too long.\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (request.action === \"delete\") {\n\t\t\t\tawait authStorage.delete(providerId, { signal });\n\t\t\t\taudit(`credential-delete ${providerId}`, \"Accepted: credential deleted through the sandbox channel.\");\n\t\t\t\twriteFrame(socket, { ok: true });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (request.action !== \"modify\") {\n\t\t\t\trefuse(socket, \"credential-channel\", 'Invalid request: action must be \"modify\" or \"delete\".');\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet changed = false;\n\t\t\tconst result = await authStorage.modify(\n\t\t\t\tproviderId,\n\t\t\t\tasync (current) => {\n\t\t\t\t\twriteFrame(socket, { type: \"current\", ...(current === undefined ? {} : { credential: current }) });\n\t\t\t\t\tconst rawCommit = await reader.next();\n\t\t\t\t\tif (!isRequestObject(rawCommit)) {\n\t\t\t\t\t\tthrow new ChannelRefusal(\n\t\t\t\t\t\t\t`credential-write ${providerId}`,\n\t\t\t\t\t\t\t\"Invalid modify response: expected an object.\",\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst commit = rawCommit as CommitRequest;\n\t\t\t\t\tif (commit.action === \"abort\") {\n\t\t\t\t\t\tthrow new ChannelRefusal(\n\t\t\t\t\t\t\t`credential-write ${providerId}`,\n\t\t\t\t\t\t\t\"Credential modification was aborted by the client.\",\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tif (commit.action === \"no_change\") return undefined;\n\t\t\t\t\tif (commit.action !== \"commit\") {\n\t\t\t\t\t\tthrow new ChannelRefusal(`credential-write ${providerId}`, \"Invalid modify response action.\");\n\t\t\t\t\t}\n\t\t\t\t\tif (!isCredential(commit.credential)) {\n\t\t\t\t\t\tthrow new ChannelRefusal(`credential-write ${providerId}`, \"Invalid credential shape.\");\n\t\t\t\t\t}\n\t\t\t\t\tconst reference = findCredentialConfigReference(commit.credential);\n\t\t\t\t\tif (reference) {\n\t\t\t\t\t\tthrow new ChannelRefusal(`credential-write ${providerId}`, referenceRefusal(reference));\n\t\t\t\t\t}\n\t\t\t\t\tchanged = true;\n\t\t\t\t\treturn commit.credential;\n\t\t\t\t},\n\t\t\t\t{ signal },\n\t\t\t);\n\t\t\tif (changed) {\n\t\t\t\taudit(`credential-write ${providerId}`, \"Accepted: credential written through the sandbox channel.\");\n\t\t\t}\n\t\t\twriteFrame(socket, { ok: true, ...(result === undefined ? {} : { credential: result }) });\n\t\t};\n\n\t\tconst server = net.createServer((socket) => {\n\t\t\tif (sockets.size >= MAX_CONNECTIONS) {\n\t\t\t\trefuse(socket, \"credential-channel\", \"Credential channel connection limit reached.\");\n\t\t\t\tsocket.end();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsockets.add(socket);\n\t\t\tconst controller = new AbortController();\n\t\t\tsocket.setTimeout(CONNECTION_IDLE_TIMEOUT_MS, () => {\n\t\t\t\taudit(\"credential-channel\", \"Refused: credential channel connection timed out.\");\n\t\t\t\tsocket.destroy(new Error(\"Credential channel connection timed out.\"));\n\t\t\t});\n\t\t\tsocket.on(\"close\", () => {\n\t\t\t\tsockets.delete(socket);\n\t\t\t\tcontroller.abort();\n\t\t\t});\n\t\t\tconst reader = new FrameReader(socket, (detail) => audit(\"credential-channel\", `Refused: ${detail}`));\n\t\t\tconst operation = handleConnection(socket, reader, controller.signal)\n\t\t\t\t.catch((error: unknown) => {\n\t\t\t\t\tif (error instanceof ChannelRefusal) {\n\t\t\t\t\t\trefuse(socket, error.command, error.message);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\trefuse(\n\t\t\t\t\t\tsocket,\n\t\t\t\t\t\t\"credential-channel\",\n\t\t\t\t\t\t\"The host credential operation failed.\",\n\t\t\t\t\t\t\"Host credential operation failed without exposing credential data.\",\n\t\t\t\t\t);\n\t\t\t\t})\n\t\t\t\t.finally(() => {\n\t\t\t\t\toperations.delete(operation);\n\t\t\t\t\tsocket.end();\n\t\t\t\t});\n\t\t\toperations.add(operation);\n\t\t});\n\n\t\tconst cleanup = (): void => {\n\t\t\trmSync(options.socketPath, { force: true });\n\t\t\tif (options.cleanupDirectory) rmSync(options.cleanupDirectory, { force: true, recursive: true });\n\t\t};\n\t\tconst failStartup = (error: Error): void => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tserver.close();\n\t\t\tfor (const socket of sockets) socket.destroy();\n\t\t\tcleanup();\n\t\t\treject(error);\n\t\t};\n\t\tserver.once(\"error\", failStartup);\n\t\tserver.listen(options.socketPath, () => {\n\t\t\ttry {\n\t\t\t\tchmodSync(options.socketPath, 0o600);\n\t\t\t} catch (error) {\n\t\t\t\tfailStartup(error instanceof Error ? error : new Error(String(error)));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsettled = true;\n\t\t\tserver.off(\"error\", failStartup);\n\t\t\tresolve({\n\t\t\t\tclose: () => {\n\t\t\t\t\tif (closing) return closing;\n\t\t\t\t\tclosing = (async () => {\n\t\t\t\t\t\tfor (const socket of sockets) socket.destroy();\n\t\t\t\t\t\tawait new Promise<void>((resolveClose) => server.close(() => resolveClose()));\n\t\t\t\t\t\tawait Promise.allSettled([...operations]);\n\t\t\t\t\t\tcleanup();\n\t\t\t\t\t})();\n\t\t\t\t\treturn closing;\n\t\t\t\t},\n\t\t\t});\n\t\t});\n\t});\n}\n"]}
|
|
1
|
+
{"version":3,"file":"credential-proxy.js","sourceRoot":"","sources":["../../../../src/core/sandbox/rpc/credential-proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjF,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,yBAAyB,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAEhG,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAExE,+EAA+E;AAC/E,MAAM,cAAc,GAAG,GAAG,CAAC;AAC3B,yFAAyF;AACzF,MAAM,eAAe,GAAG,EAAE,CAAC;AAC3B,MAAM,0BAA0B,GAAG,MAAM,CAAC;AAE1C,SAAS,cAAc,CAAC,GAAW,EAAW;IAC7C,IAAI,CAAC;QACJ,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACrB,OAAO,IAAI,CAAC;IACb,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,OAAQ,KAA+B,CAAC,IAAI,KAAK,OAAO,CAAC;IAC1D,CAAC;AAAA,CACD;AAED,+EAA+E;AAC/E,SAAS,wCAAwC,GAAS;IACzD,IAAI,OAAiB,CAAC;IACtB,IAAI,CAAC;QACJ,OAAO,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACR,OAAO;IACR,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,kCAAkC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC7D,IAAI,CAAC,KAAK;YAAE,SAAS;QACrB,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7B,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,cAAc,CAAC,GAAG,CAAC;YAAE,SAAS;QAC3E,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QACjC,IAAI,CAAC;YACJ,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;YAC7B,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,IAAI,CAAC,GAAG,KAAK,OAAO,CAAC,MAAM,EAAE,EAAE;gBAAE,SAAS;YACrE,MAAM,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAChD,CAAC;QAAC,MAAM,CAAC;YACR,8CAA8C;QAC/C,CAAC;IACF,CAAC;AAAA,CACD;AAQD;;;;;;;GAOG;AACH,MAAM,UAAU,6BAA6B,GAA2B;IACvE,wCAAwC,EAAE,CAAC;IAC3C,MAAM,mBAAmB,GAAG,WAAW,CAAC,kBAAkB,OAAO,CAAC,GAAG,GAAG,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IAChG,SAAS,CAAC,mBAAmB,EAAE,KAAK,CAAC,CAAC;IACtC,MAAM,cAAc,GAAG,IAAI,CAAC,mBAAmB,EAAE,cAAc,CAAC,CAAC;IACjE,IAAI,MAAM,CAAC,UAAU,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,cAAc,EAAE,CAAC;QAC5D,MAAM,CAAC,mBAAmB,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9D,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;IAChF,CAAC;IACD,MAAM,eAAe,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,oCAAoC,CAAC,CAAC,CAAC,cAAc,CAAC;IAC7G,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,eAAe,EAAE,CAAC;AAAA,CAChE;AAWD,2FAA2F;AAC3F,MAAM,UAAU,6BAA6B,CAAC,UAAmB,EAAyC;IACzG,MAAM,OAAO,GAA6C,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;IAC7F,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3B,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;QAC9B,IAAI,CAAC,OAAO;YAAE,MAAM;QACpB,IAAI,OAAO,OAAO,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YACvC,IAAI,oBAAoB,CAAC,OAAO,CAAC,KAAK,CAAC;gBAAE,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;YAC1F,IAAI,yBAAyB,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACzD,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;YACtD,CAAC;YACD,SAAS;QACV,CAAC;QACD,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAClC,KAAK,IAAI,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;gBAChE,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,KAAK,IAAI,KAAK,GAAG,EAAE,CAAC,CAAC;YACpF,CAAC;YACD,SAAS;QACV,CAAC;QACD,IAAI,OAAO,CAAC,KAAK,KAAK,IAAI,IAAI,OAAO,OAAO,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YACjE,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YAC9C,KAAK,IAAI,KAAK,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;gBAC1D,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,OAAO,CAAC,KAAK,CAAE,CAAC;gBACrC,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;YACjF,CAAC;QACF,CAAC;IACF,CAAC;IACD,OAAO,SAAS,CAAC;AAAA,CACjB;AAED,SAAS,cAAc,CAAC,KAAc,EAAmC;IACxE,OAAO,CACN,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QACrB,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAChE,CAAC;AAAA,CACF;AAED,kEAAkE;AAClE,MAAM,UAAU,YAAY,CAAC,KAAc,EAAuB;IACjE,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACtF,MAAM,UAAU,GAAG,KAAgC,CAAC;IACpD,IAAI,UAAU,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QACnC,OAAO,CACN,CAAC,UAAU,CAAC,GAAG,KAAK,SAAS,IAAI,OAAO,UAAU,CAAC,GAAG,KAAK,QAAQ,CAAC;YACpE,CAAC,UAAU,CAAC,GAAG,KAAK,SAAS,IAAI,cAAc,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAChE,CAAC;IACH,CAAC;IACD,OAAO,CACN,UAAU,CAAC,IAAI,KAAK,OAAO;QAC3B,OAAO,UAAU,CAAC,MAAM,KAAK,QAAQ;QACrC,OAAO,UAAU,CAAC,OAAO,KAAK,QAAQ;QACtC,OAAO,UAAU,CAAC,OAAO,KAAK,QAAQ;QACtC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC,CACnC,CAAC;AAAA,CACF;AAED,SAAS,gBAAgB,CAAC,SAAoC,EAAU;IACvE,OAAO,CACN,oFAAoF;QACpF,IAAI,SAAS,CAAC,KAAK,gBAAgB,SAAS,CAAC,IAAI,oCAAoC;QACrF,yFAAyF,CACzF,CAAC;AAAA,CACF;AAYD,MAAM,cAAe,SAAQ,KAAK;IACxB,OAAO,CAAS;IAEzB,YAAY,OAAe,EAAE,OAAe,EAAE;QAC7C,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IAAA,CACvB;CACD;AAED,wEAAwE;AACxE,MAAM,UAAU,qBAAqB,CAAC,OAMrC,EAA4B;IAC5B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;QACvC,MAAM,WAAW,GAAG,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACzD,MAAM,OAAO,GAAG,IAAI,GAAG,EAAc,CAAC;QACtC,MAAM,UAAU,GAAG,IAAI,GAAG,EAAiB,CAAC;QAC5C,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,IAAI,OAAkC,CAAC;QAEvC,MAAM,KAAK,GAAG,CAAC,OAAe,EAAE,MAAc,EAAQ,EAAE,CAAC;YACxD,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC;QAAA,CACzF,CAAC;QACF,MAAM,MAAM,GAAG,CAAC,MAAkB,EAAE,OAAe,EAAE,KAAa,EAAE,MAAM,GAAG,KAAK,EAAQ,EAAE,CAAC;YAC5F,KAAK,CAAC,OAAO,EAAE,YAAY,MAAM,EAAE,CAAC,CAAC;YACrC,UAAU,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QAAA,CACzC,CAAC;QAEF,MAAM,gBAAgB,GAAG,KAAK,EAAE,MAAkB,EAAE,MAAmB,EAAE,MAAmB,EAAiB,EAAE,CAAC;YAC/G,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;YAChC,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC3B,MAAM,CAAC,MAAM,EAAE,oBAAoB,EAAE,sCAAsC,CAAC,CAAC;gBAC7E,OAAO;YACR,CAAC;YACD,MAAM,OAAO,GAAG,GAAmB,CAAC;YACpC,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;YACtC,IACC,OAAO,UAAU,KAAK,QAAQ;gBAC9B,UAAU,CAAC,MAAM,KAAK,CAAC;gBACvB,MAAM,CAAC,UAAU,CAAC,UAAU,EAAE,MAAM,CAAC,GAAG,GAAG;gBAC3C,yDAAyD,CAAC,IAAI,CAAC,UAAU,CAAC,EACzE,CAAC;gBACF,MAAM,CACL,MAAM,EACN,oBAAoB,EACpB,wEAAwE,CACxE,CAAC;gBACF,OAAO;YACR,CAAC;YACD,IAAI,OAAO,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;gBACjC,MAAM,WAAW,CAAC,MAAM,CAAC,UAAU,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;gBACjD,KAAK,CAAC,qBAAqB,UAAU,EAAE,EAAE,2DAA2D,CAAC,CAAC;gBACtG,UAAU,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;gBACjC,OAAO;YACR,CAAC;YACD,IAAI,OAAO,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;gBACjC,MAAM,CAAC,MAAM,EAAE,oBAAoB,EAAE,uDAAuD,CAAC,CAAC;gBAC9F,OAAO;YACR,CAAC;YAED,IAAI,OAAO,GAAG,KAAK,CAAC;YACpB,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,MAAM,CACtC,UAAU,EACV,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC;gBAClB,UAAU,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;gBACnG,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;gBACtC,IAAI,CAAC,eAAe,CAAC,SAAS,CAAC,EAAE,CAAC;oBACjC,MAAM,IAAI,cAAc,CACvB,oBAAoB,UAAU,EAAE,EAChC,8CAA8C,CAC9C,CAAC;gBACH,CAAC;gBACD,MAAM,MAAM,GAAG,SAA0B,CAAC;gBAC1C,IAAI,MAAM,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;oBAC/B,MAAM,IAAI,cAAc,CACvB,oBAAoB,UAAU,EAAE,EAChC,oDAAoD,CACpD,CAAC;gBACH,CAAC;gBACD,IAAI,MAAM,CAAC,MAAM,KAAK,WAAW;oBAAE,OAAO,SAAS,CAAC;gBACpD,IAAI,MAAM,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;oBAChC,MAAM,IAAI,cAAc,CAAC,oBAAoB,UAAU,EAAE,EAAE,iCAAiC,CAAC,CAAC;gBAC/F,CAAC;gBACD,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;oBACtC,MAAM,IAAI,cAAc,CAAC,oBAAoB,UAAU,EAAE,EAAE,2BAA2B,CAAC,CAAC;gBACzF,CAAC;gBACD,MAAM,SAAS,GAAG,6BAA6B,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;gBACnE,IAAI,SAAS,EAAE,CAAC;oBACf,MAAM,IAAI,cAAc,CAAC,oBAAoB,UAAU,EAAE,EAAE,gBAAgB,CAAC,SAAS,CAAC,CAAC,CAAC;gBACzF,CAAC;gBACD,OAAO,GAAG,IAAI,CAAC;gBACf,OAAO,MAAM,CAAC,UAAU,CAAC;YAAA,CACzB,EACD,EAAE,MAAM,EAAE,CACV,CAAC;YACF,IAAI,OAAO,EAAE,CAAC;gBACb,KAAK,CAAC,oBAAoB,UAAU,EAAE,EAAE,2DAA2D,CAAC,CAAC;YACtG,CAAC;YACD,UAAU,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;QAAA,CAC1F,CAAC;QAEF,MAAM,MAAM,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;YAC3C,IAAI,OAAO,CAAC,IAAI,IAAI,eAAe,EAAE,CAAC;gBACrC,MAAM,CAAC,MAAM,EAAE,oBAAoB,EAAE,8CAA8C,CAAC,CAAC;gBACrF,MAAM,CAAC,GAAG,EAAE,CAAC;gBACb,OAAO;YACR,CAAC;YACD,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACpB,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;YACzC,MAAM,CAAC,UAAU,CAAC,0BAA0B,EAAE,GAAG,EAAE,CAAC;gBACnD,KAAK,CAAC,oBAAoB,EAAE,mDAAmD,CAAC,CAAC;gBACjF,MAAM,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC,CAAC;YAAA,CACtE,CAAC,CAAC;YACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC;gBACxB,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;gBACvB,UAAU,CAAC,KAAK,EAAE,CAAC;YAAA,CACnB,CAAC,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC,MAAM,EAAE,oBAAoB,EAAE,CAAC,MAAM,EAAE,EAAE,CACvE,KAAK,CAAC,oBAAoB,EAAE,YAAY,MAAM,EAAE,CAAC,CACjD,CAAC;YACF,MAAM,SAAS,GAAG,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,CAAC;iBACnE,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,CAAC;gBAC1B,IAAI,KAAK,YAAY,cAAc,EAAE,CAAC;oBACrC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;oBAC7C,OAAO;gBACR,CAAC;gBACD,MAAM,CACL,MAAM,EACN,oBAAoB,EACpB,uCAAuC,EACvC,oEAAoE,CACpE,CAAC;YAAA,CACF,CAAC;iBACD,OAAO,CAAC,GAAG,EAAE,CAAC;gBACd,UAAU,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;gBAC7B,MAAM,CAAC,GAAG,EAAE,CAAC;YAAA,CACb,CAAC,CAAC;YACJ,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAAA,CAC1B,CAAC,CAAC;QAEH,MAAM,OAAO,GAAG,GAAS,EAAE,CAAC;YAC3B,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YAC5C,IAAI,OAAO,CAAC,gBAAgB;gBAAE,MAAM,CAAC,OAAO,CAAC,gBAAgB,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAAA,CACjG,CAAC;QACF,MAAM,WAAW,GAAG,CAAC,KAAY,EAAQ,EAAE,CAAC;YAC3C,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,MAAM,CAAC,KAAK,EAAE,CAAC;YACf,KAAK,MAAM,MAAM,IAAI,OAAO;gBAAE,MAAM,CAAC,OAAO,EAAE,CAAC;YAC/C,OAAO,EAAE,CAAC;YACV,MAAM,CAAC,KAAK,CAAC,CAAC;QAAA,CACd,CAAC;QACF,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;QAClC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE,GAAG,EAAE,CAAC;YACvC,IAAI,CAAC;gBACJ,SAAS,CAAC,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;YACtC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,WAAW,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;gBACvE,OAAO;YACR,CAAC;YACD,OAAO,GAAG,IAAI,CAAC;YACf,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;YACjC,OAAO,CAAC;gBACP,KAAK,EAAE,GAAG,EAAE,CAAC;oBACZ,IAAI,OAAO;wBAAE,OAAO,OAAO,CAAC;oBAC5B,OAAO,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC;wBACtB,KAAK,MAAM,MAAM,IAAI,OAAO;4BAAE,MAAM,CAAC,OAAO,EAAE,CAAC;wBAC/C,MAAM,IAAI,OAAO,CAAO,CAAC,YAAY,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC;wBAC9E,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC;wBAC1C,OAAO,EAAE,CAAC;oBAAA,CACV,CAAC,EAAE,CAAC;oBACL,OAAO,OAAO,CAAC;gBAAA,CACf;aACD,CAAC,CAAC;QAAA,CACH,CAAC,CAAC;IAAA,CACH,CAAC,CAAC;AAAA,CACH","sourcesContent":["/**\n * Supervisor-owned credential mutation service for sandboxed sessions.\n *\n * Reads stay on the read-only auth.json projection. Mutations cross this Unix socket\n * and execute under the host AuthStorage lock. The child can therefore implement the\n * CredentialStore serialized read-modify-write contract without gaining filesystem\n * write access to the credential file.\n */\n\nimport { chmodSync, lstatSync, mkdtempSync, readdirSync, rmSync } from \"node:fs\";\nimport * as net from \"node:net\";\nimport { join } from \"node:path\";\nimport type { Credential } from \"@earendil-works/pi-ai\";\nimport { AuthStorage } from \"../../auth-storage.ts\";\nimport { getConfigValueEnvVarNames, isCommandConfigValue } from \"../../resolve-config-value.ts\";\nimport type { SandboxViolationStore } from \"../violations.ts\";\nimport { FrameReader, isRequestObject, writeFrame } from \"./framing.ts\";\n\n/** AF_UNIX `sun_path` is 108 bytes on Linux, including the terminating NUL. */\nconst SUN_PATH_LIMIT = 108;\n/** Credentials are small; reject larger frames before UTF-8 decoding or JSON parsing. */\nconst MAX_CONNECTIONS = 32;\nconst CONNECTION_IDLE_TIMEOUT_MS = 30_000;\n\nfunction processIsAlive(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (error) {\n\t\treturn (error as NodeJS.ErrnoException).code === \"EPERM\";\n\t}\n}\n\n/** Reclaim only same-user directories whose encoded supervisor PID is dead. */\nfunction reclaimStaleCredentialChannelDirectories(): void {\n\tlet entries: string[];\n\ttry {\n\t\tentries = readdirSync(\"/tmp\");\n\t} catch {\n\t\treturn;\n\t}\n\tfor (const entry of entries) {\n\t\tconst match = /^apex-cred-(\\d+)-[A-Za-z0-9_-]+$/.exec(entry);\n\t\tif (!match) continue;\n\t\tconst pid = Number(match[1]);\n\t\tif (!Number.isSafeInteger(pid) || pid < 1 || processIsAlive(pid)) continue;\n\t\tconst path = join(\"/tmp\", entry);\n\t\ttry {\n\t\t\tconst stat = lstatSync(path);\n\t\t\tif (!stat.isDirectory() || stat.uid !== process.getuid?.()) continue;\n\t\t\trmSync(path, { force: true, recursive: true });\n\t\t} catch {\n\t\t\t// Another launch may have reclaimed it first.\n\t\t}\n\t}\n}\n\nexport interface CredentialChannelPaths {\n\treadonly hostSocketDirectory: string;\n\treadonly hostSocketPath: string;\n\treadonly childSocketPath: string;\n}\n\n/**\n * Allocate the endpoint beneath a private fixed-root directory.\n *\n * Do not honor TMPDIR here. It can point inside the workspace on macOS, where the\n * sandbox may write it. A fresh 0700 directory under /tmp also removes the\n * listen-before-chmod authorization race: no other account can reach the socket while\n * Node applies its initial umask-derived mode.\n */\nexport function resolveCredentialChannelPaths(): CredentialChannelPaths {\n\treclaimStaleCredentialChannelDirectories();\n\tconst hostSocketDirectory = mkdtempSync(`/tmp/apex-cred-${process.pid}-`, { encoding: \"utf8\" });\n\tchmodSync(hostSocketDirectory, 0o700);\n\tconst hostSocketPath = join(hostSocketDirectory, \"channel.sock\");\n\tif (Buffer.byteLength(hostSocketPath) + 1 > SUN_PATH_LIMIT) {\n\t\trmSync(hostSocketDirectory, { force: true, recursive: true });\n\t\tthrow new Error(\"Credential channel path exceeds the Unix socket path limit.\");\n\t}\n\tconst childSocketPath = process.platform === \"linux\" ? \"/home/apex-credential-channel.sock\" : hostSocketPath;\n\treturn { hostSocketDirectory, hostSocketPath, childSocketPath };\n}\n\nexport interface CredentialProxy {\n\tclose(): Promise<void>;\n}\n\nexport interface CredentialConfigReference {\n\treadonly field: string;\n\treadonly kind: \"command\" | \"environment\";\n}\n\n/** Find a config-value reference without recursively walking attacker-controlled input. */\nexport function findCredentialConfigReference(credential: unknown): CredentialConfigReference | undefined {\n\tconst pending: Array<{ value: unknown; field: string }> = [{ value: credential, field: \"\" }];\n\twhile (pending.length > 0) {\n\t\tconst current = pending.pop();\n\t\tif (!current) break;\n\t\tif (typeof current.value === \"string\") {\n\t\t\tif (isCommandConfigValue(current.value)) return { field: current.field, kind: \"command\" };\n\t\t\tif (getConfigValueEnvVarNames(current.value).length > 0) {\n\t\t\t\treturn { field: current.field, kind: \"environment\" };\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (Array.isArray(current.value)) {\n\t\t\tfor (let index = current.value.length - 1; index >= 0; index--) {\n\t\t\t\tpending.push({ value: current.value[index], field: `${current.field}[${index}]` });\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (current.value !== null && typeof current.value === \"object\") {\n\t\t\tconst entries = Object.entries(current.value);\n\t\t\tfor (let index = entries.length - 1; index >= 0; index--) {\n\t\t\t\tconst [key, value] = entries[index]!;\n\t\t\t\tpending.push({ value, field: current.field ? `${current.field}.${key}` : key });\n\t\t\t}\n\t\t}\n\t}\n\treturn undefined;\n}\n\nfunction isStringRecord(value: unknown): value is Record<string, string> {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\t!Array.isArray(value) &&\n\t\tObject.values(value).every((entry) => typeof entry === \"string\")\n\t);\n}\n\n/** Runtime validation at the untrusted child-to-host boundary. */\nexport function isCredential(value: unknown): value is Credential {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) return false;\n\tconst credential = value as Record<string, unknown>;\n\tif (credential.type === \"api_key\") {\n\t\treturn (\n\t\t\t(credential.key === undefined || typeof credential.key === \"string\") &&\n\t\t\t(credential.env === undefined || isStringRecord(credential.env))\n\t\t);\n\t}\n\treturn (\n\t\tcredential.type === \"oauth\" &&\n\t\ttypeof credential.access === \"string\" &&\n\t\ttypeof credential.refresh === \"string\" &&\n\t\ttypeof credential.expires === \"number\" &&\n\t\tNumber.isFinite(credential.expires)\n\t);\n}\n\nfunction referenceRefusal(reference: CredentialConfigReference): string {\n\treturn (\n\t\t`Credential values written from a sandboxed session must be literal secrets: field ` +\n\t\t`\"${reference.field}\" contains a ${reference.kind} reference. To store a reference, ` +\n\t\t`edit the credential file on the host directly or use the provider environment variable.`\n\t);\n}\n\ninterface StartRequest {\n\treadonly action?: unknown;\n\treadonly providerId?: unknown;\n}\n\ninterface CommitRequest {\n\treadonly action?: unknown;\n\treadonly credential?: unknown;\n}\n\nclass ChannelRefusal extends Error {\n\treadonly command: string;\n\n\tconstructor(command: string, message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"ChannelRefusal\";\n\t\tthis.command = command;\n\t}\n}\n\n/** Serve serialized credential mutations on one private Unix socket. */\nexport function createCredentialProxy(options: {\n\tauthPath: string;\n\tviolationStore?: SandboxViolationStore;\n\tsocketPath: string;\n\t/** Private directory allocated by resolveCredentialChannelPaths; removed on close. */\n\tcleanupDirectory?: string;\n}): Promise<CredentialProxy> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst authStorage = AuthStorage.create(options.authPath);\n\t\tconst sockets = new Set<net.Socket>();\n\t\tconst operations = new Set<Promise<void>>();\n\t\tlet settled = false;\n\t\tlet closing: Promise<void> | undefined;\n\n\t\tconst audit = (command: string, detail: string): void => {\n\t\t\toptions.violationStore?.add({ kind: \"unknown\", command, detail, timestamp: new Date() });\n\t\t};\n\t\tconst refuse = (socket: net.Socket, command: string, error: string, detail = error): void => {\n\t\t\taudit(command, `Refused: ${detail}`);\n\t\t\twriteFrame(socket, { ok: false, error });\n\t\t};\n\n\t\tconst handleConnection = async (socket: net.Socket, reader: FrameReader, signal: AbortSignal): Promise<void> => {\n\t\t\tconst raw = await reader.next();\n\t\t\tif (!isRequestObject(raw)) {\n\t\t\t\trefuse(socket, \"credential-channel\", \"Invalid request: expected an object.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst request = raw as StartRequest;\n\t\t\tconst providerId = request.providerId;\n\t\t\tif (\n\t\t\t\ttypeof providerId !== \"string\" ||\n\t\t\t\tproviderId.length === 0 ||\n\t\t\t\tBuffer.byteLength(providerId, \"utf8\") > 256 ||\n\t\t\t\t/[\\u0000-\\u001f\\u007f-\\u009f\\u202a-\\u202e\\u2066-\\u2069]/u.test(providerId)\n\t\t\t) {\n\t\t\t\trefuse(\n\t\t\t\t\tsocket,\n\t\t\t\t\t\"credential-channel\",\n\t\t\t\t\t\"Invalid request: providerId contains unsafe characters or is too long.\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (request.action === \"delete\") {\n\t\t\t\tawait authStorage.delete(providerId, { signal });\n\t\t\t\taudit(`credential-delete ${providerId}`, \"Accepted: credential deleted through the sandbox channel.\");\n\t\t\t\twriteFrame(socket, { ok: true });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (request.action !== \"modify\") {\n\t\t\t\trefuse(socket, \"credential-channel\", 'Invalid request: action must be \"modify\" or \"delete\".');\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet changed = false;\n\t\t\tconst result = await authStorage.modify(\n\t\t\t\tproviderId,\n\t\t\t\tasync (current) => {\n\t\t\t\t\twriteFrame(socket, { type: \"current\", ...(current === undefined ? {} : { credential: current }) });\n\t\t\t\t\tconst rawCommit = await reader.next();\n\t\t\t\t\tif (!isRequestObject(rawCommit)) {\n\t\t\t\t\t\tthrow new ChannelRefusal(\n\t\t\t\t\t\t\t`credential-write ${providerId}`,\n\t\t\t\t\t\t\t\"Invalid modify response: expected an object.\",\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst commit = rawCommit as CommitRequest;\n\t\t\t\t\tif (commit.action === \"abort\") {\n\t\t\t\t\t\tthrow new ChannelRefusal(\n\t\t\t\t\t\t\t`credential-write ${providerId}`,\n\t\t\t\t\t\t\t\"Credential modification was aborted by the client.\",\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tif (commit.action === \"no_change\") return undefined;\n\t\t\t\t\tif (commit.action !== \"commit\") {\n\t\t\t\t\t\tthrow new ChannelRefusal(`credential-write ${providerId}`, \"Invalid modify response action.\");\n\t\t\t\t\t}\n\t\t\t\t\tif (!isCredential(commit.credential)) {\n\t\t\t\t\t\tthrow new ChannelRefusal(`credential-write ${providerId}`, \"Invalid credential shape.\");\n\t\t\t\t\t}\n\t\t\t\t\tconst reference = findCredentialConfigReference(commit.credential);\n\t\t\t\t\tif (reference) {\n\t\t\t\t\t\tthrow new ChannelRefusal(`credential-write ${providerId}`, referenceRefusal(reference));\n\t\t\t\t\t}\n\t\t\t\t\tchanged = true;\n\t\t\t\t\treturn commit.credential;\n\t\t\t\t},\n\t\t\t\t{ signal },\n\t\t\t);\n\t\t\tif (changed) {\n\t\t\t\taudit(`credential-write ${providerId}`, \"Accepted: credential written through the sandbox channel.\");\n\t\t\t}\n\t\t\twriteFrame(socket, { ok: true, ...(result === undefined ? {} : { credential: result }) });\n\t\t};\n\n\t\tconst server = net.createServer((socket) => {\n\t\t\tif (sockets.size >= MAX_CONNECTIONS) {\n\t\t\t\trefuse(socket, \"credential-channel\", \"Credential channel connection limit reached.\");\n\t\t\t\tsocket.end();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsockets.add(socket);\n\t\t\tconst controller = new AbortController();\n\t\t\tsocket.setTimeout(CONNECTION_IDLE_TIMEOUT_MS, () => {\n\t\t\t\taudit(\"credential-channel\", \"Refused: credential channel connection timed out.\");\n\t\t\t\tsocket.destroy(new Error(\"Credential channel connection timed out.\"));\n\t\t\t});\n\t\t\tsocket.on(\"close\", () => {\n\t\t\t\tsockets.delete(socket);\n\t\t\t\tcontroller.abort();\n\t\t\t});\n\t\t\tconst reader = new FrameReader(socket, \"Credential channel\", (detail) =>\n\t\t\t\taudit(\"credential-channel\", `Refused: ${detail}`),\n\t\t\t);\n\t\t\tconst operation = handleConnection(socket, reader, controller.signal)\n\t\t\t\t.catch((error: unknown) => {\n\t\t\t\t\tif (error instanceof ChannelRefusal) {\n\t\t\t\t\t\trefuse(socket, error.command, error.message);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\trefuse(\n\t\t\t\t\t\tsocket,\n\t\t\t\t\t\t\"credential-channel\",\n\t\t\t\t\t\t\"The host credential operation failed.\",\n\t\t\t\t\t\t\"Host credential operation failed without exposing credential data.\",\n\t\t\t\t\t);\n\t\t\t\t})\n\t\t\t\t.finally(() => {\n\t\t\t\t\toperations.delete(operation);\n\t\t\t\t\tsocket.end();\n\t\t\t\t});\n\t\t\toperations.add(operation);\n\t\t});\n\n\t\tconst cleanup = (): void => {\n\t\t\trmSync(options.socketPath, { force: true });\n\t\t\tif (options.cleanupDirectory) rmSync(options.cleanupDirectory, { force: true, recursive: true });\n\t\t};\n\t\tconst failStartup = (error: Error): void => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tserver.close();\n\t\t\tfor (const socket of sockets) socket.destroy();\n\t\t\tcleanup();\n\t\t\treject(error);\n\t\t};\n\t\tserver.once(\"error\", failStartup);\n\t\tserver.listen(options.socketPath, () => {\n\t\t\ttry {\n\t\t\t\tchmodSync(options.socketPath, 0o600);\n\t\t\t} catch (error) {\n\t\t\t\tfailStartup(error instanceof Error ? error : new Error(String(error)));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsettled = true;\n\t\t\tserver.off(\"error\", failStartup);\n\t\t\tresolve({\n\t\t\t\tclose: () => {\n\t\t\t\t\tif (closing) return closing;\n\t\t\t\t\tclosing = (async () => {\n\t\t\t\t\t\tfor (const socket of sockets) socket.destroy();\n\t\t\t\t\t\tawait new Promise<void>((resolveClose) => server.close(() => resolveClose()));\n\t\t\t\t\t\tawait Promise.allSettled([...operations]);\n\t\t\t\t\t\tcleanup();\n\t\t\t\t\t})();\n\t\t\t\t\treturn closing;\n\t\t\t\t},\n\t\t\t});\n\t\t});\n\t});\n}\n"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type * as net from "node:net";
|
|
2
|
+
/**
|
|
3
|
+
* The framing both supervisor-owned RPC channels share.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from `credential-proxy.ts` when the git credential channel needed the same
|
|
6
|
+
* reader. A second hand-written copy of a byte-bounded parser sitting on a socket that
|
|
7
|
+
* every process inside the sandbox can reach is exactly the divergence ADR 0010 exists to
|
|
8
|
+
* prevent for tool contracts: a hardening applied to one copy and not the other is
|
|
9
|
+
* invisible until it matters. One implementation, named per channel only in its messages.
|
|
10
|
+
*/
|
|
11
|
+
export declare const MAX_FRAME_BYTES: number;
|
|
12
|
+
export declare function writeFrame(socket: net.Socket, response: object): void;
|
|
13
|
+
export declare function isRequestObject(value: unknown): value is Record<string, unknown>;
|
|
14
|
+
/**
|
|
15
|
+
* One request frame at a time, newline-delimited JSON, bounded in bytes.
|
|
16
|
+
*
|
|
17
|
+
* Pipelining is refused rather than queued: a channel that answers a second request before
|
|
18
|
+
* the first is served makes the audit tail ambiguous about which frame a refusal belonged
|
|
19
|
+
* to, and neither channel has a caller that needs it.
|
|
20
|
+
*/
|
|
21
|
+
export declare class FrameReader {
|
|
22
|
+
private readonly socket;
|
|
23
|
+
private readonly channelName;
|
|
24
|
+
private readonly onProtocolRefusal;
|
|
25
|
+
private buffer;
|
|
26
|
+
private readonly queued;
|
|
27
|
+
private readonly waiting;
|
|
28
|
+
private terminalError;
|
|
29
|
+
constructor(socket: net.Socket, channelName: string, onProtocolRefusal: (detail: string) => void);
|
|
30
|
+
next(): Promise<unknown>;
|
|
31
|
+
private push;
|
|
32
|
+
private rejectOversizedFrame;
|
|
33
|
+
private fail;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=framing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"framing.d.ts","sourceRoot":"","sources":["../../../../src/core/sandbox/rpc/framing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,GAAG,MAAM,UAAU,CAAC;AAErC;;;;;;;;GAQG;AAEH,eAAO,MAAM,eAAe,QAAY,CAAC;AAEzC,wBAAgB,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAOrE;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEhF;AAED;;;;;;GAMG;AACH,qBAAa,WAAW;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA2B;IAC7D,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4E;IACpG,OAAO,CAAC,aAAa,CAAoB;IAEzC,YAAY,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,EAO/F;IAED,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAKvB;IAED,OAAO,CAAC,IAAI;IAyCZ,OAAO,CAAC,oBAAoB;IAO5B,OAAO,CAAC,IAAI;CAKZ","sourcesContent":["import type * as net from \"node:net\";\n\n/**\n * The framing both supervisor-owned RPC channels share.\n *\n * Extracted from `credential-proxy.ts` when the git credential channel needed the same\n * reader. A second hand-written copy of a byte-bounded parser sitting on a socket that\n * every process inside the sandbox can reach is exactly the divergence ADR 0010 exists to\n * prevent for tool contracts: a hardening applied to one copy and not the other is\n * invisible until it matters. One implementation, named per channel only in its messages.\n */\n\nexport const MAX_FRAME_BYTES = 64 * 1024;\n\nexport function writeFrame(socket: net.Socket, response: object): void {\n\tif (socket.destroyed) return;\n\tconst frame = Buffer.from(`${JSON.stringify(response)}\\n`, \"utf8\");\n\tif (frame.length > MAX_FRAME_BYTES) {\n\t\tthrow new Error(\"Channel response exceeded its byte limit.\");\n\t}\n\tsocket.write(frame);\n}\n\nexport function isRequestObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * One request frame at a time, newline-delimited JSON, bounded in bytes.\n *\n * Pipelining is refused rather than queued: a channel that answers a second request before\n * the first is served makes the audit tail ambiguous about which frame a refusal belonged\n * to, and neither channel has a caller that needs it.\n */\nexport class FrameReader {\n\tprivate readonly socket: net.Socket;\n\tprivate readonly channelName: string;\n\tprivate readonly onProtocolRefusal: (detail: string) => void;\n\tprivate buffer = Buffer.alloc(0);\n\tprivate readonly queued: unknown[] = [];\n\tprivate readonly waiting: Array<{ resolve(value: unknown): void; reject(error: Error): void }> = [];\n\tprivate terminalError: Error | undefined;\n\n\tconstructor(socket: net.Socket, channelName: string, onProtocolRefusal: (detail: string) => void) {\n\t\tthis.socket = socket;\n\t\tthis.channelName = channelName;\n\t\tthis.onProtocolRefusal = onProtocolRefusal;\n\t\tsocket.on(\"data\", (chunk: Buffer) => this.push(chunk));\n\t\tsocket.on(\"close\", () => this.fail(new Error(`${channelName} client disconnected.`)));\n\t\tsocket.on(\"error\", (error) => this.fail(error));\n\t}\n\n\tnext(): Promise<unknown> {\n\t\tconst queued = this.queued.shift();\n\t\tif (queued !== undefined) return Promise.resolve(queued);\n\t\tif (this.terminalError) return Promise.reject(this.terminalError);\n\t\treturn new Promise((resolve, reject) => this.waiting.push({ resolve, reject }));\n\t}\n\n\tprivate push(chunk: Buffer): void {\n\t\tif (this.terminalError) return;\n\t\tthis.buffer = Buffer.concat([this.buffer, chunk]);\n\t\twhile (true) {\n\t\t\tconst newline = this.buffer.indexOf(0x0a);\n\t\t\tif (newline < 0) {\n\t\t\t\tif (this.buffer.length > MAX_FRAME_BYTES) this.rejectOversizedFrame();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (newline > MAX_FRAME_BYTES) {\n\t\t\t\tthis.rejectOversizedFrame();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst frame = this.buffer.subarray(0, newline);\n\t\t\tthis.buffer = this.buffer.subarray(newline + 1);\n\t\t\tif (frame.length === 0) continue;\n\t\t\tlet value: unknown;\n\t\t\ttry {\n\t\t\t\tvalue = JSON.parse(frame.toString(\"utf8\")) as unknown;\n\t\t\t} catch {\n\t\t\t\tthis.onProtocolRefusal(\"Invalid request frame: not JSON.\");\n\t\t\t\twriteFrame(this.socket, { ok: false, error: \"Invalid request frame: not JSON.\" });\n\t\t\t\tthis.fail(new Error(`${this.channelName} received an invalid JSON frame.`));\n\t\t\t\tthis.socket.end();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst waiter = this.waiting.shift();\n\t\t\tif (waiter) {\n\t\t\t\twaiter.resolve(value);\n\t\t\t} else if (this.queued.length === 0) {\n\t\t\t\tthis.queued.push(value);\n\t\t\t} else {\n\t\t\t\tthis.onProtocolRefusal(\"Unexpected pipelined request frame.\");\n\t\t\t\twriteFrame(this.socket, { ok: false, error: \"Unexpected pipelined request frame.\" });\n\t\t\t\tthis.fail(new Error(`${this.channelName} received an unexpected pipelined frame.`));\n\t\t\t\tthis.socket.end();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate rejectOversizedFrame(): void {\n\t\tthis.onProtocolRefusal(\"Request frame exceeded the 64 KiB byte limit.\");\n\t\twriteFrame(this.socket, { ok: false, error: \"Request frame is too large.\" });\n\t\tthis.fail(new Error(`${this.channelName} frame exceeded its byte limit.`));\n\t\tthis.socket.end();\n\t}\n\n\tprivate fail(error: Error): void {\n\t\tif (this.terminalError) return;\n\t\tthis.terminalError = error;\n\t\tfor (const waiter of this.waiting.splice(0)) waiter.reject(error);\n\t}\n}\n"]}
|