apex-code 0.0.5 → 0.1.0
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 +40 -1
- package/README.md +7 -8
- package/dist/cli/agent-lifecycle.d.ts +24 -0
- package/dist/cli/agent-lifecycle.d.ts.map +1 -0
- package/dist/cli/agent-lifecycle.js +126 -0
- package/dist/cli/agent-lifecycle.js.map +1 -0
- package/dist/cli/args.d.ts +13 -12
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +80 -30
- package/dist/cli/args.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -130
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +0 -8
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +160 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +265 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/delegation/agents.d.ts.map +1 -1
- package/dist/core/delegation/agents.js +2 -1
- package/dist/core/delegation/agents.js.map +1 -1
- package/dist/core/delegation/runtime.d.ts +674 -1
- package/dist/core/delegation/runtime.d.ts.map +1 -1
- package/dist/core/delegation/runtime.js +1338 -66
- package/dist/core/delegation/runtime.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +3 -4
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/source-runtime.d.ts +4 -5
- package/dist/core/extensions/source-runtime.d.ts.map +1 -1
- package/dist/core/extensions/source-runtime.js +4 -5
- package/dist/core/extensions/source-runtime.js.map +1 -1
- package/dist/core/formatter-lifecycle.d.ts +2 -1
- package/dist/core/formatter-lifecycle.d.ts.map +1 -1
- package/dist/core/formatter-lifecycle.js +2 -1
- package/dist/core/formatter-lifecycle.js.map +1 -1
- package/dist/core/hooks/types.d.ts +2 -3
- package/dist/core/hooks/types.d.ts.map +1 -1
- package/dist/core/hooks/types.js +2 -3
- package/dist/core/hooks/types.js.map +1 -1
- package/dist/core/http-idle-timeout.d.ts +3 -5
- package/dist/core/http-idle-timeout.d.ts.map +1 -1
- package/dist/core/http-idle-timeout.js +3 -5
- package/dist/core/http-idle-timeout.js.map +1 -1
- package/dist/core/mcp/config.d.ts +18 -1
- package/dist/core/mcp/config.d.ts.map +1 -1
- package/dist/core/mcp/config.js +24 -7
- package/dist/core/mcp/config.js.map +1 -1
- package/dist/core/mcp/connector.d.ts +2 -3
- package/dist/core/mcp/connector.d.ts.map +1 -1
- package/dist/core/mcp/connector.js.map +1 -1
- package/dist/core/mcp/oauth/authorize.d.ts +2 -0
- package/dist/core/mcp/oauth/authorize.d.ts.map +1 -1
- package/dist/core/mcp/oauth/authorize.js +5 -2
- package/dist/core/mcp/oauth/authorize.js.map +1 -1
- package/dist/core/mcp/oauth/mcp-token.d.ts +4 -6
- package/dist/core/mcp/oauth/mcp-token.d.ts.map +1 -1
- package/dist/core/mcp/oauth/mcp-token.js +5 -7
- package/dist/core/mcp/oauth/mcp-token.js.map +1 -1
- package/dist/core/mcp/runtime.d.ts +1 -0
- package/dist/core/mcp/runtime.d.ts.map +1 -1
- package/dist/core/mcp/runtime.js +2 -1
- package/dist/core/mcp/runtime.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +0 -15
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/permissions/store.d.ts +6 -11
- package/dist/core/permissions/store.d.ts.map +1 -1
- package/dist/core/permissions/store.js +23 -51
- package/dist/core/permissions/store.js.map +1 -1
- package/dist/core/project-resources.d.ts +62 -0
- package/dist/core/project-resources.d.ts.map +1 -0
- package/dist/core/project-resources.js +42 -0
- package/dist/core/project-resources.js.map +1 -0
- package/dist/core/sdk.d.ts +62 -12
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +313 -44
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-share.d.ts +6 -8
- package/dist/core/session-share.d.ts.map +1 -1
- package/dist/core/session-share.js +9 -12
- package/dist/core/session-share.js.map +1 -1
- package/dist/core/settings-manager.d.ts +0 -21
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +4 -12
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +0 -47
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/delegate.d.ts +8 -0
- package/dist/core/tools/delegate.d.ts.map +1 -1
- package/dist/core/tools/delegate.js +33 -3
- package/dist/core/tools/delegate.js.map +1 -1
- package/dist/core/tools/web-fetch.d.ts.map +1 -1
- package/dist/core/tools/web-fetch.js +4 -8
- package/dist/core/tools/web-fetch.js.map +1 -1
- package/dist/core/tools/web-search-exa.d.ts +3 -6
- package/dist/core/tools/web-search-exa.d.ts.map +1 -1
- package/dist/core/tools/web-search-exa.js +5 -8
- package/dist/core/tools/web-search-exa.js.map +1 -1
- package/dist/core/trust-manager.d.ts +6 -5
- package/dist/core/trust-manager.d.ts.map +1 -1
- package/dist/core/trust-manager.js +42 -19
- package/dist/core/trust-manager.js.map +1 -1
- package/dist/core/web-search-provider.d.ts +0 -10
- package/dist/core/web-search-provider.d.ts.map +1 -1
- package/dist/core/web-search-provider.js +2 -16
- package/dist/core/web-search-provider.js.map +1 -1
- package/dist/core/workspace/git-observer.d.ts +16 -0
- package/dist/core/workspace/git-observer.d.ts.map +1 -1
- package/dist/core/workspace/git-observer.js +8 -1
- package/dist/core/workspace/git-observer.js.map +1 -1
- package/dist/core/workspace/git-worktree-owner.d.ts +89 -0
- package/dist/core/workspace/git-worktree-owner.d.ts.map +1 -0
- package/dist/core/workspace/git-worktree-owner.js +240 -0
- package/dist/core/workspace/git-worktree-owner.js.map +1 -0
- package/dist/main.d.ts +0 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +22 -3
- package/dist/main.js.map +1 -1
- package/dist/modes/acp/server.d.ts +43 -1
- package/dist/modes/acp/server.d.ts.map +1 -1
- package/dist/modes/acp/server.js +78 -0
- package/dist/modes/acp/server.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +1 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +0 -29
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +38 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +110 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +0 -25
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +3 -52
- package/dist/utils/tools-manager.js.map +1 -1
- package/dist/utils/version-check.d.ts +0 -2
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +0 -2
- package/dist/utils/version-check.js.map +1 -1
- package/docs/settings.md +2 -7
- package/npm-shrinkwrap.json +5 -5
- package/package.json +2 -2
- package/dist/core/sandbox/bwrap-arguments.d.ts +0 -41
- package/dist/core/sandbox/bwrap-arguments.d.ts.map +0 -1
- package/dist/core/sandbox/bwrap-arguments.js +0 -136
- package/dist/core/sandbox/bwrap-arguments.js.map +0 -1
- package/dist/core/sandbox/child-entry.d.ts +0 -2
- package/dist/core/sandbox/child-entry.d.ts.map +0 -1
- package/dist/core/sandbox/child-entry.js +0 -24
- package/dist/core/sandbox/child-entry.js.map +0 -1
- package/dist/core/sandbox/cli-launch.d.ts +0 -162
- package/dist/core/sandbox/cli-launch.d.ts.map +0 -1
- package/dist/core/sandbox/cli-launch.js +0 -366
- package/dist/core/sandbox/cli-launch.js.map +0 -1
- package/dist/core/sandbox/cli-supervisor.d.ts +0 -31
- package/dist/core/sandbox/cli-supervisor.d.ts.map +0 -1
- package/dist/core/sandbox/cli-supervisor.js +0 -147
- package/dist/core/sandbox/cli-supervisor.js.map +0 -1
- package/dist/core/sandbox/default-hosts.d.ts +0 -28
- package/dist/core/sandbox/default-hosts.d.ts.map +0 -1
- package/dist/core/sandbox/default-hosts.js +0 -69
- package/dist/core/sandbox/default-hosts.js.map +0 -1
- package/dist/core/sandbox/full-access.d.ts +0 -28
- package/dist/core/sandbox/full-access.d.ts.map +0 -1
- package/dist/core/sandbox/full-access.js +0 -63
- package/dist/core/sandbox/full-access.js.map +0 -1
- package/dist/core/sandbox/git-identity.d.ts +0 -57
- package/dist/core/sandbox/git-identity.d.ts.map +0 -1
- package/dist/core/sandbox/git-identity.js +0 -78
- package/dist/core/sandbox/git-identity.js.map +0 -1
- package/dist/core/sandbox/host-approval.d.ts +0 -42
- package/dist/core/sandbox/host-approval.d.ts.map +0 -1
- package/dist/core/sandbox/host-approval.js +0 -99
- package/dist/core/sandbox/host-approval.js.map +0 -1
- package/dist/core/sandbox/linux-backend.d.ts +0 -48
- package/dist/core/sandbox/linux-backend.d.ts.map +0 -1
- package/dist/core/sandbox/linux-backend.js +0 -328
- package/dist/core/sandbox/linux-backend.js.map +0 -1
- package/dist/core/sandbox/macos-backend.d.ts +0 -30
- package/dist/core/sandbox/macos-backend.d.ts.map +0 -1
- package/dist/core/sandbox/macos-backend.js +0 -385
- package/dist/core/sandbox/macos-backend.js.map +0 -1
- package/dist/core/sandbox/network-proxy.d.ts +0 -38
- package/dist/core/sandbox/network-proxy.d.ts.map +0 -1
- package/dist/core/sandbox/network-proxy.js +0 -153
- package/dist/core/sandbox/network-proxy.js.map +0 -1
- package/dist/core/sandbox/network-refusal.d.ts +0 -24
- package/dist/core/sandbox/network-refusal.d.ts.map +0 -1
- package/dist/core/sandbox/network-refusal.js +0 -72
- package/dist/core/sandbox/network-refusal.js.map +0 -1
- package/dist/core/sandbox/policy.d.ts +0 -40
- package/dist/core/sandbox/policy.d.ts.map +0 -1
- package/dist/core/sandbox/policy.js +0 -55
- package/dist/core/sandbox/policy.js.map +0 -1
- package/dist/core/sandbox/profiles.d.ts +0 -25
- package/dist/core/sandbox/profiles.d.ts.map +0 -1
- package/dist/core/sandbox/profiles.js +0 -23
- package/dist/core/sandbox/profiles.js.map +0 -1
- package/dist/core/sandbox/rpc/command-client.d.ts +0 -28
- package/dist/core/sandbox/rpc/command-client.d.ts.map +0 -1
- package/dist/core/sandbox/rpc/command-client.js +0 -86
- package/dist/core/sandbox/rpc/command-client.js.map +0 -1
- package/dist/core/sandbox/rpc/command-proxy.d.ts +0 -48
- package/dist/core/sandbox/rpc/command-proxy.d.ts.map +0 -1
- package/dist/core/sandbox/rpc/command-proxy.js +0 -120
- package/dist/core/sandbox/rpc/command-proxy.js.map +0 -1
- package/dist/core/sandbox/rpc/credential-client.d.ts +0 -20
- package/dist/core/sandbox/rpc/credential-client.d.ts.map +0 -1
- package/dist/core/sandbox/rpc/credential-client.js +0 -170
- package/dist/core/sandbox/rpc/credential-client.js.map +0 -1
- package/dist/core/sandbox/rpc/credential-proxy.d.ts +0 -44
- package/dist/core/sandbox/rpc/credential-proxy.d.ts.map +0 -1
- package/dist/core/sandbox/rpc/credential-proxy.js +0 -287
- package/dist/core/sandbox/rpc/credential-proxy.js.map +0 -1
- package/dist/core/sandbox/rpc/framing.d.ts +0 -35
- package/dist/core/sandbox/rpc/framing.d.ts.map +0 -1
- package/dist/core/sandbox/rpc/framing.js +0 -114
- package/dist/core/sandbox/rpc/framing.js.map +0 -1
- package/dist/core/sandbox/rpc/git-credential-helper.d.ts +0 -47
- package/dist/core/sandbox/rpc/git-credential-helper.d.ts.map +0 -1
- package/dist/core/sandbox/rpc/git-credential-helper.js +0 -222
- package/dist/core/sandbox/rpc/git-credential-helper.js.map +0 -1
- package/dist/core/sandbox/rpc/git-credential-proxy.d.ts +0 -66
- package/dist/core/sandbox/rpc/git-credential-proxy.d.ts.map +0 -1
- package/dist/core/sandbox/rpc/git-credential-proxy.js +0 -209
- package/dist/core/sandbox/rpc/git-credential-proxy.js.map +0 -1
- package/dist/core/sandbox/supervisor-temp.d.ts +0 -18
- package/dist/core/sandbox/supervisor-temp.d.ts.map +0 -1
- package/dist/core/sandbox/supervisor-temp.js +0 -21
- package/dist/core/sandbox/supervisor-temp.js.map +0 -1
- package/dist/core/sandbox/supervisor.d.ts +0 -62
- package/dist/core/sandbox/supervisor.d.ts.map +0 -1
- package/dist/core/sandbox/supervisor.js +0 -32
- package/dist/core/sandbox/supervisor.js.map +0 -1
- package/dist/core/sandbox/terminal-handoff.d.ts +0 -68
- package/dist/core/sandbox/terminal-handoff.d.ts.map +0 -1
- package/dist/core/sandbox/terminal-handoff.js +0 -221
- package/dist/core/sandbox/terminal-handoff.js.map +0 -1
- package/dist/core/sandbox/terminal-size.d.ts +0 -53
- package/dist/core/sandbox/terminal-size.d.ts.map +0 -1
- package/dist/core/sandbox/terminal-size.js +0 -126
- package/dist/core/sandbox/terminal-size.js.map +0 -1
- package/dist/core/sandbox/violations.d.ts +0 -23
- package/dist/core/sandbox/violations.d.ts.map +0 -1
- package/dist/core/sandbox/violations.js +0 -30
- package/dist/core/sandbox/violations.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"host-approval.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/host-approval.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,qBAAqB;IACrC,OAAO,EAAE,eAAe,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC,WAAW,CAAC;CAC5B;AAyDD,4DAA4D;AAC5D,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,qBAAqB,GAC5B,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,SAAS,CAUpE;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACvC,OAAO,EAAE,qBAAqB,GAC5B,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,SAAS,CASlD;AAED;;;;;;GAMG;AACH,wBAAgB,+BAA+B,CAC9C,OAAO,EAAE,qBAAqB,GAC5B,CAAC,CAAC,OAAO,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,SAAS,CAYxF","sourcesContent":["import type { TerminalHandoff } from \"./terminal-handoff.ts\";\n\n/**\n * The supervisor's own approval prompt for one refused host.\n *\n * ADR 0023 puts the decision here rather than in the child. An approval asserted from\n * inside the boundary is indistinguishable, at the supervisor, from one forged by a\n * postinstall script or a git hook -- the code the boundary exists to contain -- so the\n * human's answer is read by the side that owns the decision, on a terminal borrowed back\n * from the child for the duration.\n *\n * Returning undefined when there is no terminal is the mechanism, not a fallback. The\n * proxy's own deny-without-asking path then runs unchanged, so ADR 0005's headless, print,\n * JSON, and RPC behaviour holds by construction rather than by a mode check that a later\n * change could forget to make.\n */\nexport interface TerminalPromptOptions {\n\thandoff: TerminalHandoff;\n\tinput?: NodeJS.ReadStream;\n\toutput?: NodeJS.WriteStream;\n}\n\n/**\n * Build a yes/no prompt the supervisor owns, or nothing at all when there is no terminal.\n *\n * Both sandbox escalations ask the same shape of question and must fail closed the same\n * way, so they share one implementation. Returning undefined rather than a\n * function-that-says-no is deliberate: the caller's own refuse path then runs untouched,\n * which is how ADR 0005's headless behaviour stays correct without a mode check.\n */\nfunction createTerminalPrompt(\n\toptions: TerminalPromptOptions,\n\tcompose: (subject: string) => { question: string; granted: string; refused: string },\n): ((subject: string) => Promise<boolean>) | undefined {\n\tconst input = options.input ?? process.stdin;\n\tconst output = options.output ?? process.stdout;\n\tif (!input.isTTY || !output.isTTY) return undefined;\n\n\tfunction readAnswer(): Promise<string> {\n\t\treturn new Promise((resolve) => {\n\t\t\tlet buffer = \"\";\n\t\t\tconst finish = (value: string) => {\n\t\t\t\tinput.off(\"data\", onData);\n\t\t\t\tinput.off(\"error\", onError);\n\t\t\t\tinput.off(\"end\", onEnd);\n\t\t\t\t// The child had stdin in raw mode and takes it back on resume, so the\n\t\t\t\t// supervisor leaves the stream paused rather than owning it past the answer.\n\t\t\t\tinput.pause();\n\t\t\t\tresolve(value);\n\t\t\t};\n\t\t\tconst onData = (chunk: Buffer | string) => {\n\t\t\t\tbuffer += chunk.toString();\n\t\t\t\tconst newline = buffer.indexOf(\"\\n\");\n\t\t\t\tif (newline >= 0) finish(buffer.slice(0, newline));\n\t\t\t};\n\t\t\t// Anything unreadable declines. Treating an unanswerable prompt as consent would\n\t\t\t// hand the grant to whatever broke the terminal.\n\t\t\tconst onError = () => finish(\"\");\n\t\t\tconst onEnd = () => finish(buffer);\n\t\t\tinput.on(\"data\", onData);\n\t\t\tinput.on(\"error\", onError);\n\t\t\tinput.on(\"end\", onEnd);\n\t\t\tinput.resume();\n\t\t});\n\t}\n\n\treturn (subject: string): Promise<boolean> =>\n\t\toptions.handoff.borrowTerminal(async () => {\n\t\t\tconst text = compose(subject);\n\t\t\toutput.write(`\\n${text.question}`);\n\t\t\tconst answer = (await readAnswer()).trim().toLowerCase();\n\t\t\tconst approved = answer === \"y\" || answer === \"yes\";\n\t\t\toutput.write(`${approved ? text.granted : text.refused}\\n\\n`);\n\t\t\treturn approved;\n\t\t});\n}\n\n/** Approve one refused host for the rest of the session. */\nexport function createHostApprover(\n\toptions: TerminalPromptOptions,\n): ((hostname: string, port: number) => Promise<boolean>) | undefined {\n\tconst prompt = createTerminalPrompt(options, (subject) => ({\n\t\tquestion:\n\t\t\t`The sandboxed session asked to reach ${subject}, which is not on the network allowlist.\\n` +\n\t\t\t`Allow it for this session only? [y/N] `,\n\t\tgranted: `Allowed ${subject} for this session. Add it to \"network.allowedHosts\" in your global settings to make it permanent.`,\n\t\trefused: `Refused ${subject}.`,\n\t}));\n\tif (!prompt) return undefined;\n\treturn (hostname: string, port: number) => prompt(`${hostname}:${port}`);\n}\n\n/**\n * Release the host's own git credential for one host, for the rest of the session.\n *\n * Separate from the host approval above even though both are yes/no, because they are\n * different grants and conflating them would let approving a push also release a token to\n * everything else the session later contacts.\n */\nexport function createCredentialReleaser(\n\toptions: TerminalPromptOptions,\n): ((host: string) => Promise<boolean>) | undefined {\n\treturn createTerminalPrompt(options, (subject) => ({\n\t\tquestion:\n\t\t\t`The sandboxed session asked for your git credential for ${subject}.\\n` +\n\t\t\t`It is read from your host credential store and never written into the workspace.\\n` +\n\t\t\t`Release it for this session only? [y/N] `,\n\t\tgranted: `Released the ${subject} credential for this session.`,\n\t\trefused: `Refused the ${subject} credential.`,\n\t}));\n}\n\n/**\n * Approve one command to run in a separate child with one extra writable root.\n *\n * Unlike a host or a credential, nothing is remembered. A command is not a stable subject\n * to grant against, and a remembered approval would silently cover the next command that\n * happened to name the same root.\n */\nexport function createCommandEscalationApprover(\n\toptions: TerminalPromptOptions,\n): ((request: { command: string; writableRoot: string }) => Promise<boolean>) | undefined {\n\tconst prompt = createTerminalPrompt(options, (subject) => ({\n\t\tquestion:\n\t\t\t`The sandboxed session asked to run a command with write access outside its workspace.\\n\\n` +\n\t\t\t` ${subject}\\n\\n` +\n\t\t\t`It runs once, in a separate sandbox. The session's own boundary does not change.\\n` +\n\t\t\t`Allow it? [y/N] `,\n\t\tgranted: \"Ran the command once with that root writable.\",\n\t\trefused: \"Refused.\",\n\t}));\n\tif (!prompt) return undefined;\n\treturn (request) => prompt(`${request.command}\\n (writable: ${request.writableRoot})`);\n}\n"]}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Build a yes/no prompt the supervisor owns, or nothing at all when there is no terminal.
|
|
3
|
-
*
|
|
4
|
-
* Both sandbox escalations ask the same shape of question and must fail closed the same
|
|
5
|
-
* way, so they share one implementation. Returning undefined rather than a
|
|
6
|
-
* function-that-says-no is deliberate: the caller's own refuse path then runs untouched,
|
|
7
|
-
* which is how ADR 0005's headless behaviour stays correct without a mode check.
|
|
8
|
-
*/
|
|
9
|
-
function createTerminalPrompt(options, compose) {
|
|
10
|
-
const input = options.input ?? process.stdin;
|
|
11
|
-
const output = options.output ?? process.stdout;
|
|
12
|
-
if (!input.isTTY || !output.isTTY)
|
|
13
|
-
return undefined;
|
|
14
|
-
function readAnswer() {
|
|
15
|
-
return new Promise((resolve) => {
|
|
16
|
-
let buffer = "";
|
|
17
|
-
const finish = (value) => {
|
|
18
|
-
input.off("data", onData);
|
|
19
|
-
input.off("error", onError);
|
|
20
|
-
input.off("end", onEnd);
|
|
21
|
-
// The child had stdin in raw mode and takes it back on resume, so the
|
|
22
|
-
// supervisor leaves the stream paused rather than owning it past the answer.
|
|
23
|
-
input.pause();
|
|
24
|
-
resolve(value);
|
|
25
|
-
};
|
|
26
|
-
const onData = (chunk) => {
|
|
27
|
-
buffer += chunk.toString();
|
|
28
|
-
const newline = buffer.indexOf("\n");
|
|
29
|
-
if (newline >= 0)
|
|
30
|
-
finish(buffer.slice(0, newline));
|
|
31
|
-
};
|
|
32
|
-
// Anything unreadable declines. Treating an unanswerable prompt as consent would
|
|
33
|
-
// hand the grant to whatever broke the terminal.
|
|
34
|
-
const onError = () => finish("");
|
|
35
|
-
const onEnd = () => finish(buffer);
|
|
36
|
-
input.on("data", onData);
|
|
37
|
-
input.on("error", onError);
|
|
38
|
-
input.on("end", onEnd);
|
|
39
|
-
input.resume();
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
return (subject) => options.handoff.borrowTerminal(async () => {
|
|
43
|
-
const text = compose(subject);
|
|
44
|
-
output.write(`\n${text.question}`);
|
|
45
|
-
const answer = (await readAnswer()).trim().toLowerCase();
|
|
46
|
-
const approved = answer === "y" || answer === "yes";
|
|
47
|
-
output.write(`${approved ? text.granted : text.refused}\n\n`);
|
|
48
|
-
return approved;
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
/** Approve one refused host for the rest of the session. */
|
|
52
|
-
export function createHostApprover(options) {
|
|
53
|
-
const prompt = createTerminalPrompt(options, (subject) => ({
|
|
54
|
-
question: `The sandboxed session asked to reach ${subject}, which is not on the network allowlist.\n` +
|
|
55
|
-
`Allow it for this session only? [y/N] `,
|
|
56
|
-
granted: `Allowed ${subject} for this session. Add it to "network.allowedHosts" in your global settings to make it permanent.`,
|
|
57
|
-
refused: `Refused ${subject}.`,
|
|
58
|
-
}));
|
|
59
|
-
if (!prompt)
|
|
60
|
-
return undefined;
|
|
61
|
-
return (hostname, port) => prompt(`${hostname}:${port}`);
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Release the host's own git credential for one host, for the rest of the session.
|
|
65
|
-
*
|
|
66
|
-
* Separate from the host approval above even though both are yes/no, because they are
|
|
67
|
-
* different grants and conflating them would let approving a push also release a token to
|
|
68
|
-
* everything else the session later contacts.
|
|
69
|
-
*/
|
|
70
|
-
export function createCredentialReleaser(options) {
|
|
71
|
-
return createTerminalPrompt(options, (subject) => ({
|
|
72
|
-
question: `The sandboxed session asked for your git credential for ${subject}.\n` +
|
|
73
|
-
`It is read from your host credential store and never written into the workspace.\n` +
|
|
74
|
-
`Release it for this session only? [y/N] `,
|
|
75
|
-
granted: `Released the ${subject} credential for this session.`,
|
|
76
|
-
refused: `Refused the ${subject} credential.`,
|
|
77
|
-
}));
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Approve one command to run in a separate child with one extra writable root.
|
|
81
|
-
*
|
|
82
|
-
* Unlike a host or a credential, nothing is remembered. A command is not a stable subject
|
|
83
|
-
* to grant against, and a remembered approval would silently cover the next command that
|
|
84
|
-
* happened to name the same root.
|
|
85
|
-
*/
|
|
86
|
-
export function createCommandEscalationApprover(options) {
|
|
87
|
-
const prompt = createTerminalPrompt(options, (subject) => ({
|
|
88
|
-
question: `The sandboxed session asked to run a command with write access outside its workspace.\n\n` +
|
|
89
|
-
` ${subject}\n\n` +
|
|
90
|
-
`It runs once, in a separate sandbox. The session's own boundary does not change.\n` +
|
|
91
|
-
`Allow it? [y/N] `,
|
|
92
|
-
granted: "Ran the command once with that root writable.",
|
|
93
|
-
refused: "Refused.",
|
|
94
|
-
}));
|
|
95
|
-
if (!prompt)
|
|
96
|
-
return undefined;
|
|
97
|
-
return (request) => prompt(`${request.command}\n (writable: ${request.writableRoot})`);
|
|
98
|
-
}
|
|
99
|
-
//# sourceMappingURL=host-approval.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"host-approval.js","sourceRoot":"","sources":["../../../src/core/sandbox/host-approval.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TerminalHandoff } from \"./terminal-handoff.ts\";\n\n/**\n * The supervisor's own approval prompt for one refused host.\n *\n * ADR 0023 puts the decision here rather than in the child. An approval asserted from\n * inside the boundary is indistinguishable, at the supervisor, from one forged by a\n * postinstall script or a git hook -- the code the boundary exists to contain -- so the\n * human's answer is read by the side that owns the decision, on a terminal borrowed back\n * from the child for the duration.\n *\n * Returning undefined when there is no terminal is the mechanism, not a fallback. The\n * proxy's own deny-without-asking path then runs unchanged, so ADR 0005's headless, print,\n * JSON, and RPC behaviour holds by construction rather than by a mode check that a later\n * change could forget to make.\n */\nexport interface TerminalPromptOptions {\n\thandoff: TerminalHandoff;\n\tinput?: NodeJS.ReadStream;\n\toutput?: NodeJS.WriteStream;\n}\n\n/**\n * Build a yes/no prompt the supervisor owns, or nothing at all when there is no terminal.\n *\n * Both sandbox escalations ask the same shape of question and must fail closed the same\n * way, so they share one implementation. Returning undefined rather than a\n * function-that-says-no is deliberate: the caller's own refuse path then runs untouched,\n * which is how ADR 0005's headless behaviour stays correct without a mode check.\n */\nfunction createTerminalPrompt(\n\toptions: TerminalPromptOptions,\n\tcompose: (subject: string) => { question: string; granted: string; refused: string },\n): ((subject: string) => Promise<boolean>) | undefined {\n\tconst input = options.input ?? process.stdin;\n\tconst output = options.output ?? process.stdout;\n\tif (!input.isTTY || !output.isTTY) return undefined;\n\n\tfunction readAnswer(): Promise<string> {\n\t\treturn new Promise((resolve) => {\n\t\t\tlet buffer = \"\";\n\t\t\tconst finish = (value: string) => {\n\t\t\t\tinput.off(\"data\", onData);\n\t\t\t\tinput.off(\"error\", onError);\n\t\t\t\tinput.off(\"end\", onEnd);\n\t\t\t\t// The child had stdin in raw mode and takes it back on resume, so the\n\t\t\t\t// supervisor leaves the stream paused rather than owning it past the answer.\n\t\t\t\tinput.pause();\n\t\t\t\tresolve(value);\n\t\t\t};\n\t\t\tconst onData = (chunk: Buffer | string) => {\n\t\t\t\tbuffer += chunk.toString();\n\t\t\t\tconst newline = buffer.indexOf(\"\\n\");\n\t\t\t\tif (newline >= 0) finish(buffer.slice(0, newline));\n\t\t\t};\n\t\t\t// Anything unreadable declines. Treating an unanswerable prompt as consent would\n\t\t\t// hand the grant to whatever broke the terminal.\n\t\t\tconst onError = () => finish(\"\");\n\t\t\tconst onEnd = () => finish(buffer);\n\t\t\tinput.on(\"data\", onData);\n\t\t\tinput.on(\"error\", onError);\n\t\t\tinput.on(\"end\", onEnd);\n\t\t\tinput.resume();\n\t\t});\n\t}\n\n\treturn (subject: string): Promise<boolean> =>\n\t\toptions.handoff.borrowTerminal(async () => {\n\t\t\tconst text = compose(subject);\n\t\t\toutput.write(`\\n${text.question}`);\n\t\t\tconst answer = (await readAnswer()).trim().toLowerCase();\n\t\t\tconst approved = answer === \"y\" || answer === \"yes\";\n\t\t\toutput.write(`${approved ? text.granted : text.refused}\\n\\n`);\n\t\t\treturn approved;\n\t\t});\n}\n\n/** Approve one refused host for the rest of the session. */\nexport function createHostApprover(\n\toptions: TerminalPromptOptions,\n): ((hostname: string, port: number) => Promise<boolean>) | undefined {\n\tconst prompt = createTerminalPrompt(options, (subject) => ({\n\t\tquestion:\n\t\t\t`The sandboxed session asked to reach ${subject}, which is not on the network allowlist.\\n` +\n\t\t\t`Allow it for this session only? [y/N] `,\n\t\tgranted: `Allowed ${subject} for this session. Add it to \"network.allowedHosts\" in your global settings to make it permanent.`,\n\t\trefused: `Refused ${subject}.`,\n\t}));\n\tif (!prompt) return undefined;\n\treturn (hostname: string, port: number) => prompt(`${hostname}:${port}`);\n}\n\n/**\n * Release the host's own git credential for one host, for the rest of the session.\n *\n * Separate from the host approval above even though both are yes/no, because they are\n * different grants and conflating them would let approving a push also release a token to\n * everything else the session later contacts.\n */\nexport function createCredentialReleaser(\n\toptions: TerminalPromptOptions,\n): ((host: string) => Promise<boolean>) | undefined {\n\treturn createTerminalPrompt(options, (subject) => ({\n\t\tquestion:\n\t\t\t`The sandboxed session asked for your git credential for ${subject}.\\n` +\n\t\t\t`It is read from your host credential store and never written into the workspace.\\n` +\n\t\t\t`Release it for this session only? [y/N] `,\n\t\tgranted: `Released the ${subject} credential for this session.`,\n\t\trefused: `Refused the ${subject} credential.`,\n\t}));\n}\n\n/**\n * Approve one command to run in a separate child with one extra writable root.\n *\n * Unlike a host or a credential, nothing is remembered. A command is not a stable subject\n * to grant against, and a remembered approval would silently cover the next command that\n * happened to name the same root.\n */\nexport function createCommandEscalationApprover(\n\toptions: TerminalPromptOptions,\n): ((request: { command: string; writableRoot: string }) => Promise<boolean>) | undefined {\n\tconst prompt = createTerminalPrompt(options, (subject) => ({\n\t\tquestion:\n\t\t\t`The sandboxed session asked to run a command with write access outside its workspace.\\n\\n` +\n\t\t\t` ${subject}\\n\\n` +\n\t\t\t`It runs once, in a separate sandbox. The session's own boundary does not change.\\n` +\n\t\t\t`Allow it? [y/N] `,\n\t\tgranted: \"Ran the command once with that root writable.\",\n\t\trefused: \"Refused.\",\n\t}));\n\tif (!prompt) return undefined;\n\treturn (request) => prompt(`${request.command}\\n (writable: ${request.writableRoot})`);\n}\n"]}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { type CommandEscalationRequest } from "./rpc/command-proxy.ts";
|
|
3
|
-
import { fillHostGitCredential } from "./rpc/git-credential-helper.ts";
|
|
4
|
-
import type { SandboxBackend } from "./supervisor.ts";
|
|
5
|
-
import type { SandboxViolationStore } from "./violations.ts";
|
|
6
|
-
export interface LinuxSandboxBackendOptions {
|
|
7
|
-
/** Injectable only for preflight tests; production uses the running platform. */
|
|
8
|
-
platform?: NodeJS.Platform;
|
|
9
|
-
commandExists?: (command: string) => boolean;
|
|
10
|
-
violationStore?: SandboxViolationStore;
|
|
11
|
-
/** Injectable only to prove crash cleanup in tests; production spawns `bwrap` directly. */
|
|
12
|
-
spawnChild?: typeof spawn;
|
|
13
|
-
/**
|
|
14
|
-
* Injectable only for tests, which have no terminal and would otherwise be unable to
|
|
15
|
-
* exercise a released credential at all. Production builds the releaser from the
|
|
16
|
-
* terminal handoff, and its absence there is what makes a headless session refuse.
|
|
17
|
-
*/
|
|
18
|
-
requestGitCredentialRelease?: (host: string) => Promise<boolean>;
|
|
19
|
-
/** Injectable only for tests; production resolves against the real host store. */
|
|
20
|
-
fillGitCredential?: typeof fillHostGitCredential;
|
|
21
|
-
/** Injectable only for tests, which have no terminal to approve an escalation at. */
|
|
22
|
-
requestCommandEscalation?: (request: CommandEscalationRequest) => Promise<boolean>;
|
|
23
|
-
/** Retained for source compatibility; launch state is now required on each launch. */
|
|
24
|
-
supervisorStateDirectory?: string;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Where the supervisor's proxy socket lives on each side of the boundary.
|
|
28
|
-
*
|
|
29
|
-
* AF_UNIX caps `sun_path` at 108 bytes, so the socket cannot live under the workspace:
|
|
30
|
-
* a deep-but-ordinary project directory exhausts that budget and the sandbox fails to
|
|
31
|
-
* start at all with `listen EINVAL`. The host side therefore gets a short, unique path
|
|
32
|
-
* under the system temp directory, and it is bind-mounted into the child.
|
|
33
|
-
*
|
|
34
|
-
* The child-side path has to sit under `/home`. That is the one writable mount at the
|
|
35
|
-
* point the mountpoint is created — the sandbox root is a read-only bind, so bwrap
|
|
36
|
-
* cannot create a mountpoint at `/run`, `/tmp`, or anywhere else on it.
|
|
37
|
-
*/
|
|
38
|
-
export declare function resolveProxySocketPaths(temporaryDirectory?: string): {
|
|
39
|
-
hostSocketPath: string;
|
|
40
|
-
childSocketPath: string;
|
|
41
|
-
};
|
|
42
|
-
/**
|
|
43
|
-
* Linux's deliberately small platform adapter. The sandbox starts from a read-only
|
|
44
|
-
* host root and bind-mounts only the canonical workspace as writable. `--unshare-net`
|
|
45
|
-
* removes all direct network interfaces for the child and every descendant.
|
|
46
|
-
*/
|
|
47
|
-
export declare function createLinuxSandboxBackend(options?: LinuxSandboxBackendOptions): SandboxBackend;
|
|
48
|
-
//# sourceMappingURL=linux-backend.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"linux-backend.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/linux-backend.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAa,MAAM,oBAAoB,CAAC;AAQtD,OAAO,EAEN,KAAK,wBAAwB,EAI7B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACN,qBAAqB,EAIrB,MAAM,gCAAgC,CAAC;AAExC,OAAO,KAAK,EAAE,cAAc,EAAiB,MAAM,iBAAiB,CAAC;AAQrE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAK7D,MAAM,WAAW,0BAA0B;IAC1C,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,cAAc,CAAC,EAAE,qBAAqB,CAAC;IACvC,2FAA2F;IAC3F,UAAU,CAAC,EAAE,OAAO,KAAK,CAAC;IAC1B;;;;OAIG;IACH,2BAA2B,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACjE,kFAAkF;IAClF,iBAAiB,CAAC,EAAE,OAAO,qBAAqB,CAAC;IACjD,qFAAqF;IACrF,wBAAwB,CAAC,EAAE,CAAC,OAAO,EAAE,wBAAwB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACnF,sFAAsF;IACtF,wBAAwB,CAAC,EAAE,MAAM,CAAC;CAClC;AAcD;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,kBAAkB,GAAE,MAAiB,GAAG;IAC/E,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;CACxB,CAKA;AAmBD;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE,0BAA0B,GAAG,cAAc,CA2N9F","sourcesContent":["import { spawn, spawnSync } from \"node:child_process\";\nimport { randomBytes } from \"node:crypto\";\nimport { closeSync, mkdirSync, openSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { buildBwrapArguments, descriptorBackedReadOnlyPaths } from \"./bwrap-arguments.ts\";\nimport { createCommandEscalationApprover, createCredentialReleaser, createHostApprover } from \"./host-approval.ts\";\nimport { createSandboxNetworkProxy, type SandboxNetworkProxy } from \"./network-proxy.ts\";\nimport {\n\tCOMMAND_ESCALATION_SOCKET_VARIABLE,\n\ttype CommandEscalationRequest,\n\ttype CommandEscalationResult,\n\tcreateCommandEscalationProxy,\n\tresolveCommandEscalationChannelPaths,\n} from \"./rpc/command-proxy.ts\";\nimport {\n\tfillHostGitCredential,\n\tGIT_CREDENTIAL_SOCKET_VARIABLE,\n\tresolveGitCredentialChannelPaths,\n\twriteGitCredentialHelper,\n} from \"./rpc/git-credential-helper.ts\";\nimport { createGitCredentialProxy, type GitCredentialProxy } from \"./rpc/git-credential-proxy.ts\";\nimport type { SandboxBackend, SandboxLaunch } from \"./supervisor.ts\";\nimport {\n\tcreateTerminalHandoff,\n\tTERMINAL_HANDOFF_ACK_PATH_VARIABLE,\n\tTERMINAL_HANDOFF_PATH_VARIABLE,\n\ttype TerminalHandoff,\n} from \"./terminal-handoff.ts\";\nimport { publishTerminalSize, TERMINAL_SIZE_PATH_VARIABLE } from \"./terminal-size.ts\";\nimport type { SandboxViolationStore } from \"./violations.ts\";\n\n/** AF_UNIX `sun_path` is 108 bytes on Linux, including the terminating NUL. */\nconst SUN_PATH_LIMIT = 108;\n\nexport interface LinuxSandboxBackendOptions {\n\t/** Injectable only for preflight tests; production uses the running platform. */\n\tplatform?: NodeJS.Platform;\n\tcommandExists?: (command: string) => boolean;\n\tviolationStore?: SandboxViolationStore;\n\t/** Injectable only to prove crash cleanup in tests; production spawns `bwrap` directly. */\n\tspawnChild?: typeof spawn;\n\t/**\n\t * Injectable only for tests, which have no terminal and would otherwise be unable to\n\t * exercise a released credential at all. Production builds the releaser from the\n\t * terminal handoff, and its absence there is what makes a headless session refuse.\n\t */\n\trequestGitCredentialRelease?: (host: string) => Promise<boolean>;\n\t/** Injectable only for tests; production resolves against the real host store. */\n\tfillGitCredential?: typeof fillHostGitCredential;\n\t/** Injectable only for tests, which have no terminal to approve an escalation at. */\n\trequestCommandEscalation?: (request: CommandEscalationRequest) => Promise<boolean>;\n\t/** Retained for source compatibility; launch state is now required on each launch. */\n\tsupervisorStateDirectory?: string;\n}\n\nfunction commandExists(command: string): boolean {\n\tconst result = spawnSync(command, [\"--version\"], { stdio: \"ignore\", timeout: 1_000 });\n\treturn result.status === 0;\n}\n\nfunction waitForExit(child: ReturnType<typeof spawn>): Promise<number> {\n\treturn new Promise((resolve, reject) => {\n\t\tchild.once(\"error\", reject);\n\t\tchild.once(\"exit\", (code) => resolve(code ?? 1));\n\t});\n}\n\n/**\n * Where the supervisor's proxy socket lives on each side of the boundary.\n *\n * AF_UNIX caps `sun_path` at 108 bytes, so the socket cannot live under the workspace:\n * a deep-but-ordinary project directory exhausts that budget and the sandbox fails to\n * start at all with `listen EINVAL`. The host side therefore gets a short, unique path\n * under the system temp directory, and it is bind-mounted into the child.\n *\n * The child-side path has to sit under `/home`. That is the one writable mount at the\n * point the mountpoint is created — the sandbox root is a read-only bind, so bwrap\n * cannot create a mountpoint at `/run`, `/tmp`, or anywhere else on it.\n */\nexport function resolveProxySocketPaths(temporaryDirectory: string = tmpdir()): {\n\thostSocketPath: string;\n\tchildSocketPath: string;\n} {\n\tconst name = `apex-${process.pid}-${randomBytes(4).toString(\"hex\")}.sock`;\n\t// An unusually long TMPDIR would reintroduce the very limit this avoids.\n\tconst base = join(temporaryDirectory, name).length + 1 > SUN_PATH_LIMIT ? \"/tmp\" : temporaryDirectory;\n\treturn { hostSocketPath: join(base, name), childSocketPath: `/home/${name}` };\n}\n\n/**\n * Classify an unsuccessful child from what its stderr actually evidences, or return\n * undefined when nothing suggests the boundary refused anything.\n *\n * A non-zero exit is not a violation. An invalid API key, a failing test command, or a\n * script exiting 1 are the child's own business, and recording them as sandbox\n * violations blames the boundary for failures it had no part in — which is worse than\n * silence, because it sends whoever reads the output looking for a refusal that never\n * happened. Only Linux's own refusal wording earns a violation here; the proxy records\n * network refusals itself, before this fallback is consulted.\n */\nfunction classifySandboxFailure(stderr: string): \"filesystem\" | \"network\" | undefined {\n\tif (/Read-only file system|Permission denied|Operation not permitted/i.test(stderr)) return \"filesystem\";\n\tif (/Network is unreachable|ENETUNREACH|EHOSTUNREACH/i.test(stderr)) return \"network\";\n\treturn undefined;\n}\n\n/**\n * Linux's deliberately small platform adapter. The sandbox starts from a read-only\n * host root and bind-mounts only the canonical workspace as writable. `--unshare-net`\n * removes all direct network interfaces for the child and every descendant.\n */\nexport function createLinuxSandboxBackend(options?: LinuxSandboxBackendOptions): SandboxBackend {\n\tconst platform = options?.platform ?? process.platform;\n\tif (platform !== \"linux\") {\n\t\treturn {\n\t\t\tstatus: { kind: \"unavailable\", reason: \"OS sandbox is supported on Linux only.\" },\n\t\t\tasync launch() {\n\t\t\t\tthrow new Error(\"Linux sandbox backend is unavailable.\");\n\t\t\t},\n\t\t\tasync close() {},\n\t\t};\n\t}\n\tconst hasCommand = options?.commandExists ?? commandExists;\n\tconst violationStore = options?.violationStore;\n\tlet proxy: SandboxNetworkProxy | undefined;\n\tlet handoff: TerminalHandoff | undefined;\n\tlet gitCredentialProxy: GitCredentialProxy | undefined;\n\tlet gitCredentialDirectory: string | undefined;\n\tlet escalationProxy: Awaited<ReturnType<typeof createCommandEscalationProxy>> | undefined;\n\tlet escalationDirectory: string | undefined;\n\tif (!hasCommand(\"bwrap\")) {\n\t\treturn {\n\t\t\tstatus: { kind: \"unavailable\", reason: \"Bubblewrap (bwrap) is required for OS sandboxing.\" },\n\t\t\tasync launch() {\n\t\t\t\tthrow new Error(\"Linux sandbox backend is unavailable.\");\n\t\t\t},\n\t\t\tasync close() {},\n\t\t};\n\t}\n\treturn {\n\t\tstatus: { kind: \"enforced\" },\n\t\tasync launch(launch: SandboxLaunch): Promise<number> {\n\t\t\tconst supervisorStateDirectory = launch.supervisorStateDirectory;\n\t\t\tmkdirSync(supervisorStateDirectory, { recursive: true, mode: 0o700 });\n\t\t\tconst stateDirectory = launch.policy.workspace;\n\t\t\tconst acknowledgementPath = join(launch.policy.workspace, \".apex-code\", \"sandbox-handoff-ack\");\n\t\t\tmkdirSync(join(launch.policy.workspace, \".apex-code\"), { recursive: true });\n\n\t\t\tconst violationCountBeforeLaunch = violationStore?.totalCount ?? 0;\n\n\t\t\tconst { hostSocketPath, childSocketPath } = resolveProxySocketPaths();\n\t\t\thandoff = createTerminalHandoff(supervisorStateDirectory, { acknowledgementPath });\n\t\t\tconst gitCredentialPaths = resolveGitCredentialChannelPaths();\n\t\t\tgitCredentialDirectory = gitCredentialPaths.hostSocketDirectory;\n\t\t\tproxy = await createSandboxNetworkProxy({\n\t\t\t\tsocketPath: hostSocketPath,\n\t\t\t\tallowedHosts: launch.policy.allowedHosts,\n\t\t\t\tviolationStore,\n\t\t\t\t// Undefined without a terminal, which leaves the proxy's own\n\t\t\t\t// deny-without-asking path in place for headless, print, JSON, and RPC.\n\t\t\t\trequestApproval: createHostApprover({ handoff }),\n\t\t\t});\n\t\t\t// After the network proxy, because reachability is its answer to give: the\n\t\t\t// credential channel must never hand out a token for a host this session\n\t\t\t// cannot even open a connection to.\n\t\t\tgitCredentialProxy = await createGitCredentialProxy({\n\t\t\t\tsocketPath: gitCredentialPaths.hostSocketPath,\n\t\t\t\tisHostAllowed: (host) => proxy?.isHostReachable(host) ?? false,\n\t\t\t\trequestRelease: options?.requestGitCredentialRelease ?? createCredentialReleaser({ handoff }),\n\t\t\t\t// The supervisor's own environment, deliberately: this runs outside the\n\t\t\t\t// boundary and must resolve against the real host home, which is exactly\n\t\t\t\t// what the child cannot see.\n\t\t\t\tfillCredential: (request) =>\n\t\t\t\t\t(options?.fillGitCredential ?? fillHostGitCredential)(request, { environment: process.env }),\n\t\t\t\tviolationStore,\n\t\t\t});\n\t\t\twriteGitCredentialHelper(supervisorStateDirectory);\n\n\t\t\tconst escalationPaths = resolveCommandEscalationChannelPaths();\n\t\t\tescalationDirectory = escalationPaths.hostSocketDirectory;\n\n\t\t\t// .cjs forces CommonJS regardless of the target workspace's package.json \"type\"\n\t\t\t// field -- a plain .js here would be parsed as ESM under \"type\": \"module\" and\n\t\t\t// crash on `require`, since Node resolves module type from the nearest package.json.\n\t\t\tconst relayScriptPath = join(supervisorStateDirectory, \"relay.cjs\");\n\t\t\twriteFileSync(\n\t\t\t\trelayScriptPath,\n\t\t\t\t`\nconst net = require(\"node:net\");\nconst child_process = require(\"node:child_process\");\n\nconst socketPath = process.env.APEX_UDS_PATH;\nconst server = net.createServer((c) => {\n\tconst client = net.connect(socketPath);\n\tc.pipe(client).pipe(c);\n\tc.on(\"error\", () => {});\n\tclient.on(\"error\", () => {});\n});\n\nserver.listen(0, \"127.0.0.1\", () => {\n\tconst port = server.address().port;\n\tconst env = Object.assign({}, process.env, {\n\t\tHTTP_PROXY: \\`http://127.0.0.1:\\${port}\\`,\n\t\tHTTPS_PROXY: \\`http://127.0.0.1:\\${port}\\`\n\t});\n\tconst args = process.argv.slice(2);\n\tconst child = child_process.spawn(args[0], args.slice(1), {\n\t\tstdio: \"inherit\",\n\t\tenv: env,\n\t});\n\tchild.on(\"exit\", (code, signal) => {\n\t\tprocess.exit(code ?? (signal ? 128 : 1));\n\t});\n});\nserver.on(\"error\", (err) => {\n\tconsole.error(\"Relay error:\", err);\n\tprocess.exit(1);\n});\n`.trim(),\n\t\t\t);\n\n\t\t\tconst primaryReadOnlyPaths = [\n\t\t\t\tprocess.execPath,\n\t\t\t\tlaunch.command,\n\t\t\t\tsupervisorStateDirectory,\n\t\t\t\t...(launch.readOnlyPaths ?? []),\n\t\t\t];\n\t\t\tconst readOnlyFileDescriptors = [\n\t\t\t\t...descriptorBackedReadOnlyPaths(primaryReadOnlyPaths),\n\t\t\t\t...(launch.readOnlyFiles ?? []),\n\t\t\t].map((path) => openSync(path, \"r\"));\n\t\t\t// The descriptors above are opened before the child spawns and must be closed\n\t\t\t// on every exit path -- including a spawn/wait rejection -- or a crashed launch\n\t\t\t// leaks an open handle onto a host-owned credential file for the process's life.\n\t\t\t// The workspace is bind-mounted read-write into the sandbox, so a file\n\t\t\t// here is visible at the same absolute path on both sides of it.\n\t\t\tconst terminalSizePath = join(supervisorStateDirectory, \"terminal-size\");\n\t\t\tconst stopPublishingTerminalSize = publishTerminalSize(terminalSizePath);\n\n\t\t\t// The second child derives its argv from the same builder as the first, with one\n\t\t\t// extra writable root. Nothing about the original child's namespace changes.\n\t\t\tescalationProxy = await createCommandEscalationProxy({\n\t\t\t\tsocketPath: escalationPaths.hostSocketPath,\n\t\t\t\trequestApproval: options?.requestCommandEscalation ?? createCommandEscalationApprover({ handoff }),\n\t\t\t\tviolationStore,\n\t\t\t\trunEscalated: (request) =>\n\t\t\t\t\trunEscalatedCommand({\n\t\t\t\t\t\trequest,\n\t\t\t\t\t\tlaunch,\n\t\t\t\t\t\tstateDirectory,\n\t\t\t\t\t\tspawnBwrap: options?.spawnChild ?? spawn,\n\t\t\t\t\t}),\n\t\t\t});\n\t\t\ttry {\n\t\t\t\tconst spawnBwrap = options?.spawnChild ?? spawn;\n\t\t\t\tconst child = spawnBwrap(\n\t\t\t\t\t\"bwrap\",\n\t\t\t\t\tbuildBwrapArguments({\n\t\t\t\t\t\tworkspace: launch.policy.workspace,\n\t\t\t\t\t\tadditionalWritableRoots: launch.policy.additionalWritableRoots,\n\t\t\t\t\t\treadOnlyPaths: primaryReadOnlyPaths,\n\t\t\t\t\t\treadOnlyFiles: launch.readOnlyFiles ?? [],\n\t\t\t\t\t\treadOnlyBinaries: launch.readOnlyBinaries ?? [],\n\t\t\t\t\t\tsockets: [\n\t\t\t\t\t\t\t{ hostPath: hostSocketPath, childPath: childSocketPath },\n\t\t\t\t\t\t\t// Its child-side path is what `APEX_CREDENTIAL_PROXY_PATH` names.\n\t\t\t\t\t\t\t...(launch.credentialChannel\n\t\t\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\thostPath: launch.credentialChannel.hostSocketPath,\n\t\t\t\t\t\t\t\t\t\t\tchildPath: launch.credentialChannel.childSocketPath,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t: []),\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\thostPath: gitCredentialPaths.hostSocketPath,\n\t\t\t\t\t\t\t\tchildPath: gitCredentialPaths.childSocketPath,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t\tenvironment: {\n\t\t\t\t\t\t\tHOME: launch.environment?.HOME ?? stateDirectory,\n\t\t\t\t\t\t\tTMPDIR: launch.environment?.TMPDIR ?? stateDirectory,\n\t\t\t\t\t\t\tAPEX_UDS_PATH: childSocketPath,\n\t\t\t\t\t\t\t[TERMINAL_SIZE_PATH_VARIABLE]: terminalSizePath,\n\t\t\t\t\t\t\t[TERMINAL_HANDOFF_PATH_VARIABLE]: supervisorStateDirectory,\n\t\t\t\t\t\t\t[TERMINAL_HANDOFF_ACK_PATH_VARIABLE]: acknowledgementPath,\n\t\t\t\t\t\t\t[GIT_CREDENTIAL_SOCKET_VARIABLE]: gitCredentialPaths.childSocketPath,\n\t\t\t\t\t\t\t[COMMAND_ESCALATION_SOCKET_VARIABLE]: escalationPaths.childSocketPath,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tcommand: process.execPath,\n\t\t\t\t\t\targs: [relayScriptPath, launch.command, ...launch.args],\n\t\t\t\t\t}),\n\t\t\t\t\t{ env: launch.environment, stdio: [\"inherit\", \"inherit\", \"pipe\", ...readOnlyFileDescriptors] },\n\t\t\t\t);\n\t\t\t\tlet stderr = \"\";\n\t\t\t\tchild.stderr?.setEncoding(\"utf8\");\n\t\t\t\tchild.stderr?.on(\"data\", (chunk: string) => {\n\t\t\t\t\tstderr += chunk;\n\t\t\t\t\tprocess.stderr.write(chunk);\n\t\t\t\t});\n\t\t\t\tconst exitCode = await waitForExit(child);\n\t\t\t\tawait proxy?.close();\n\t\t\t\tconst proxyAlreadyRecordedAViolation = (violationStore?.totalCount ?? 0) > violationCountBeforeLaunch;\n\t\t\t\tconst kind = exitCode !== 0 && !proxyAlreadyRecordedAViolation ? classifySandboxFailure(stderr) : undefined;\n\t\t\t\tif (kind) {\n\t\t\t\t\tviolationStore?.add({\n\t\t\t\t\t\tkind,\n\t\t\t\t\t\tcommand: [launch.command, ...launch.args].join(\" \"),\n\t\t\t\t\t\tdetail: stderr.trim(),\n\t\t\t\t\t\ttimestamp: new Date(),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn exitCode;\n\t\t\t} finally {\n\t\t\t\tstopPublishingTerminalSize();\n\t\t\t\tfor (const descriptor of readOnlyFileDescriptors) closeSync(descriptor);\n\t\t\t\t// The socket now lives outside the workspace, so nothing else will ever clean\n\t\t\t\t// it up; leaving it would litter the temp directory once per launch.\n\t\t\t\trmSync(hostSocketPath, { force: true });\n\t\t\t}\n\t\t},\n\t\tasync close() {\n\t\t\thandoff?.stop();\n\t\t\tawait escalationProxy?.close();\n\t\t\tif (escalationDirectory) rmSync(escalationDirectory, { force: true, recursive: true });\n\t\t\tawait gitCredentialProxy?.close();\n\t\t\tif (gitCredentialDirectory) rmSync(gitCredentialDirectory, { force: true, recursive: true });\n\t\t\tawait proxy?.close();\n\t\t},\n\t};\n}\n\n/**\n * Start one approved command in its own child, with one extra writable root.\n *\n * Deliberately minimal compared with the session child: no network relay, no credential\n * channels, no terminal handoff. An escalated command is a single shell invocation the\n * human just read and approved, not a session, and every channel omitted here is one it\n * cannot reach. Its stdio is captured rather than inherited so the session that asked\n * receives the output instead of the escalated process writing over the TUI.\n */\nasync function runEscalatedCommand(options: {\n\trequest: CommandEscalationRequest;\n\tlaunch: SandboxLaunch;\n\tstateDirectory: string;\n\tspawnBwrap: typeof spawn;\n}): Promise<CommandEscalationResult> {\n\tconst { request, launch, stateDirectory } = options;\n\tconst escalatedReadOnlyPaths = launch.readOnlyPaths ?? [];\n\tconst readOnlyFileDescriptors = [\n\t\t...descriptorBackedReadOnlyPaths(escalatedReadOnlyPaths),\n\t\t...(launch.readOnlyFiles ?? []),\n\t].map((path) => openSync(path, \"r\"));\n\tconst child = options.spawnBwrap(\n\t\t\"bwrap\",\n\t\tbuildBwrapArguments({\n\t\t\tworkspace: launch.policy.workspace,\n\t\t\tadditionalWritableRoots: [...launch.policy.additionalWritableRoots, request.writableRoot],\n\t\t\treadOnlyPaths: escalatedReadOnlyPaths,\n\t\t\treadOnlyFiles: launch.readOnlyFiles ?? [],\n\t\t\treadOnlyBinaries: launch.readOnlyBinaries ?? [],\n\t\t\tsockets: [],\n\t\t\tenvironment: {\n\t\t\t\tHOME: launch.environment?.HOME ?? stateDirectory,\n\t\t\t\tTMPDIR: launch.environment?.TMPDIR ?? stateDirectory,\n\t\t\t},\n\t\t\tcommand: \"/bin/sh\",\n\t\t\targs: [\"-c\", request.command],\n\t\t}),\n\t\t{ stdio: [\"ignore\", \"pipe\", \"pipe\", ...readOnlyFileDescriptors] },\n\t);\n\tlet stdout = \"\";\n\tlet stderr = \"\";\n\ttry {\n\t\tchild.stdout?.setEncoding(\"utf8\");\n\t\tchild.stdout?.on(\"data\", (chunk: string) => {\n\t\t\tstdout += chunk;\n\t\t});\n\t\tchild.stderr?.setEncoding(\"utf8\");\n\t\tchild.stderr?.on(\"data\", (chunk: string) => {\n\t\t\tstderr += chunk;\n\t\t});\n\t\tconst code = await new Promise<number>((resolveCode) => {\n\t\t\tchild.once(\"error\", () => resolveCode(1));\n\t\t\tchild.once(\"exit\", (exitCode) => resolveCode(exitCode ?? 1));\n\t\t});\n\t\treturn { code, stdout, stderr };\n\t} finally {\n\t\tfor (const descriptor of readOnlyFileDescriptors) closeSync(descriptor);\n\t}\n}\n"]}
|
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
-
import { randomBytes } from "node:crypto";
|
|
3
|
-
import { closeSync, mkdirSync, openSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import { buildBwrapArguments, descriptorBackedReadOnlyPaths } from "./bwrap-arguments.js";
|
|
7
|
-
import { createCommandEscalationApprover, createCredentialReleaser, createHostApprover } from "./host-approval.js";
|
|
8
|
-
import { createSandboxNetworkProxy } from "./network-proxy.js";
|
|
9
|
-
import { COMMAND_ESCALATION_SOCKET_VARIABLE, createCommandEscalationProxy, resolveCommandEscalationChannelPaths, } from "./rpc/command-proxy.js";
|
|
10
|
-
import { fillHostGitCredential, GIT_CREDENTIAL_SOCKET_VARIABLE, resolveGitCredentialChannelPaths, writeGitCredentialHelper, } from "./rpc/git-credential-helper.js";
|
|
11
|
-
import { createGitCredentialProxy } from "./rpc/git-credential-proxy.js";
|
|
12
|
-
import { createTerminalHandoff, TERMINAL_HANDOFF_ACK_PATH_VARIABLE, TERMINAL_HANDOFF_PATH_VARIABLE, } from "./terminal-handoff.js";
|
|
13
|
-
import { publishTerminalSize, TERMINAL_SIZE_PATH_VARIABLE } from "./terminal-size.js";
|
|
14
|
-
/** AF_UNIX `sun_path` is 108 bytes on Linux, including the terminating NUL. */
|
|
15
|
-
const SUN_PATH_LIMIT = 108;
|
|
16
|
-
function commandExists(command) {
|
|
17
|
-
const result = spawnSync(command, ["--version"], { stdio: "ignore", timeout: 1_000 });
|
|
18
|
-
return result.status === 0;
|
|
19
|
-
}
|
|
20
|
-
function waitForExit(child) {
|
|
21
|
-
return new Promise((resolve, reject) => {
|
|
22
|
-
child.once("error", reject);
|
|
23
|
-
child.once("exit", (code) => resolve(code ?? 1));
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Where the supervisor's proxy socket lives on each side of the boundary.
|
|
28
|
-
*
|
|
29
|
-
* AF_UNIX caps `sun_path` at 108 bytes, so the socket cannot live under the workspace:
|
|
30
|
-
* a deep-but-ordinary project directory exhausts that budget and the sandbox fails to
|
|
31
|
-
* start at all with `listen EINVAL`. The host side therefore gets a short, unique path
|
|
32
|
-
* under the system temp directory, and it is bind-mounted into the child.
|
|
33
|
-
*
|
|
34
|
-
* The child-side path has to sit under `/home`. That is the one writable mount at the
|
|
35
|
-
* point the mountpoint is created — the sandbox root is a read-only bind, so bwrap
|
|
36
|
-
* cannot create a mountpoint at `/run`, `/tmp`, or anywhere else on it.
|
|
37
|
-
*/
|
|
38
|
-
export function resolveProxySocketPaths(temporaryDirectory = tmpdir()) {
|
|
39
|
-
const name = `apex-${process.pid}-${randomBytes(4).toString("hex")}.sock`;
|
|
40
|
-
// An unusually long TMPDIR would reintroduce the very limit this avoids.
|
|
41
|
-
const base = join(temporaryDirectory, name).length + 1 > SUN_PATH_LIMIT ? "/tmp" : temporaryDirectory;
|
|
42
|
-
return { hostSocketPath: join(base, name), childSocketPath: `/home/${name}` };
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Classify an unsuccessful child from what its stderr actually evidences, or return
|
|
46
|
-
* undefined when nothing suggests the boundary refused anything.
|
|
47
|
-
*
|
|
48
|
-
* A non-zero exit is not a violation. An invalid API key, a failing test command, or a
|
|
49
|
-
* script exiting 1 are the child's own business, and recording them as sandbox
|
|
50
|
-
* violations blames the boundary for failures it had no part in — which is worse than
|
|
51
|
-
* silence, because it sends whoever reads the output looking for a refusal that never
|
|
52
|
-
* happened. Only Linux's own refusal wording earns a violation here; the proxy records
|
|
53
|
-
* network refusals itself, before this fallback is consulted.
|
|
54
|
-
*/
|
|
55
|
-
function classifySandboxFailure(stderr) {
|
|
56
|
-
if (/Read-only file system|Permission denied|Operation not permitted/i.test(stderr))
|
|
57
|
-
return "filesystem";
|
|
58
|
-
if (/Network is unreachable|ENETUNREACH|EHOSTUNREACH/i.test(stderr))
|
|
59
|
-
return "network";
|
|
60
|
-
return undefined;
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Linux's deliberately small platform adapter. The sandbox starts from a read-only
|
|
64
|
-
* host root and bind-mounts only the canonical workspace as writable. `--unshare-net`
|
|
65
|
-
* removes all direct network interfaces for the child and every descendant.
|
|
66
|
-
*/
|
|
67
|
-
export function createLinuxSandboxBackend(options) {
|
|
68
|
-
const platform = options?.platform ?? process.platform;
|
|
69
|
-
if (platform !== "linux") {
|
|
70
|
-
return {
|
|
71
|
-
status: { kind: "unavailable", reason: "OS sandbox is supported on Linux only." },
|
|
72
|
-
async launch() {
|
|
73
|
-
throw new Error("Linux sandbox backend is unavailable.");
|
|
74
|
-
},
|
|
75
|
-
async close() { },
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
const hasCommand = options?.commandExists ?? commandExists;
|
|
79
|
-
const violationStore = options?.violationStore;
|
|
80
|
-
let proxy;
|
|
81
|
-
let handoff;
|
|
82
|
-
let gitCredentialProxy;
|
|
83
|
-
let gitCredentialDirectory;
|
|
84
|
-
let escalationProxy;
|
|
85
|
-
let escalationDirectory;
|
|
86
|
-
if (!hasCommand("bwrap")) {
|
|
87
|
-
return {
|
|
88
|
-
status: { kind: "unavailable", reason: "Bubblewrap (bwrap) is required for OS sandboxing." },
|
|
89
|
-
async launch() {
|
|
90
|
-
throw new Error("Linux sandbox backend is unavailable.");
|
|
91
|
-
},
|
|
92
|
-
async close() { },
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
return {
|
|
96
|
-
status: { kind: "enforced" },
|
|
97
|
-
async launch(launch) {
|
|
98
|
-
const supervisorStateDirectory = launch.supervisorStateDirectory;
|
|
99
|
-
mkdirSync(supervisorStateDirectory, { recursive: true, mode: 0o700 });
|
|
100
|
-
const stateDirectory = launch.policy.workspace;
|
|
101
|
-
const acknowledgementPath = join(launch.policy.workspace, ".apex-code", "sandbox-handoff-ack");
|
|
102
|
-
mkdirSync(join(launch.policy.workspace, ".apex-code"), { recursive: true });
|
|
103
|
-
const violationCountBeforeLaunch = violationStore?.totalCount ?? 0;
|
|
104
|
-
const { hostSocketPath, childSocketPath } = resolveProxySocketPaths();
|
|
105
|
-
handoff = createTerminalHandoff(supervisorStateDirectory, { acknowledgementPath });
|
|
106
|
-
const gitCredentialPaths = resolveGitCredentialChannelPaths();
|
|
107
|
-
gitCredentialDirectory = gitCredentialPaths.hostSocketDirectory;
|
|
108
|
-
proxy = await createSandboxNetworkProxy({
|
|
109
|
-
socketPath: hostSocketPath,
|
|
110
|
-
allowedHosts: launch.policy.allowedHosts,
|
|
111
|
-
violationStore,
|
|
112
|
-
// Undefined without a terminal, which leaves the proxy's own
|
|
113
|
-
// deny-without-asking path in place for headless, print, JSON, and RPC.
|
|
114
|
-
requestApproval: createHostApprover({ handoff }),
|
|
115
|
-
});
|
|
116
|
-
// After the network proxy, because reachability is its answer to give: the
|
|
117
|
-
// credential channel must never hand out a token for a host this session
|
|
118
|
-
// cannot even open a connection to.
|
|
119
|
-
gitCredentialProxy = await createGitCredentialProxy({
|
|
120
|
-
socketPath: gitCredentialPaths.hostSocketPath,
|
|
121
|
-
isHostAllowed: (host) => proxy?.isHostReachable(host) ?? false,
|
|
122
|
-
requestRelease: options?.requestGitCredentialRelease ?? createCredentialReleaser({ handoff }),
|
|
123
|
-
// The supervisor's own environment, deliberately: this runs outside the
|
|
124
|
-
// boundary and must resolve against the real host home, which is exactly
|
|
125
|
-
// what the child cannot see.
|
|
126
|
-
fillCredential: (request) => (options?.fillGitCredential ?? fillHostGitCredential)(request, { environment: process.env }),
|
|
127
|
-
violationStore,
|
|
128
|
-
});
|
|
129
|
-
writeGitCredentialHelper(supervisorStateDirectory);
|
|
130
|
-
const escalationPaths = resolveCommandEscalationChannelPaths();
|
|
131
|
-
escalationDirectory = escalationPaths.hostSocketDirectory;
|
|
132
|
-
// .cjs forces CommonJS regardless of the target workspace's package.json "type"
|
|
133
|
-
// field -- a plain .js here would be parsed as ESM under "type": "module" and
|
|
134
|
-
// crash on `require`, since Node resolves module type from the nearest package.json.
|
|
135
|
-
const relayScriptPath = join(supervisorStateDirectory, "relay.cjs");
|
|
136
|
-
writeFileSync(relayScriptPath, `
|
|
137
|
-
const net = require("node:net");
|
|
138
|
-
const child_process = require("node:child_process");
|
|
139
|
-
|
|
140
|
-
const socketPath = process.env.APEX_UDS_PATH;
|
|
141
|
-
const server = net.createServer((c) => {
|
|
142
|
-
const client = net.connect(socketPath);
|
|
143
|
-
c.pipe(client).pipe(c);
|
|
144
|
-
c.on("error", () => {});
|
|
145
|
-
client.on("error", () => {});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
server.listen(0, "127.0.0.1", () => {
|
|
149
|
-
const port = server.address().port;
|
|
150
|
-
const env = Object.assign({}, process.env, {
|
|
151
|
-
HTTP_PROXY: \`http://127.0.0.1:\${port}\`,
|
|
152
|
-
HTTPS_PROXY: \`http://127.0.0.1:\${port}\`
|
|
153
|
-
});
|
|
154
|
-
const args = process.argv.slice(2);
|
|
155
|
-
const child = child_process.spawn(args[0], args.slice(1), {
|
|
156
|
-
stdio: "inherit",
|
|
157
|
-
env: env,
|
|
158
|
-
});
|
|
159
|
-
child.on("exit", (code, signal) => {
|
|
160
|
-
process.exit(code ?? (signal ? 128 : 1));
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
server.on("error", (err) => {
|
|
164
|
-
console.error("Relay error:", err);
|
|
165
|
-
process.exit(1);
|
|
166
|
-
});
|
|
167
|
-
`.trim());
|
|
168
|
-
const primaryReadOnlyPaths = [
|
|
169
|
-
process.execPath,
|
|
170
|
-
launch.command,
|
|
171
|
-
supervisorStateDirectory,
|
|
172
|
-
...(launch.readOnlyPaths ?? []),
|
|
173
|
-
];
|
|
174
|
-
const readOnlyFileDescriptors = [
|
|
175
|
-
...descriptorBackedReadOnlyPaths(primaryReadOnlyPaths),
|
|
176
|
-
...(launch.readOnlyFiles ?? []),
|
|
177
|
-
].map((path) => openSync(path, "r"));
|
|
178
|
-
// The descriptors above are opened before the child spawns and must be closed
|
|
179
|
-
// on every exit path -- including a spawn/wait rejection -- or a crashed launch
|
|
180
|
-
// leaks an open handle onto a host-owned credential file for the process's life.
|
|
181
|
-
// The workspace is bind-mounted read-write into the sandbox, so a file
|
|
182
|
-
// here is visible at the same absolute path on both sides of it.
|
|
183
|
-
const terminalSizePath = join(supervisorStateDirectory, "terminal-size");
|
|
184
|
-
const stopPublishingTerminalSize = publishTerminalSize(terminalSizePath);
|
|
185
|
-
// The second child derives its argv from the same builder as the first, with one
|
|
186
|
-
// extra writable root. Nothing about the original child's namespace changes.
|
|
187
|
-
escalationProxy = await createCommandEscalationProxy({
|
|
188
|
-
socketPath: escalationPaths.hostSocketPath,
|
|
189
|
-
requestApproval: options?.requestCommandEscalation ?? createCommandEscalationApprover({ handoff }),
|
|
190
|
-
violationStore,
|
|
191
|
-
runEscalated: (request) => runEscalatedCommand({
|
|
192
|
-
request,
|
|
193
|
-
launch,
|
|
194
|
-
stateDirectory,
|
|
195
|
-
spawnBwrap: options?.spawnChild ?? spawn,
|
|
196
|
-
}),
|
|
197
|
-
});
|
|
198
|
-
try {
|
|
199
|
-
const spawnBwrap = options?.spawnChild ?? spawn;
|
|
200
|
-
const child = spawnBwrap("bwrap", buildBwrapArguments({
|
|
201
|
-
workspace: launch.policy.workspace,
|
|
202
|
-
additionalWritableRoots: launch.policy.additionalWritableRoots,
|
|
203
|
-
readOnlyPaths: primaryReadOnlyPaths,
|
|
204
|
-
readOnlyFiles: launch.readOnlyFiles ?? [],
|
|
205
|
-
readOnlyBinaries: launch.readOnlyBinaries ?? [],
|
|
206
|
-
sockets: [
|
|
207
|
-
{ hostPath: hostSocketPath, childPath: childSocketPath },
|
|
208
|
-
// Its child-side path is what `APEX_CREDENTIAL_PROXY_PATH` names.
|
|
209
|
-
...(launch.credentialChannel
|
|
210
|
-
? [
|
|
211
|
-
{
|
|
212
|
-
hostPath: launch.credentialChannel.hostSocketPath,
|
|
213
|
-
childPath: launch.credentialChannel.childSocketPath,
|
|
214
|
-
},
|
|
215
|
-
]
|
|
216
|
-
: []),
|
|
217
|
-
{
|
|
218
|
-
hostPath: gitCredentialPaths.hostSocketPath,
|
|
219
|
-
childPath: gitCredentialPaths.childSocketPath,
|
|
220
|
-
},
|
|
221
|
-
],
|
|
222
|
-
environment: {
|
|
223
|
-
HOME: launch.environment?.HOME ?? stateDirectory,
|
|
224
|
-
TMPDIR: launch.environment?.TMPDIR ?? stateDirectory,
|
|
225
|
-
APEX_UDS_PATH: childSocketPath,
|
|
226
|
-
[TERMINAL_SIZE_PATH_VARIABLE]: terminalSizePath,
|
|
227
|
-
[TERMINAL_HANDOFF_PATH_VARIABLE]: supervisorStateDirectory,
|
|
228
|
-
[TERMINAL_HANDOFF_ACK_PATH_VARIABLE]: acknowledgementPath,
|
|
229
|
-
[GIT_CREDENTIAL_SOCKET_VARIABLE]: gitCredentialPaths.childSocketPath,
|
|
230
|
-
[COMMAND_ESCALATION_SOCKET_VARIABLE]: escalationPaths.childSocketPath,
|
|
231
|
-
},
|
|
232
|
-
command: process.execPath,
|
|
233
|
-
args: [relayScriptPath, launch.command, ...launch.args],
|
|
234
|
-
}), { env: launch.environment, stdio: ["inherit", "inherit", "pipe", ...readOnlyFileDescriptors] });
|
|
235
|
-
let stderr = "";
|
|
236
|
-
child.stderr?.setEncoding("utf8");
|
|
237
|
-
child.stderr?.on("data", (chunk) => {
|
|
238
|
-
stderr += chunk;
|
|
239
|
-
process.stderr.write(chunk);
|
|
240
|
-
});
|
|
241
|
-
const exitCode = await waitForExit(child);
|
|
242
|
-
await proxy?.close();
|
|
243
|
-
const proxyAlreadyRecordedAViolation = (violationStore?.totalCount ?? 0) > violationCountBeforeLaunch;
|
|
244
|
-
const kind = exitCode !== 0 && !proxyAlreadyRecordedAViolation ? classifySandboxFailure(stderr) : undefined;
|
|
245
|
-
if (kind) {
|
|
246
|
-
violationStore?.add({
|
|
247
|
-
kind,
|
|
248
|
-
command: [launch.command, ...launch.args].join(" "),
|
|
249
|
-
detail: stderr.trim(),
|
|
250
|
-
timestamp: new Date(),
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
return exitCode;
|
|
254
|
-
}
|
|
255
|
-
finally {
|
|
256
|
-
stopPublishingTerminalSize();
|
|
257
|
-
for (const descriptor of readOnlyFileDescriptors)
|
|
258
|
-
closeSync(descriptor);
|
|
259
|
-
// The socket now lives outside the workspace, so nothing else will ever clean
|
|
260
|
-
// it up; leaving it would litter the temp directory once per launch.
|
|
261
|
-
rmSync(hostSocketPath, { force: true });
|
|
262
|
-
}
|
|
263
|
-
},
|
|
264
|
-
async close() {
|
|
265
|
-
handoff?.stop();
|
|
266
|
-
await escalationProxy?.close();
|
|
267
|
-
if (escalationDirectory)
|
|
268
|
-
rmSync(escalationDirectory, { force: true, recursive: true });
|
|
269
|
-
await gitCredentialProxy?.close();
|
|
270
|
-
if (gitCredentialDirectory)
|
|
271
|
-
rmSync(gitCredentialDirectory, { force: true, recursive: true });
|
|
272
|
-
await proxy?.close();
|
|
273
|
-
},
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Start one approved command in its own child, with one extra writable root.
|
|
278
|
-
*
|
|
279
|
-
* Deliberately minimal compared with the session child: no network relay, no credential
|
|
280
|
-
* channels, no terminal handoff. An escalated command is a single shell invocation the
|
|
281
|
-
* human just read and approved, not a session, and every channel omitted here is one it
|
|
282
|
-
* cannot reach. Its stdio is captured rather than inherited so the session that asked
|
|
283
|
-
* receives the output instead of the escalated process writing over the TUI.
|
|
284
|
-
*/
|
|
285
|
-
async function runEscalatedCommand(options) {
|
|
286
|
-
const { request, launch, stateDirectory } = options;
|
|
287
|
-
const escalatedReadOnlyPaths = launch.readOnlyPaths ?? [];
|
|
288
|
-
const readOnlyFileDescriptors = [
|
|
289
|
-
...descriptorBackedReadOnlyPaths(escalatedReadOnlyPaths),
|
|
290
|
-
...(launch.readOnlyFiles ?? []),
|
|
291
|
-
].map((path) => openSync(path, "r"));
|
|
292
|
-
const child = options.spawnBwrap("bwrap", buildBwrapArguments({
|
|
293
|
-
workspace: launch.policy.workspace,
|
|
294
|
-
additionalWritableRoots: [...launch.policy.additionalWritableRoots, request.writableRoot],
|
|
295
|
-
readOnlyPaths: escalatedReadOnlyPaths,
|
|
296
|
-
readOnlyFiles: launch.readOnlyFiles ?? [],
|
|
297
|
-
readOnlyBinaries: launch.readOnlyBinaries ?? [],
|
|
298
|
-
sockets: [],
|
|
299
|
-
environment: {
|
|
300
|
-
HOME: launch.environment?.HOME ?? stateDirectory,
|
|
301
|
-
TMPDIR: launch.environment?.TMPDIR ?? stateDirectory,
|
|
302
|
-
},
|
|
303
|
-
command: "/bin/sh",
|
|
304
|
-
args: ["-c", request.command],
|
|
305
|
-
}), { stdio: ["ignore", "pipe", "pipe", ...readOnlyFileDescriptors] });
|
|
306
|
-
let stdout = "";
|
|
307
|
-
let stderr = "";
|
|
308
|
-
try {
|
|
309
|
-
child.stdout?.setEncoding("utf8");
|
|
310
|
-
child.stdout?.on("data", (chunk) => {
|
|
311
|
-
stdout += chunk;
|
|
312
|
-
});
|
|
313
|
-
child.stderr?.setEncoding("utf8");
|
|
314
|
-
child.stderr?.on("data", (chunk) => {
|
|
315
|
-
stderr += chunk;
|
|
316
|
-
});
|
|
317
|
-
const code = await new Promise((resolveCode) => {
|
|
318
|
-
child.once("error", () => resolveCode(1));
|
|
319
|
-
child.once("exit", (exitCode) => resolveCode(exitCode ?? 1));
|
|
320
|
-
});
|
|
321
|
-
return { code, stdout, stderr };
|
|
322
|
-
}
|
|
323
|
-
finally {
|
|
324
|
-
for (const descriptor of readOnlyFileDescriptors)
|
|
325
|
-
closeSync(descriptor);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
//# sourceMappingURL=linux-backend.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"linux-backend.js","sourceRoot":"","sources":["../../../src/core/sandbox/linux-backend.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAChF,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,mBAAmB,EAAE,6BAA6B,EAAE,MAAM,sBAAsB,CAAC;AAC1F,OAAO,EAAE,+BAA+B,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACnH,OAAO,EAAE,yBAAyB,EAA4B,MAAM,oBAAoB,CAAC;AACzF,OAAO,EACN,kCAAkC,EAGlC,4BAA4B,EAC5B,oCAAoC,GACpC,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACN,qBAAqB,EACrB,8BAA8B,EAC9B,gCAAgC,EAChC,wBAAwB,GACxB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,wBAAwB,EAA2B,MAAM,+BAA+B,CAAC;AAElG,OAAO,EACN,qBAAqB,EACrB,kCAAkC,EAClC,8BAA8B,GAE9B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,mBAAmB,EAAE,2BAA2B,EAAE,MAAM,oBAAoB,CAAC;AAGtF,+EAA+E;AAC/E,MAAM,cAAc,GAAG,GAAG,CAAC;AAuB3B,SAAS,aAAa,CAAC,OAAe,EAAW;IAChD,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,CAAC,WAAW,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;IACtF,OAAO,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC;AAAA,CAC3B;AAED,SAAS,WAAW,CAAC,KAA+B,EAAmB;IACtE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;QACvC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC5B,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;IAAA,CACjD,CAAC,CAAC;AAAA,CACH;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,uBAAuB,CAAC,kBAAkB,GAAW,MAAM,EAAE,EAG3E;IACD,MAAM,IAAI,GAAG,QAAQ,OAAO,CAAC,GAAG,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC;IAC1E,yEAAyE;IACzE,MAAM,IAAI,GAAG,IAAI,CAAC,kBAAkB,EAAE,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,kBAAkB,CAAC;IACtG,OAAO,EAAE,cAAc,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,eAAe,EAAE,SAAS,IAAI,EAAE,EAAE,CAAC;AAAA,CAC9E;AAED;;;;;;;;;;GAUG;AACH,SAAS,sBAAsB,CAAC,MAAc,EAAwC;IACrF,IAAI,kEAAkE,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,YAAY,CAAC;IACzG,IAAI,kDAAkD,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,SAAS,CAAC;IACtF,OAAO,SAAS,CAAC;AAAA,CACjB;AAED;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAAoC,EAAkB;IAC/F,MAAM,QAAQ,GAAG,OAAO,EAAE,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC;IACvD,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QAC1B,OAAO;YACN,MAAM,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,wCAAwC,EAAE;YACjF,KAAK,CAAC,MAAM,GAAG;gBACd,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;YAAA,CACzD;YACD,KAAK,CAAC,KAAK,GAAG,EAAC,CAAC;SAChB,CAAC;IACH,CAAC;IACD,MAAM,UAAU,GAAG,OAAO,EAAE,aAAa,IAAI,aAAa,CAAC;IAC3D,MAAM,cAAc,GAAG,OAAO,EAAE,cAAc,CAAC;IAC/C,IAAI,KAAsC,CAAC;IAC3C,IAAI,OAAoC,CAAC;IACzC,IAAI,kBAAkD,CAAC;IACvD,IAAI,sBAA0C,CAAC;IAC/C,IAAI,eAAqF,CAAC;IAC1F,IAAI,mBAAuC,CAAC;IAC5C,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAC1B,OAAO;YACN,MAAM,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,mDAAmD,EAAE;YAC5F,KAAK,CAAC,MAAM,GAAG;gBACd,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;YAAA,CACzD;YACD,KAAK,CAAC,KAAK,GAAG,EAAC,CAAC;SAChB,CAAC;IACH,CAAC;IACD,OAAO;QACN,MAAM,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE;QAC5B,KAAK,CAAC,MAAM,CAAC,MAAqB,EAAmB;YACpD,MAAM,wBAAwB,GAAG,MAAM,CAAC,wBAAwB,CAAC;YACjE,SAAS,CAAC,wBAAwB,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YACtE,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC;YAC/C,MAAM,mBAAmB,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,YAAY,EAAE,qBAAqB,CAAC,CAAC;YAC/F,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAE5E,MAAM,0BAA0B,GAAG,cAAc,EAAE,UAAU,IAAI,CAAC,CAAC;YAEnE,MAAM,EAAE,cAAc,EAAE,eAAe,EAAE,GAAG,uBAAuB,EAAE,CAAC;YACtE,OAAO,GAAG,qBAAqB,CAAC,wBAAwB,EAAE,EAAE,mBAAmB,EAAE,CAAC,CAAC;YACnF,MAAM,kBAAkB,GAAG,gCAAgC,EAAE,CAAC;YAC9D,sBAAsB,GAAG,kBAAkB,CAAC,mBAAmB,CAAC;YAChE,KAAK,GAAG,MAAM,yBAAyB,CAAC;gBACvC,UAAU,EAAE,cAAc;gBAC1B,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,YAAY;gBACxC,cAAc;gBACd,6DAA6D;gBAC7D,wEAAwE;gBACxE,eAAe,EAAE,kBAAkB,CAAC,EAAE,OAAO,EAAE,CAAC;aAChD,CAAC,CAAC;YACH,2EAA2E;YAC3E,yEAAyE;YACzE,oCAAoC;YACpC,kBAAkB,GAAG,MAAM,wBAAwB,CAAC;gBACnD,UAAU,EAAE,kBAAkB,CAAC,cAAc;gBAC7C,aAAa,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,EAAE,eAAe,CAAC,IAAI,CAAC,IAAI,KAAK;gBAC9D,cAAc,EAAE,OAAO,EAAE,2BAA2B,IAAI,wBAAwB,CAAC,EAAE,OAAO,EAAE,CAAC;gBAC7F,wEAAwE;gBACxE,yEAAyE;gBACzE,6BAA6B;gBAC7B,cAAc,EAAE,CAAC,OAAO,EAAE,EAAE,CAC3B,CAAC,OAAO,EAAE,iBAAiB,IAAI,qBAAqB,CAAC,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC;gBAC7F,cAAc;aACd,CAAC,CAAC;YACH,wBAAwB,CAAC,wBAAwB,CAAC,CAAC;YAEnD,MAAM,eAAe,GAAG,oCAAoC,EAAE,CAAC;YAC/D,mBAAmB,GAAG,eAAe,CAAC,mBAAmB,CAAC;YAE1D,gFAAgF;YAChF,8EAA8E;YAC9E,qFAAqF;YACrF,MAAM,eAAe,GAAG,IAAI,CAAC,wBAAwB,EAAE,WAAW,CAAC,CAAC;YACpE,aAAa,CACZ,eAAe,EACf;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+BH,CAAC,IAAI,EAAE,CACJ,CAAC;YAEF,MAAM,oBAAoB,GAAG;gBAC5B,OAAO,CAAC,QAAQ;gBAChB,MAAM,CAAC,OAAO;gBACd,wBAAwB;gBACxB,GAAG,CAAC,MAAM,CAAC,aAAa,IAAI,EAAE,CAAC;aAC/B,CAAC;YACF,MAAM,uBAAuB,GAAG;gBAC/B,GAAG,6BAA6B,CAAC,oBAAoB,CAAC;gBACtD,GAAG,CAAC,MAAM,CAAC,aAAa,IAAI,EAAE,CAAC;aAC/B,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;YACrC,8EAA8E;YAC9E,gFAAgF;YAChF,iFAAiF;YACjF,uEAAuE;YACvE,iEAAiE;YACjE,MAAM,gBAAgB,GAAG,IAAI,CAAC,wBAAwB,EAAE,eAAe,CAAC,CAAC;YACzE,MAAM,0BAA0B,GAAG,mBAAmB,CAAC,gBAAgB,CAAC,CAAC;YAEzE,iFAAiF;YACjF,6EAA6E;YAC7E,eAAe,GAAG,MAAM,4BAA4B,CAAC;gBACpD,UAAU,EAAE,eAAe,CAAC,cAAc;gBAC1C,eAAe,EAAE,OAAO,EAAE,wBAAwB,IAAI,+BAA+B,CAAC,EAAE,OAAO,EAAE,CAAC;gBAClG,cAAc;gBACd,YAAY,EAAE,CAAC,OAAO,EAAE,EAAE,CACzB,mBAAmB,CAAC;oBACnB,OAAO;oBACP,MAAM;oBACN,cAAc;oBACd,UAAU,EAAE,OAAO,EAAE,UAAU,IAAI,KAAK;iBACxC,CAAC;aACH,CAAC,CAAC;YACH,IAAI,CAAC;gBACJ,MAAM,UAAU,GAAG,OAAO,EAAE,UAAU,IAAI,KAAK,CAAC;gBAChD,MAAM,KAAK,GAAG,UAAU,CACvB,OAAO,EACP,mBAAmB,CAAC;oBACnB,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,SAAS;oBAClC,uBAAuB,EAAE,MAAM,CAAC,MAAM,CAAC,uBAAuB;oBAC9D,aAAa,EAAE,oBAAoB;oBACnC,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,EAAE;oBACzC,gBAAgB,EAAE,MAAM,CAAC,gBAAgB,IAAI,EAAE;oBAC/C,OAAO,EAAE;wBACR,EAAE,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,eAAe,EAAE;wBACxD,kEAAkE;wBAClE,GAAG,CAAC,MAAM,CAAC,iBAAiB;4BAC3B,CAAC,CAAC;gCACA;oCACC,QAAQ,EAAE,MAAM,CAAC,iBAAiB,CAAC,cAAc;oCACjD,SAAS,EAAE,MAAM,CAAC,iBAAiB,CAAC,eAAe;iCACnD;6BACD;4BACF,CAAC,CAAC,EAAE,CAAC;wBACN;4BACC,QAAQ,EAAE,kBAAkB,CAAC,cAAc;4BAC3C,SAAS,EAAE,kBAAkB,CAAC,eAAe;yBAC7C;qBACD;oBACD,WAAW,EAAE;wBACZ,IAAI,EAAE,MAAM,CAAC,WAAW,EAAE,IAAI,IAAI,cAAc;wBAChD,MAAM,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,IAAI,cAAc;wBACpD,aAAa,EAAE,eAAe;wBAC9B,CAAC,2BAA2B,CAAC,EAAE,gBAAgB;wBAC/C,CAAC,8BAA8B,CAAC,EAAE,wBAAwB;wBAC1D,CAAC,kCAAkC,CAAC,EAAE,mBAAmB;wBACzD,CAAC,8BAA8B,CAAC,EAAE,kBAAkB,CAAC,eAAe;wBACpE,CAAC,kCAAkC,CAAC,EAAE,eAAe,CAAC,eAAe;qBACrE;oBACD,OAAO,EAAE,OAAO,CAAC,QAAQ;oBACzB,IAAI,EAAE,CAAC,eAAe,EAAE,MAAM,CAAC,OAAO,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC;iBACvD,CAAC,EACF,EAAE,GAAG,EAAE,MAAM,CAAC,WAAW,EAAE,KAAK,EAAE,CAAC,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,uBAAuB,CAAC,EAAE,CAC9F,CAAC;gBACF,IAAI,MAAM,GAAG,EAAE,CAAC;gBAChB,KAAK,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;gBAClC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC;oBAC3C,MAAM,IAAI,KAAK,CAAC;oBAChB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;gBAAA,CAC5B,CAAC,CAAC;gBACH,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,KAAK,CAAC,CAAC;gBAC1C,MAAM,KAAK,EAAE,KAAK,EAAE,CAAC;gBACrB,MAAM,8BAA8B,GAAG,CAAC,cAAc,EAAE,UAAU,IAAI,CAAC,CAAC,GAAG,0BAA0B,CAAC;gBACtG,MAAM,IAAI,GAAG,QAAQ,KAAK,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC,CAAC,sBAAsB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;gBAC5G,IAAI,IAAI,EAAE,CAAC;oBACV,cAAc,EAAE,GAAG,CAAC;wBACnB,IAAI;wBACJ,OAAO,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;wBACnD,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE;wBACrB,SAAS,EAAE,IAAI,IAAI,EAAE;qBACrB,CAAC,CAAC;gBACJ,CAAC;gBACD,OAAO,QAAQ,CAAC;YACjB,CAAC;oBAAS,CAAC;gBACV,0BAA0B,EAAE,CAAC;gBAC7B,KAAK,MAAM,UAAU,IAAI,uBAAuB;oBAAE,SAAS,CAAC,UAAU,CAAC,CAAC;gBACxE,8EAA8E;gBAC9E,qEAAqE;gBACrE,MAAM,CAAC,cAAc,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACzC,CAAC;QAAA,CACD;QACD,KAAK,CAAC,KAAK,GAAG;YACb,OAAO,EAAE,IAAI,EAAE,CAAC;YAChB,MAAM,eAAe,EAAE,KAAK,EAAE,CAAC;YAC/B,IAAI,mBAAmB;gBAAE,MAAM,CAAC,mBAAmB,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACvF,MAAM,kBAAkB,EAAE,KAAK,EAAE,CAAC;YAClC,IAAI,sBAAsB;gBAAE,MAAM,CAAC,sBAAsB,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC7F,MAAM,KAAK,EAAE,KAAK,EAAE,CAAC;QAAA,CACrB;KACD,CAAC;AAAA,CACF;AAED;;;;;;;;GAQG;AACH,KAAK,UAAU,mBAAmB,CAAC,OAKlC,EAAoC;IACpC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC;IACpD,MAAM,sBAAsB,GAAG,MAAM,CAAC,aAAa,IAAI,EAAE,CAAC;IAC1D,MAAM,uBAAuB,GAAG;QAC/B,GAAG,6BAA6B,CAAC,sBAAsB,CAAC;QACxD,GAAG,CAAC,MAAM,CAAC,aAAa,IAAI,EAAE,CAAC;KAC/B,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;IACrC,MAAM,KAAK,GAAG,OAAO,CAAC,UAAU,CAC/B,OAAO,EACP,mBAAmB,CAAC;QACnB,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,SAAS;QAClC,uBAAuB,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,uBAAuB,EAAE,OAAO,CAAC,YAAY,CAAC;QACzF,aAAa,EAAE,sBAAsB;QACrC,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,EAAE;QACzC,gBAAgB,EAAE,MAAM,CAAC,gBAAgB,IAAI,EAAE;QAC/C,OAAO,EAAE,EAAE;QACX,WAAW,EAAE;YACZ,IAAI,EAAE,MAAM,CAAC,WAAW,EAAE,IAAI,IAAI,cAAc;YAChD,MAAM,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,IAAI,cAAc;SACpD;QACD,OAAO,EAAE,SAAS;QAClB,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC;KAC7B,CAAC,EACF,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,uBAAuB,CAAC,EAAE,CACjE,CAAC;IACF,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,IAAI,CAAC;QACJ,KAAK,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;QAClC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC;YAC3C,MAAM,IAAI,KAAK,CAAC;QAAA,CAChB,CAAC,CAAC;QACH,KAAK,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;QAClC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC;YAC3C,MAAM,IAAI,KAAK,CAAC;QAAA,CAChB,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,IAAI,OAAO,CAAS,CAAC,WAAW,EAAE,EAAE,CAAC;YACvD,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;YAC1C,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,WAAW,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,CAAC;QAAA,CAC7D,CAAC,CAAC;QACH,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjC,CAAC;YAAS,CAAC;QACV,KAAK,MAAM,UAAU,IAAI,uBAAuB;YAAE,SAAS,CAAC,UAAU,CAAC,CAAC;IACzE,CAAC;AAAA,CACD","sourcesContent":["import { spawn, spawnSync } from \"node:child_process\";\nimport { randomBytes } from \"node:crypto\";\nimport { closeSync, mkdirSync, openSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { buildBwrapArguments, descriptorBackedReadOnlyPaths } from \"./bwrap-arguments.ts\";\nimport { createCommandEscalationApprover, createCredentialReleaser, createHostApprover } from \"./host-approval.ts\";\nimport { createSandboxNetworkProxy, type SandboxNetworkProxy } from \"./network-proxy.ts\";\nimport {\n\tCOMMAND_ESCALATION_SOCKET_VARIABLE,\n\ttype CommandEscalationRequest,\n\ttype CommandEscalationResult,\n\tcreateCommandEscalationProxy,\n\tresolveCommandEscalationChannelPaths,\n} from \"./rpc/command-proxy.ts\";\nimport {\n\tfillHostGitCredential,\n\tGIT_CREDENTIAL_SOCKET_VARIABLE,\n\tresolveGitCredentialChannelPaths,\n\twriteGitCredentialHelper,\n} from \"./rpc/git-credential-helper.ts\";\nimport { createGitCredentialProxy, type GitCredentialProxy } from \"./rpc/git-credential-proxy.ts\";\nimport type { SandboxBackend, SandboxLaunch } from \"./supervisor.ts\";\nimport {\n\tcreateTerminalHandoff,\n\tTERMINAL_HANDOFF_ACK_PATH_VARIABLE,\n\tTERMINAL_HANDOFF_PATH_VARIABLE,\n\ttype TerminalHandoff,\n} from \"./terminal-handoff.ts\";\nimport { publishTerminalSize, TERMINAL_SIZE_PATH_VARIABLE } from \"./terminal-size.ts\";\nimport type { SandboxViolationStore } from \"./violations.ts\";\n\n/** AF_UNIX `sun_path` is 108 bytes on Linux, including the terminating NUL. */\nconst SUN_PATH_LIMIT = 108;\n\nexport interface LinuxSandboxBackendOptions {\n\t/** Injectable only for preflight tests; production uses the running platform. */\n\tplatform?: NodeJS.Platform;\n\tcommandExists?: (command: string) => boolean;\n\tviolationStore?: SandboxViolationStore;\n\t/** Injectable only to prove crash cleanup in tests; production spawns `bwrap` directly. */\n\tspawnChild?: typeof spawn;\n\t/**\n\t * Injectable only for tests, which have no terminal and would otherwise be unable to\n\t * exercise a released credential at all. Production builds the releaser from the\n\t * terminal handoff, and its absence there is what makes a headless session refuse.\n\t */\n\trequestGitCredentialRelease?: (host: string) => Promise<boolean>;\n\t/** Injectable only for tests; production resolves against the real host store. */\n\tfillGitCredential?: typeof fillHostGitCredential;\n\t/** Injectable only for tests, which have no terminal to approve an escalation at. */\n\trequestCommandEscalation?: (request: CommandEscalationRequest) => Promise<boolean>;\n\t/** Retained for source compatibility; launch state is now required on each launch. */\n\tsupervisorStateDirectory?: string;\n}\n\nfunction commandExists(command: string): boolean {\n\tconst result = spawnSync(command, [\"--version\"], { stdio: \"ignore\", timeout: 1_000 });\n\treturn result.status === 0;\n}\n\nfunction waitForExit(child: ReturnType<typeof spawn>): Promise<number> {\n\treturn new Promise((resolve, reject) => {\n\t\tchild.once(\"error\", reject);\n\t\tchild.once(\"exit\", (code) => resolve(code ?? 1));\n\t});\n}\n\n/**\n * Where the supervisor's proxy socket lives on each side of the boundary.\n *\n * AF_UNIX caps `sun_path` at 108 bytes, so the socket cannot live under the workspace:\n * a deep-but-ordinary project directory exhausts that budget and the sandbox fails to\n * start at all with `listen EINVAL`. The host side therefore gets a short, unique path\n * under the system temp directory, and it is bind-mounted into the child.\n *\n * The child-side path has to sit under `/home`. That is the one writable mount at the\n * point the mountpoint is created — the sandbox root is a read-only bind, so bwrap\n * cannot create a mountpoint at `/run`, `/tmp`, or anywhere else on it.\n */\nexport function resolveProxySocketPaths(temporaryDirectory: string = tmpdir()): {\n\thostSocketPath: string;\n\tchildSocketPath: string;\n} {\n\tconst name = `apex-${process.pid}-${randomBytes(4).toString(\"hex\")}.sock`;\n\t// An unusually long TMPDIR would reintroduce the very limit this avoids.\n\tconst base = join(temporaryDirectory, name).length + 1 > SUN_PATH_LIMIT ? \"/tmp\" : temporaryDirectory;\n\treturn { hostSocketPath: join(base, name), childSocketPath: `/home/${name}` };\n}\n\n/**\n * Classify an unsuccessful child from what its stderr actually evidences, or return\n * undefined when nothing suggests the boundary refused anything.\n *\n * A non-zero exit is not a violation. An invalid API key, a failing test command, or a\n * script exiting 1 are the child's own business, and recording them as sandbox\n * violations blames the boundary for failures it had no part in — which is worse than\n * silence, because it sends whoever reads the output looking for a refusal that never\n * happened. Only Linux's own refusal wording earns a violation here; the proxy records\n * network refusals itself, before this fallback is consulted.\n */\nfunction classifySandboxFailure(stderr: string): \"filesystem\" | \"network\" | undefined {\n\tif (/Read-only file system|Permission denied|Operation not permitted/i.test(stderr)) return \"filesystem\";\n\tif (/Network is unreachable|ENETUNREACH|EHOSTUNREACH/i.test(stderr)) return \"network\";\n\treturn undefined;\n}\n\n/**\n * Linux's deliberately small platform adapter. The sandbox starts from a read-only\n * host root and bind-mounts only the canonical workspace as writable. `--unshare-net`\n * removes all direct network interfaces for the child and every descendant.\n */\nexport function createLinuxSandboxBackend(options?: LinuxSandboxBackendOptions): SandboxBackend {\n\tconst platform = options?.platform ?? process.platform;\n\tif (platform !== \"linux\") {\n\t\treturn {\n\t\t\tstatus: { kind: \"unavailable\", reason: \"OS sandbox is supported on Linux only.\" },\n\t\t\tasync launch() {\n\t\t\t\tthrow new Error(\"Linux sandbox backend is unavailable.\");\n\t\t\t},\n\t\t\tasync close() {},\n\t\t};\n\t}\n\tconst hasCommand = options?.commandExists ?? commandExists;\n\tconst violationStore = options?.violationStore;\n\tlet proxy: SandboxNetworkProxy | undefined;\n\tlet handoff: TerminalHandoff | undefined;\n\tlet gitCredentialProxy: GitCredentialProxy | undefined;\n\tlet gitCredentialDirectory: string | undefined;\n\tlet escalationProxy: Awaited<ReturnType<typeof createCommandEscalationProxy>> | undefined;\n\tlet escalationDirectory: string | undefined;\n\tif (!hasCommand(\"bwrap\")) {\n\t\treturn {\n\t\t\tstatus: { kind: \"unavailable\", reason: \"Bubblewrap (bwrap) is required for OS sandboxing.\" },\n\t\t\tasync launch() {\n\t\t\t\tthrow new Error(\"Linux sandbox backend is unavailable.\");\n\t\t\t},\n\t\t\tasync close() {},\n\t\t};\n\t}\n\treturn {\n\t\tstatus: { kind: \"enforced\" },\n\t\tasync launch(launch: SandboxLaunch): Promise<number> {\n\t\t\tconst supervisorStateDirectory = launch.supervisorStateDirectory;\n\t\t\tmkdirSync(supervisorStateDirectory, { recursive: true, mode: 0o700 });\n\t\t\tconst stateDirectory = launch.policy.workspace;\n\t\t\tconst acknowledgementPath = join(launch.policy.workspace, \".apex-code\", \"sandbox-handoff-ack\");\n\t\t\tmkdirSync(join(launch.policy.workspace, \".apex-code\"), { recursive: true });\n\n\t\t\tconst violationCountBeforeLaunch = violationStore?.totalCount ?? 0;\n\n\t\t\tconst { hostSocketPath, childSocketPath } = resolveProxySocketPaths();\n\t\t\thandoff = createTerminalHandoff(supervisorStateDirectory, { acknowledgementPath });\n\t\t\tconst gitCredentialPaths = resolveGitCredentialChannelPaths();\n\t\t\tgitCredentialDirectory = gitCredentialPaths.hostSocketDirectory;\n\t\t\tproxy = await createSandboxNetworkProxy({\n\t\t\t\tsocketPath: hostSocketPath,\n\t\t\t\tallowedHosts: launch.policy.allowedHosts,\n\t\t\t\tviolationStore,\n\t\t\t\t// Undefined without a terminal, which leaves the proxy's own\n\t\t\t\t// deny-without-asking path in place for headless, print, JSON, and RPC.\n\t\t\t\trequestApproval: createHostApprover({ handoff }),\n\t\t\t});\n\t\t\t// After the network proxy, because reachability is its answer to give: the\n\t\t\t// credential channel must never hand out a token for a host this session\n\t\t\t// cannot even open a connection to.\n\t\t\tgitCredentialProxy = await createGitCredentialProxy({\n\t\t\t\tsocketPath: gitCredentialPaths.hostSocketPath,\n\t\t\t\tisHostAllowed: (host) => proxy?.isHostReachable(host) ?? false,\n\t\t\t\trequestRelease: options?.requestGitCredentialRelease ?? createCredentialReleaser({ handoff }),\n\t\t\t\t// The supervisor's own environment, deliberately: this runs outside the\n\t\t\t\t// boundary and must resolve against the real host home, which is exactly\n\t\t\t\t// what the child cannot see.\n\t\t\t\tfillCredential: (request) =>\n\t\t\t\t\t(options?.fillGitCredential ?? fillHostGitCredential)(request, { environment: process.env }),\n\t\t\t\tviolationStore,\n\t\t\t});\n\t\t\twriteGitCredentialHelper(supervisorStateDirectory);\n\n\t\t\tconst escalationPaths = resolveCommandEscalationChannelPaths();\n\t\t\tescalationDirectory = escalationPaths.hostSocketDirectory;\n\n\t\t\t// .cjs forces CommonJS regardless of the target workspace's package.json \"type\"\n\t\t\t// field -- a plain .js here would be parsed as ESM under \"type\": \"module\" and\n\t\t\t// crash on `require`, since Node resolves module type from the nearest package.json.\n\t\t\tconst relayScriptPath = join(supervisorStateDirectory, \"relay.cjs\");\n\t\t\twriteFileSync(\n\t\t\t\trelayScriptPath,\n\t\t\t\t`\nconst net = require(\"node:net\");\nconst child_process = require(\"node:child_process\");\n\nconst socketPath = process.env.APEX_UDS_PATH;\nconst server = net.createServer((c) => {\n\tconst client = net.connect(socketPath);\n\tc.pipe(client).pipe(c);\n\tc.on(\"error\", () => {});\n\tclient.on(\"error\", () => {});\n});\n\nserver.listen(0, \"127.0.0.1\", () => {\n\tconst port = server.address().port;\n\tconst env = Object.assign({}, process.env, {\n\t\tHTTP_PROXY: \\`http://127.0.0.1:\\${port}\\`,\n\t\tHTTPS_PROXY: \\`http://127.0.0.1:\\${port}\\`\n\t});\n\tconst args = process.argv.slice(2);\n\tconst child = child_process.spawn(args[0], args.slice(1), {\n\t\tstdio: \"inherit\",\n\t\tenv: env,\n\t});\n\tchild.on(\"exit\", (code, signal) => {\n\t\tprocess.exit(code ?? (signal ? 128 : 1));\n\t});\n});\nserver.on(\"error\", (err) => {\n\tconsole.error(\"Relay error:\", err);\n\tprocess.exit(1);\n});\n`.trim(),\n\t\t\t);\n\n\t\t\tconst primaryReadOnlyPaths = [\n\t\t\t\tprocess.execPath,\n\t\t\t\tlaunch.command,\n\t\t\t\tsupervisorStateDirectory,\n\t\t\t\t...(launch.readOnlyPaths ?? []),\n\t\t\t];\n\t\t\tconst readOnlyFileDescriptors = [\n\t\t\t\t...descriptorBackedReadOnlyPaths(primaryReadOnlyPaths),\n\t\t\t\t...(launch.readOnlyFiles ?? []),\n\t\t\t].map((path) => openSync(path, \"r\"));\n\t\t\t// The descriptors above are opened before the child spawns and must be closed\n\t\t\t// on every exit path -- including a spawn/wait rejection -- or a crashed launch\n\t\t\t// leaks an open handle onto a host-owned credential file for the process's life.\n\t\t\t// The workspace is bind-mounted read-write into the sandbox, so a file\n\t\t\t// here is visible at the same absolute path on both sides of it.\n\t\t\tconst terminalSizePath = join(supervisorStateDirectory, \"terminal-size\");\n\t\t\tconst stopPublishingTerminalSize = publishTerminalSize(terminalSizePath);\n\n\t\t\t// The second child derives its argv from the same builder as the first, with one\n\t\t\t// extra writable root. Nothing about the original child's namespace changes.\n\t\t\tescalationProxy = await createCommandEscalationProxy({\n\t\t\t\tsocketPath: escalationPaths.hostSocketPath,\n\t\t\t\trequestApproval: options?.requestCommandEscalation ?? createCommandEscalationApprover({ handoff }),\n\t\t\t\tviolationStore,\n\t\t\t\trunEscalated: (request) =>\n\t\t\t\t\trunEscalatedCommand({\n\t\t\t\t\t\trequest,\n\t\t\t\t\t\tlaunch,\n\t\t\t\t\t\tstateDirectory,\n\t\t\t\t\t\tspawnBwrap: options?.spawnChild ?? spawn,\n\t\t\t\t\t}),\n\t\t\t});\n\t\t\ttry {\n\t\t\t\tconst spawnBwrap = options?.spawnChild ?? spawn;\n\t\t\t\tconst child = spawnBwrap(\n\t\t\t\t\t\"bwrap\",\n\t\t\t\t\tbuildBwrapArguments({\n\t\t\t\t\t\tworkspace: launch.policy.workspace,\n\t\t\t\t\t\tadditionalWritableRoots: launch.policy.additionalWritableRoots,\n\t\t\t\t\t\treadOnlyPaths: primaryReadOnlyPaths,\n\t\t\t\t\t\treadOnlyFiles: launch.readOnlyFiles ?? [],\n\t\t\t\t\t\treadOnlyBinaries: launch.readOnlyBinaries ?? [],\n\t\t\t\t\t\tsockets: [\n\t\t\t\t\t\t\t{ hostPath: hostSocketPath, childPath: childSocketPath },\n\t\t\t\t\t\t\t// Its child-side path is what `APEX_CREDENTIAL_PROXY_PATH` names.\n\t\t\t\t\t\t\t...(launch.credentialChannel\n\t\t\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\thostPath: launch.credentialChannel.hostSocketPath,\n\t\t\t\t\t\t\t\t\t\t\tchildPath: launch.credentialChannel.childSocketPath,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t: []),\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\thostPath: gitCredentialPaths.hostSocketPath,\n\t\t\t\t\t\t\t\tchildPath: gitCredentialPaths.childSocketPath,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t\tenvironment: {\n\t\t\t\t\t\t\tHOME: launch.environment?.HOME ?? stateDirectory,\n\t\t\t\t\t\t\tTMPDIR: launch.environment?.TMPDIR ?? stateDirectory,\n\t\t\t\t\t\t\tAPEX_UDS_PATH: childSocketPath,\n\t\t\t\t\t\t\t[TERMINAL_SIZE_PATH_VARIABLE]: terminalSizePath,\n\t\t\t\t\t\t\t[TERMINAL_HANDOFF_PATH_VARIABLE]: supervisorStateDirectory,\n\t\t\t\t\t\t\t[TERMINAL_HANDOFF_ACK_PATH_VARIABLE]: acknowledgementPath,\n\t\t\t\t\t\t\t[GIT_CREDENTIAL_SOCKET_VARIABLE]: gitCredentialPaths.childSocketPath,\n\t\t\t\t\t\t\t[COMMAND_ESCALATION_SOCKET_VARIABLE]: escalationPaths.childSocketPath,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tcommand: process.execPath,\n\t\t\t\t\t\targs: [relayScriptPath, launch.command, ...launch.args],\n\t\t\t\t\t}),\n\t\t\t\t\t{ env: launch.environment, stdio: [\"inherit\", \"inherit\", \"pipe\", ...readOnlyFileDescriptors] },\n\t\t\t\t);\n\t\t\t\tlet stderr = \"\";\n\t\t\t\tchild.stderr?.setEncoding(\"utf8\");\n\t\t\t\tchild.stderr?.on(\"data\", (chunk: string) => {\n\t\t\t\t\tstderr += chunk;\n\t\t\t\t\tprocess.stderr.write(chunk);\n\t\t\t\t});\n\t\t\t\tconst exitCode = await waitForExit(child);\n\t\t\t\tawait proxy?.close();\n\t\t\t\tconst proxyAlreadyRecordedAViolation = (violationStore?.totalCount ?? 0) > violationCountBeforeLaunch;\n\t\t\t\tconst kind = exitCode !== 0 && !proxyAlreadyRecordedAViolation ? classifySandboxFailure(stderr) : undefined;\n\t\t\t\tif (kind) {\n\t\t\t\t\tviolationStore?.add({\n\t\t\t\t\t\tkind,\n\t\t\t\t\t\tcommand: [launch.command, ...launch.args].join(\" \"),\n\t\t\t\t\t\tdetail: stderr.trim(),\n\t\t\t\t\t\ttimestamp: new Date(),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn exitCode;\n\t\t\t} finally {\n\t\t\t\tstopPublishingTerminalSize();\n\t\t\t\tfor (const descriptor of readOnlyFileDescriptors) closeSync(descriptor);\n\t\t\t\t// The socket now lives outside the workspace, so nothing else will ever clean\n\t\t\t\t// it up; leaving it would litter the temp directory once per launch.\n\t\t\t\trmSync(hostSocketPath, { force: true });\n\t\t\t}\n\t\t},\n\t\tasync close() {\n\t\t\thandoff?.stop();\n\t\t\tawait escalationProxy?.close();\n\t\t\tif (escalationDirectory) rmSync(escalationDirectory, { force: true, recursive: true });\n\t\t\tawait gitCredentialProxy?.close();\n\t\t\tif (gitCredentialDirectory) rmSync(gitCredentialDirectory, { force: true, recursive: true });\n\t\t\tawait proxy?.close();\n\t\t},\n\t};\n}\n\n/**\n * Start one approved command in its own child, with one extra writable root.\n *\n * Deliberately minimal compared with the session child: no network relay, no credential\n * channels, no terminal handoff. An escalated command is a single shell invocation the\n * human just read and approved, not a session, and every channel omitted here is one it\n * cannot reach. Its stdio is captured rather than inherited so the session that asked\n * receives the output instead of the escalated process writing over the TUI.\n */\nasync function runEscalatedCommand(options: {\n\trequest: CommandEscalationRequest;\n\tlaunch: SandboxLaunch;\n\tstateDirectory: string;\n\tspawnBwrap: typeof spawn;\n}): Promise<CommandEscalationResult> {\n\tconst { request, launch, stateDirectory } = options;\n\tconst escalatedReadOnlyPaths = launch.readOnlyPaths ?? [];\n\tconst readOnlyFileDescriptors = [\n\t\t...descriptorBackedReadOnlyPaths(escalatedReadOnlyPaths),\n\t\t...(launch.readOnlyFiles ?? []),\n\t].map((path) => openSync(path, \"r\"));\n\tconst child = options.spawnBwrap(\n\t\t\"bwrap\",\n\t\tbuildBwrapArguments({\n\t\t\tworkspace: launch.policy.workspace,\n\t\t\tadditionalWritableRoots: [...launch.policy.additionalWritableRoots, request.writableRoot],\n\t\t\treadOnlyPaths: escalatedReadOnlyPaths,\n\t\t\treadOnlyFiles: launch.readOnlyFiles ?? [],\n\t\t\treadOnlyBinaries: launch.readOnlyBinaries ?? [],\n\t\t\tsockets: [],\n\t\t\tenvironment: {\n\t\t\t\tHOME: launch.environment?.HOME ?? stateDirectory,\n\t\t\t\tTMPDIR: launch.environment?.TMPDIR ?? stateDirectory,\n\t\t\t},\n\t\t\tcommand: \"/bin/sh\",\n\t\t\targs: [\"-c\", request.command],\n\t\t}),\n\t\t{ stdio: [\"ignore\", \"pipe\", \"pipe\", ...readOnlyFileDescriptors] },\n\t);\n\tlet stdout = \"\";\n\tlet stderr = \"\";\n\ttry {\n\t\tchild.stdout?.setEncoding(\"utf8\");\n\t\tchild.stdout?.on(\"data\", (chunk: string) => {\n\t\t\tstdout += chunk;\n\t\t});\n\t\tchild.stderr?.setEncoding(\"utf8\");\n\t\tchild.stderr?.on(\"data\", (chunk: string) => {\n\t\t\tstderr += chunk;\n\t\t});\n\t\tconst code = await new Promise<number>((resolveCode) => {\n\t\t\tchild.once(\"error\", () => resolveCode(1));\n\t\t\tchild.once(\"exit\", (exitCode) => resolveCode(exitCode ?? 1));\n\t\t});\n\t\treturn { code, stdout, stderr };\n\t} finally {\n\t\tfor (const descriptor of readOnlyFileDescriptors) closeSync(descriptor);\n\t}\n}\n"]}
|