apex-code 0.0.6 → 0.1.1
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 +33 -0
- package/README.md +8 -9
- package/dist/cli/args.d.ts +0 -12
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +2 -32
- 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/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 +6 -14
- package/dist/core/delegation/runtime.d.ts.map +1 -1
- package/dist/core/delegation/runtime.js +1 -2
- 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 +1 -9
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +10 -35
- 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/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/main.d.ts +0 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +2 -3
- package/dist/main.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-types.d.ts +1 -1
- 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/containerization.md +8 -4
- package/docs/sdk.md +17 -24
- package/docs/security.md +7 -5
- package/docs/settings.md +2 -7
- package/docs/skills.md +4 -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,153 +0,0 @@
|
|
|
1
|
-
import { existsSync, unlinkSync } from "node:fs";
|
|
2
|
-
import * as http from "node:http";
|
|
3
|
-
import * as net from "node:net";
|
|
4
|
-
export function createSandboxNetworkProxy(options) {
|
|
5
|
-
/** Hosts a human permitted this session. Never persisted; a durable entry is a settings edit. */
|
|
6
|
-
const granted = new Set();
|
|
7
|
-
/**
|
|
8
|
-
* Approvals currently awaiting an answer, keyed the same way a grant is.
|
|
9
|
-
*
|
|
10
|
-
* A real prompt takes as long as a human takes to read it, and a refused host usually
|
|
11
|
-
* arrives as several near-simultaneous connections. Without this, each one raises its
|
|
12
|
-
* own prompt for the same question.
|
|
13
|
-
*/
|
|
14
|
-
const pending = new Map();
|
|
15
|
-
function isHostReachable(hostname) {
|
|
16
|
-
if (options.allowedHosts.some((entry) => entry === hostname || entry.startsWith(`${hostname}:`)))
|
|
17
|
-
return true;
|
|
18
|
-
for (const grant of granted) {
|
|
19
|
-
if (grant === hostname || grant.startsWith(`${hostname}:`))
|
|
20
|
-
return true;
|
|
21
|
-
}
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
function isAllowed(hostname, port) {
|
|
25
|
-
return (options.allowedHosts.includes(hostname) ||
|
|
26
|
-
options.allowedHosts.includes(`${hostname}:${port}`) ||
|
|
27
|
-
granted.has(`${hostname}:${port}`));
|
|
28
|
-
}
|
|
29
|
-
async function approve(hostname, port) {
|
|
30
|
-
const key = `${hostname}:${port}`;
|
|
31
|
-
if (granted.has(key))
|
|
32
|
-
return true;
|
|
33
|
-
const inFlight = pending.get(key);
|
|
34
|
-
if (inFlight)
|
|
35
|
-
return inFlight;
|
|
36
|
-
// Declining is deliberately not remembered. A second attempt asks again, because a
|
|
37
|
-
// cached "no" would be indistinguishable to the user from the boundary having
|
|
38
|
-
// silently stopped asking.
|
|
39
|
-
const question = (async () => {
|
|
40
|
-
try {
|
|
41
|
-
const approved = await options.requestApproval(hostname, port);
|
|
42
|
-
if (approved)
|
|
43
|
-
granted.add(key);
|
|
44
|
-
return approved;
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
finally {
|
|
50
|
-
pending.delete(key);
|
|
51
|
-
}
|
|
52
|
-
})();
|
|
53
|
-
pending.set(key, question);
|
|
54
|
-
return question;
|
|
55
|
-
}
|
|
56
|
-
return new Promise((resolve, reject) => {
|
|
57
|
-
const server = http.createServer((_req, res) => {
|
|
58
|
-
res.writeHead(405, { "Content-Type": "text/plain" });
|
|
59
|
-
res.end("Method Not Allowed\n");
|
|
60
|
-
});
|
|
61
|
-
server.on("connect", (req, clientSocket, head) => {
|
|
62
|
-
const match = req.url?.match(/^([^:]+):(\d+)$/);
|
|
63
|
-
if (!match) {
|
|
64
|
-
clientSocket.destroy();
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
const hostname = match[1];
|
|
68
|
-
const port = Number.parseInt(match[2], 10);
|
|
69
|
-
function refuse() {
|
|
70
|
-
if (!clientSocket.destroyed) {
|
|
71
|
-
clientSocket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
72
|
-
clientSocket.destroy();
|
|
73
|
-
}
|
|
74
|
-
options.violationStore?.add({
|
|
75
|
-
kind: "network",
|
|
76
|
-
command: `CONNECT ${hostname}:${port}`,
|
|
77
|
-
detail: `Host ${hostname} refused by allowlist policy.`,
|
|
78
|
-
timestamp: new Date(),
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
if (!isAllowed(hostname, port)) {
|
|
82
|
-
if (!options.requestApproval) {
|
|
83
|
-
refuse();
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
// The client is left waiting on its CONNECT while the human reads the
|
|
87
|
-
// prompt, which is what makes an approved host resume the original request
|
|
88
|
-
// instead of surfacing as a failure the caller has to retry.
|
|
89
|
-
void approve(hostname, port).then((approved) => {
|
|
90
|
-
if (approved)
|
|
91
|
-
tunnel();
|
|
92
|
-
else
|
|
93
|
-
refuse();
|
|
94
|
-
});
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
tunnel();
|
|
98
|
-
function tunnel() {
|
|
99
|
-
if (clientSocket.destroyed)
|
|
100
|
-
return;
|
|
101
|
-
const serverSocket = net.connect(Number(port) || 443, hostname, () => {
|
|
102
|
-
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
103
|
-
serverSocket.write(head);
|
|
104
|
-
serverSocket.pipe(clientSocket);
|
|
105
|
-
clientSocket.pipe(serverSocket);
|
|
106
|
-
});
|
|
107
|
-
serverSocket.on("error", () => {
|
|
108
|
-
if (!clientSocket.destroyed) {
|
|
109
|
-
clientSocket.write(`HTTP/1.1 502 Bad Gateway\r\n\r\n`);
|
|
110
|
-
clientSocket.destroy();
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
clientSocket.on("error", () => {
|
|
114
|
-
serverSocket.destroy();
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
server.on("error", reject);
|
|
119
|
-
if (options.tcpHost !== undefined) {
|
|
120
|
-
// macOS's Seatbelt can permit a specific localhost port directly, so the
|
|
121
|
-
// proxy listens on loopback TCP there instead of a Unix domain socket --
|
|
122
|
-
// no in-child relay process is needed the way Linux's `--unshare-net`
|
|
123
|
-
// requires one.
|
|
124
|
-
server.listen(0, options.tcpHost, () => {
|
|
125
|
-
server.off("error", reject);
|
|
126
|
-
const address = server.address();
|
|
127
|
-
const port = typeof address === "object" && address !== null ? address.port : undefined;
|
|
128
|
-
resolve({
|
|
129
|
-
server,
|
|
130
|
-
port,
|
|
131
|
-
isHostReachable,
|
|
132
|
-
close: () => new Promise((r) => server.close(() => r())),
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
// A prior sandboxed launch that never called close() (a crash, a SIGKILL)
|
|
138
|
-
// leaves a stale socket file behind; Node's listen() refuses to bind over
|
|
139
|
-
// one even though nothing is listening on it.
|
|
140
|
-
if (existsSync(options.socketPath)) {
|
|
141
|
-
unlinkSync(options.socketPath);
|
|
142
|
-
}
|
|
143
|
-
server.listen(options.socketPath, () => {
|
|
144
|
-
server.off("error", reject);
|
|
145
|
-
resolve({
|
|
146
|
-
server,
|
|
147
|
-
isHostReachable,
|
|
148
|
-
close: () => new Promise((r) => server.close(() => r())),
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
//# sourceMappingURL=network-proxy.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"network-proxy.js","sourceRoot":"","sources":["../../../src/core/sandbox/network-proxy.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, unlinkSync } from \"node:fs\";\nimport * as http from \"node:http\";\nimport * as net from \"node:net\";\nimport type { SandboxViolationStore } from \"./violations.ts\";\n\nexport interface SandboxNetworkProxy {\n\treadonly server: http.Server;\n\t/** Set only in TCP-listen mode (`tcpHost`); the ephemeral port the proxy bound. */\n\treadonly port?: number;\n\t/**\n\t * Whether this session may reach `hostname` on any port, by configuration or by a\n\t * grant the human made this session.\n\t *\n\t * Exposed so the git credential channel can ask the one component that actually knows,\n\t * rather than keeping a second copy of the allowlist that would drift from this one the\n\t * first time a host is approved at runtime.\n\t */\n\tisHostReachable(hostname: string): boolean;\n\tclose(): Promise<void>;\n}\n\nexport function createSandboxNetworkProxy(\n\toptions: {\n\t\tallowedHosts: readonly string[];\n\t\tviolationStore?: SandboxViolationStore;\n\t\t/**\n\t\t * Ask a human whether to permit one refused host for the rest of this session.\n\t\t *\n\t\t * Supplied by the supervisor and never by the child: per ADR 0023, an approval\n\t\t * asserted from inside the boundary would be indistinguishable from one forged by\n\t\t * the code the boundary exists to contain. Absent means deny without asking, which\n\t\t * is exactly today's behaviour and what ADR 0005 requires of headless, print,\n\t\t * JSON, and RPC modes.\n\t\t */\n\t\trequestApproval?: (hostname: string, port: number) => Promise<boolean>;\n\t} & ({ socketPath: string; tcpHost?: undefined } | { socketPath?: undefined; tcpHost: string }),\n): Promise<SandboxNetworkProxy> {\n\t/** Hosts a human permitted this session. Never persisted; a durable entry is a settings edit. */\n\tconst granted = new Set<string>();\n\t/**\n\t * Approvals currently awaiting an answer, keyed the same way a grant is.\n\t *\n\t * A real prompt takes as long as a human takes to read it, and a refused host usually\n\t * arrives as several near-simultaneous connections. Without this, each one raises its\n\t * own prompt for the same question.\n\t */\n\tconst pending = new Map<string, Promise<boolean>>();\n\n\tfunction isHostReachable(hostname: string): boolean {\n\t\tif (options.allowedHosts.some((entry) => entry === hostname || entry.startsWith(`${hostname}:`))) return true;\n\t\tfor (const grant of granted) {\n\t\t\tif (grant === hostname || grant.startsWith(`${hostname}:`)) return true;\n\t\t}\n\t\treturn false;\n\t}\n\n\tfunction isAllowed(hostname: string, port: number): boolean {\n\t\treturn (\n\t\t\toptions.allowedHosts.includes(hostname) ||\n\t\t\toptions.allowedHosts.includes(`${hostname}:${port}`) ||\n\t\t\tgranted.has(`${hostname}:${port}`)\n\t\t);\n\t}\n\n\tasync function approve(hostname: string, port: number): Promise<boolean> {\n\t\tconst key = `${hostname}:${port}`;\n\t\tif (granted.has(key)) return true;\n\t\tconst inFlight = pending.get(key);\n\t\tif (inFlight) return inFlight;\n\t\t// Declining is deliberately not remembered. A second attempt asks again, because a\n\t\t// cached \"no\" would be indistinguishable to the user from the boundary having\n\t\t// silently stopped asking.\n\t\tconst question = (async () => {\n\t\t\ttry {\n\t\t\t\tconst approved = await (options.requestApproval as (h: string, p: number) => Promise<boolean>)(\n\t\t\t\t\thostname,\n\t\t\t\t\tport,\n\t\t\t\t);\n\t\t\t\tif (approved) granted.add(key);\n\t\t\t\treturn approved;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t} finally {\n\t\t\t\tpending.delete(key);\n\t\t\t}\n\t\t})();\n\t\tpending.set(key, question);\n\t\treturn question;\n\t}\n\n\treturn new Promise((resolve, reject) => {\n\t\tconst server = http.createServer((_req, res) => {\n\t\t\tres.writeHead(405, { \"Content-Type\": \"text/plain\" });\n\t\t\tres.end(\"Method Not Allowed\\n\");\n\t\t});\n\n\t\tserver.on(\"connect\", (req, clientSocket, head) => {\n\t\t\tconst match = req.url?.match(/^([^:]+):(\\d+)$/);\n\t\t\tif (!match) {\n\t\t\t\tclientSocket.destroy();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst hostname = match[1];\n\t\t\tconst port = Number.parseInt(match[2], 10);\n\n\t\t\tfunction refuse(): void {\n\t\t\t\tif (!clientSocket.destroyed) {\n\t\t\t\t\tclientSocket.write(\"HTTP/1.1 403 Forbidden\\r\\n\\r\\n\");\n\t\t\t\t\tclientSocket.destroy();\n\t\t\t\t}\n\t\t\t\toptions.violationStore?.add({\n\t\t\t\t\tkind: \"network\",\n\t\t\t\t\tcommand: `CONNECT ${hostname}:${port}`,\n\t\t\t\t\tdetail: `Host ${hostname} refused by allowlist policy.`,\n\t\t\t\t\ttimestamp: new Date(),\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (!isAllowed(hostname, port)) {\n\t\t\t\tif (!options.requestApproval) {\n\t\t\t\t\trefuse();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// The client is left waiting on its CONNECT while the human reads the\n\t\t\t\t// prompt, which is what makes an approved host resume the original request\n\t\t\t\t// instead of surfacing as a failure the caller has to retry.\n\t\t\t\tvoid approve(hostname, port).then((approved) => {\n\t\t\t\t\tif (approved) tunnel();\n\t\t\t\t\telse refuse();\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttunnel();\n\n\t\t\tfunction tunnel(): void {\n\t\t\t\tif (clientSocket.destroyed) return;\n\t\t\t\tconst serverSocket = net.connect(Number(port) || 443, hostname, () => {\n\t\t\t\t\tclientSocket.write(\"HTTP/1.1 200 Connection Established\\r\\n\\r\\n\");\n\t\t\t\t\tserverSocket.write(head);\n\t\t\t\t\tserverSocket.pipe(clientSocket);\n\t\t\t\t\tclientSocket.pipe(serverSocket);\n\t\t\t\t});\n\n\t\t\t\tserverSocket.on(\"error\", () => {\n\t\t\t\t\tif (!clientSocket.destroyed) {\n\t\t\t\t\t\tclientSocket.write(`HTTP/1.1 502 Bad Gateway\\r\\n\\r\\n`);\n\t\t\t\t\t\tclientSocket.destroy();\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t\tclientSocket.on(\"error\", () => {\n\t\t\t\t\tserverSocket.destroy();\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\n\t\tserver.on(\"error\", reject);\n\n\t\tif (options.tcpHost !== undefined) {\n\t\t\t// macOS's Seatbelt can permit a specific localhost port directly, so the\n\t\t\t// proxy listens on loopback TCP there instead of a Unix domain socket --\n\t\t\t// no in-child relay process is needed the way Linux's `--unshare-net`\n\t\t\t// requires one.\n\t\t\tserver.listen(0, options.tcpHost, () => {\n\t\t\t\tserver.off(\"error\", reject);\n\t\t\t\tconst address = server.address();\n\t\t\t\tconst port = typeof address === \"object\" && address !== null ? address.port : undefined;\n\t\t\t\tresolve({\n\t\t\t\t\tserver,\n\t\t\t\t\tport,\n\t\t\t\t\tisHostReachable,\n\t\t\t\t\tclose: () => new Promise((r) => server.close(() => r())),\n\t\t\t\t});\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\t// A prior sandboxed launch that never called close() (a crash, a SIGKILL)\n\t\t// leaves a stale socket file behind; Node's listen() refuses to bind over\n\t\t// one even though nothing is listening on it.\n\t\tif (existsSync(options.socketPath)) {\n\t\t\tunlinkSync(options.socketPath);\n\t\t}\n\n\t\tserver.listen(options.socketPath, () => {\n\t\t\tserver.off(\"error\", reject);\n\t\t\tresolve({\n\t\t\t\tserver,\n\t\t\t\tisHostReachable,\n\t\t\t\tclose: () => new Promise((r) => server.close(() => r())),\n\t\t\t});\n\t\t});\n\t});\n}\n"]}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Turn a sandbox allowlist refusal into a message the user can act on.
|
|
3
|
-
*
|
|
4
|
-
* The child reaches the network only through the supervisor's CONNECT proxy, which
|
|
5
|
-
* answers 403 for any host absent from `network.allowedHosts`. undici reports that as
|
|
6
|
-
* `Proxy response (403) !== 200 when HTTP Tunneling`, but fetch surfaces only the
|
|
7
|
-
* generic `fetch failed` at the top of the cause chain — so the caller sees a network
|
|
8
|
-
* error with no host, no reason, and no remedy. Recovering the detail here is what
|
|
9
|
-
* separates "something is broken" from "this host is not on the allowlist".
|
|
10
|
-
*/
|
|
11
|
-
/**
|
|
12
|
-
* Describe a refused request, or return undefined when the failure was something else.
|
|
13
|
-
* Only the sandboxed child installs this, so attributing the refusal to the sandbox
|
|
14
|
-
* allowlist is accurate rather than a guess about whose proxy answered.
|
|
15
|
-
*/
|
|
16
|
-
export declare function describeSandboxNetworkRefusal(error: unknown, url: string): string | undefined;
|
|
17
|
-
/**
|
|
18
|
-
* Wrap global fetch so every caller — model providers, the version check, catalogs —
|
|
19
|
-
* reports a refusal in the same actionable terms without each having to know the
|
|
20
|
-
* sandbox exists. The original error is preserved as `cause`, and anything that is not
|
|
21
|
-
* a refusal is rethrown untouched so retry and abort handling behave exactly as before.
|
|
22
|
-
*/
|
|
23
|
-
export declare function installSandboxNetworkRefusalMessages(): void;
|
|
24
|
-
//# sourceMappingURL=network-refusal.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"network-refusal.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/network-refusal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAwBH;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAK7F;AASD;;;;;GAKG;AACH,wBAAgB,oCAAoC,IAAI,IAAI,CAW3D","sourcesContent":["/**\n * Turn a sandbox allowlist refusal into a message the user can act on.\n *\n * The child reaches the network only through the supervisor's CONNECT proxy, which\n * answers 403 for any host absent from `network.allowedHosts`. undici reports that as\n * `Proxy response (403) !== 200 when HTTP Tunneling`, but fetch surfaces only the\n * generic `fetch failed` at the top of the cause chain — so the caller sees a network\n * error with no host, no reason, and no remedy. Recovering the detail here is what\n * separates \"something is broken\" from \"this host is not on the allowlist\".\n */\n\n/** undici's own wording for a tunnel the proxy declined to open. */\nconst TUNNEL_REFUSAL = /Proxy response \\((\\d+)\\) !== 200 when HTTP Tunneling/;\n\nconst MAXIMUM_CAUSE_DEPTH = 8;\n\nfunction findTunnelRefusal(error: unknown): boolean {\n\tlet current: unknown = error;\n\tfor (let depth = 0; depth < MAXIMUM_CAUSE_DEPTH && current instanceof Error; depth++) {\n\t\tif (TUNNEL_REFUSAL.test(current.message)) return true;\n\t\tcurrent = current.cause;\n\t}\n\treturn false;\n}\n\nfunction hostFromUrl(url: string): string | undefined {\n\ttry {\n\t\treturn new URL(url).host;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Describe a refused request, or return undefined when the failure was something else.\n * Only the sandboxed child installs this, so attributing the refusal to the sandbox\n * allowlist is accurate rather than a guess about whose proxy answered.\n */\nexport function describeSandboxNetworkRefusal(error: unknown, url: string): string | undefined {\n\tif (!findTunnelRefusal(error)) return undefined;\n\tconst host = hostFromUrl(url);\n\tconst subject = host ? `Host ${host} is` : \"That host is\";\n\treturn `${subject} not on the sandbox network allowlist, so the request was refused. Add it to \"network.allowedHosts\" in your global Apex Code settings to permit it.`;\n}\n\nfunction requestUrl(input: unknown): string {\n\tif (typeof input === \"string\") return input;\n\tif (input instanceof URL) return input.href;\n\tif (input instanceof Request) return input.url;\n\treturn \"\";\n}\n\n/**\n * Wrap global fetch so every caller — model providers, the version check, catalogs —\n * reports a refusal in the same actionable terms without each having to know the\n * sandbox exists. The original error is preserved as `cause`, and anything that is not\n * a refusal is rethrown untouched so retry and abort handling behave exactly as before.\n */\nexport function installSandboxNetworkRefusalMessages(): void {\n\tconst wrapped = globalThis.fetch;\n\tglobalThis.fetch = async function fetchWithRefusalDetail(input, init) {\n\t\ttry {\n\t\t\treturn await wrapped(input, init);\n\t\t} catch (error) {\n\t\t\tconst description = describeSandboxNetworkRefusal(error, requestUrl(input));\n\t\t\tif (!description) throw error;\n\t\t\tthrow new Error(description, { cause: error });\n\t\t}\n\t} as typeof globalThis.fetch;\n}\n"]}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Turn a sandbox allowlist refusal into a message the user can act on.
|
|
3
|
-
*
|
|
4
|
-
* The child reaches the network only through the supervisor's CONNECT proxy, which
|
|
5
|
-
* answers 403 for any host absent from `network.allowedHosts`. undici reports that as
|
|
6
|
-
* `Proxy response (403) !== 200 when HTTP Tunneling`, but fetch surfaces only the
|
|
7
|
-
* generic `fetch failed` at the top of the cause chain — so the caller sees a network
|
|
8
|
-
* error with no host, no reason, and no remedy. Recovering the detail here is what
|
|
9
|
-
* separates "something is broken" from "this host is not on the allowlist".
|
|
10
|
-
*/
|
|
11
|
-
/** undici's own wording for a tunnel the proxy declined to open. */
|
|
12
|
-
const TUNNEL_REFUSAL = /Proxy response \((\d+)\) !== 200 when HTTP Tunneling/;
|
|
13
|
-
const MAXIMUM_CAUSE_DEPTH = 8;
|
|
14
|
-
function findTunnelRefusal(error) {
|
|
15
|
-
let current = error;
|
|
16
|
-
for (let depth = 0; depth < MAXIMUM_CAUSE_DEPTH && current instanceof Error; depth++) {
|
|
17
|
-
if (TUNNEL_REFUSAL.test(current.message))
|
|
18
|
-
return true;
|
|
19
|
-
current = current.cause;
|
|
20
|
-
}
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
function hostFromUrl(url) {
|
|
24
|
-
try {
|
|
25
|
-
return new URL(url).host;
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Describe a refused request, or return undefined when the failure was something else.
|
|
33
|
-
* Only the sandboxed child installs this, so attributing the refusal to the sandbox
|
|
34
|
-
* allowlist is accurate rather than a guess about whose proxy answered.
|
|
35
|
-
*/
|
|
36
|
-
export function describeSandboxNetworkRefusal(error, url) {
|
|
37
|
-
if (!findTunnelRefusal(error))
|
|
38
|
-
return undefined;
|
|
39
|
-
const host = hostFromUrl(url);
|
|
40
|
-
const subject = host ? `Host ${host} is` : "That host is";
|
|
41
|
-
return `${subject} not on the sandbox network allowlist, so the request was refused. Add it to "network.allowedHosts" in your global Apex Code settings to permit it.`;
|
|
42
|
-
}
|
|
43
|
-
function requestUrl(input) {
|
|
44
|
-
if (typeof input === "string")
|
|
45
|
-
return input;
|
|
46
|
-
if (input instanceof URL)
|
|
47
|
-
return input.href;
|
|
48
|
-
if (input instanceof Request)
|
|
49
|
-
return input.url;
|
|
50
|
-
return "";
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Wrap global fetch so every caller — model providers, the version check, catalogs —
|
|
54
|
-
* reports a refusal in the same actionable terms without each having to know the
|
|
55
|
-
* sandbox exists. The original error is preserved as `cause`, and anything that is not
|
|
56
|
-
* a refusal is rethrown untouched so retry and abort handling behave exactly as before.
|
|
57
|
-
*/
|
|
58
|
-
export function installSandboxNetworkRefusalMessages() {
|
|
59
|
-
const wrapped = globalThis.fetch;
|
|
60
|
-
globalThis.fetch = async function fetchWithRefusalDetail(input, init) {
|
|
61
|
-
try {
|
|
62
|
-
return await wrapped(input, init);
|
|
63
|
-
}
|
|
64
|
-
catch (error) {
|
|
65
|
-
const description = describeSandboxNetworkRefusal(error, requestUrl(input));
|
|
66
|
-
if (!description)
|
|
67
|
-
throw error;
|
|
68
|
-
throw new Error(description, { cause: error });
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
//# sourceMappingURL=network-refusal.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"network-refusal.js","sourceRoot":"","sources":["../../../src/core/sandbox/network-refusal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,oEAAoE;AACpE,MAAM,cAAc,GAAG,sDAAsD,CAAC;AAE9E,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAE9B,SAAS,iBAAiB,CAAC,KAAc,EAAW;IACnD,IAAI,OAAO,GAAY,KAAK,CAAC;IAC7B,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,mBAAmB,IAAI,OAAO,YAAY,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC;QACtF,IAAI,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC;YAAE,OAAO,IAAI,CAAC;QACtD,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC;IACzB,CAAC;IACD,OAAO,KAAK,CAAC;AAAA,CACb;AAED,SAAS,WAAW,CAAC,GAAW,EAAsB;IACrD,IAAI,CAAC;QACJ,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,SAAS,CAAC;IAClB,CAAC;AAAA,CACD;AAED;;;;GAIG;AACH,MAAM,UAAU,6BAA6B,CAAC,KAAc,EAAE,GAAW,EAAsB;IAC9F,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAChD,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,KAAK,CAAC,CAAC,CAAC,cAAc,CAAC;IAC1D,OAAO,GAAG,OAAO,qJAAqJ,CAAC;AAAA,CACvK;AAED,SAAS,UAAU,CAAC,KAAc,EAAU;IAC3C,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,IAAI,KAAK,YAAY,GAAG;QAAE,OAAO,KAAK,CAAC,IAAI,CAAC;IAC5C,IAAI,KAAK,YAAY,OAAO;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC;IAC/C,OAAO,EAAE,CAAC;AAAA,CACV;AAED;;;;;GAKG;AACH,MAAM,UAAU,oCAAoC,GAAS;IAC5D,MAAM,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC;IACjC,UAAU,CAAC,KAAK,GAAG,KAAK,UAAU,sBAAsB,CAAC,KAAK,EAAE,IAAI,EAAE;QACrE,IAAI,CAAC;YACJ,OAAO,MAAM,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACnC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,WAAW,GAAG,6BAA6B,CAAC,KAAK,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;YAC5E,IAAI,CAAC,WAAW;gBAAE,MAAM,KAAK,CAAC;YAC9B,MAAM,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QAChD,CAAC;IAAA,CAC0B,CAAC;AAAA,CAC7B","sourcesContent":["/**\n * Turn a sandbox allowlist refusal into a message the user can act on.\n *\n * The child reaches the network only through the supervisor's CONNECT proxy, which\n * answers 403 for any host absent from `network.allowedHosts`. undici reports that as\n * `Proxy response (403) !== 200 when HTTP Tunneling`, but fetch surfaces only the\n * generic `fetch failed` at the top of the cause chain — so the caller sees a network\n * error with no host, no reason, and no remedy. Recovering the detail here is what\n * separates \"something is broken\" from \"this host is not on the allowlist\".\n */\n\n/** undici's own wording for a tunnel the proxy declined to open. */\nconst TUNNEL_REFUSAL = /Proxy response \\((\\d+)\\) !== 200 when HTTP Tunneling/;\n\nconst MAXIMUM_CAUSE_DEPTH = 8;\n\nfunction findTunnelRefusal(error: unknown): boolean {\n\tlet current: unknown = error;\n\tfor (let depth = 0; depth < MAXIMUM_CAUSE_DEPTH && current instanceof Error; depth++) {\n\t\tif (TUNNEL_REFUSAL.test(current.message)) return true;\n\t\tcurrent = current.cause;\n\t}\n\treturn false;\n}\n\nfunction hostFromUrl(url: string): string | undefined {\n\ttry {\n\t\treturn new URL(url).host;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Describe a refused request, or return undefined when the failure was something else.\n * Only the sandboxed child installs this, so attributing the refusal to the sandbox\n * allowlist is accurate rather than a guess about whose proxy answered.\n */\nexport function describeSandboxNetworkRefusal(error: unknown, url: string): string | undefined {\n\tif (!findTunnelRefusal(error)) return undefined;\n\tconst host = hostFromUrl(url);\n\tconst subject = host ? `Host ${host} is` : \"That host is\";\n\treturn `${subject} not on the sandbox network allowlist, so the request was refused. Add it to \"network.allowedHosts\" in your global Apex Code settings to permit it.`;\n}\n\nfunction requestUrl(input: unknown): string {\n\tif (typeof input === \"string\") return input;\n\tif (input instanceof URL) return input.href;\n\tif (input instanceof Request) return input.url;\n\treturn \"\";\n}\n\n/**\n * Wrap global fetch so every caller — model providers, the version check, catalogs —\n * reports a refusal in the same actionable terms without each having to know the\n * sandbox exists. The original error is preserved as `cause`, and anything that is not\n * a refusal is rethrown untouched so retry and abort handling behave exactly as before.\n */\nexport function installSandboxNetworkRefusalMessages(): void {\n\tconst wrapped = globalThis.fetch;\n\tglobalThis.fetch = async function fetchWithRefusalDetail(input, init) {\n\t\ttry {\n\t\t\treturn await wrapped(input, init);\n\t\t} catch (error) {\n\t\t\tconst description = describeSandboxNetworkRefusal(error, requestUrl(input));\n\t\t\tif (!description) throw error;\n\t\t\tthrow new Error(description, { cause: error });\n\t\t}\n\t} as typeof globalThis.fetch;\n}\n"]}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
export interface SandboxPolicy {
|
|
2
|
-
/**
|
|
3
|
-
* The one directory the session is anchored to. State, sessions, and the concurrency
|
|
4
|
-
* lease all live under it, and the child starts here, so it stays singular even when
|
|
5
|
-
* more roots are writable.
|
|
6
|
-
*/
|
|
7
|
-
readonly workspace: string;
|
|
8
|
-
readonly allowedHosts: readonly string[];
|
|
9
|
-
/** Further writable directories, named on the command line and validated the same way. */
|
|
10
|
-
readonly additionalWritableRoots: readonly string[];
|
|
11
|
-
}
|
|
12
|
-
export type SandboxPolicyResult = {
|
|
13
|
-
readonly kind: "valid";
|
|
14
|
-
readonly policy: SandboxPolicy;
|
|
15
|
-
} | {
|
|
16
|
-
readonly kind: "invalid";
|
|
17
|
-
readonly reason: string;
|
|
18
|
-
};
|
|
19
|
-
export type SandboxStatus = {
|
|
20
|
-
readonly kind: "enforced";
|
|
21
|
-
} | {
|
|
22
|
-
readonly kind: "unavailable";
|
|
23
|
-
readonly reason: string;
|
|
24
|
-
};
|
|
25
|
-
export declare class SandboxUnavailableError extends Error {
|
|
26
|
-
constructor(reason: string);
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Validate the single writable boundary before any platform backend runs. We accept
|
|
30
|
-
* no relative, missing, or non-directory workspace because a backend must never
|
|
31
|
-
* silently broaden an ambiguous path into host authority.
|
|
32
|
-
*/
|
|
33
|
-
export declare function createSandboxPolicy(options: {
|
|
34
|
-
workspace: string;
|
|
35
|
-
allowedHosts?: readonly string[];
|
|
36
|
-
additionalWritableRoots?: readonly string[];
|
|
37
|
-
}): SandboxPolicyResult;
|
|
38
|
-
/** Throws rather than returning an executable fallback when OS enforcement is absent. */
|
|
39
|
-
export declare function requireSandboxEnforcement(status: SandboxStatus): void;
|
|
40
|
-
//# sourceMappingURL=policy.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/policy.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,aAAa;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,0FAA0F;IAC1F,QAAQ,CAAC,uBAAuB,EAAE,SAAS,MAAM,EAAE,CAAC;CACpD;AAED,MAAM,MAAM,mBAAmB,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAA;CAAE,GAC1D;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD,MAAM,MAAM,aAAa,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtH,qBAAa,uBAAwB,SAAQ,KAAK;IACjD,YAAY,MAAM,EAAE,MAAM,EAGzB;CACD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,uBAAuB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5C,GAAG,mBAAmB,CAetB;AAsBD,yFAAyF;AACzF,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAGrE","sourcesContent":["import { existsSync, realpathSync, statSync } from \"node:fs\";\nimport { isAbsolute } from \"node:path\";\n\nexport interface SandboxPolicy {\n\t/**\n\t * The one directory the session is anchored to. State, sessions, and the concurrency\n\t * lease all live under it, and the child starts here, so it stays singular even when\n\t * more roots are writable.\n\t */\n\treadonly workspace: string;\n\treadonly allowedHosts: readonly string[];\n\t/** Further writable directories, named on the command line and validated the same way. */\n\treadonly additionalWritableRoots: readonly string[];\n}\n\nexport type SandboxPolicyResult =\n\t| { readonly kind: \"valid\"; readonly policy: SandboxPolicy }\n\t| { readonly kind: \"invalid\"; readonly reason: string };\n\nexport type SandboxStatus = { readonly kind: \"enforced\" } | { readonly kind: \"unavailable\"; readonly reason: string };\n\nexport class SandboxUnavailableError extends Error {\n\tconstructor(reason: string) {\n\t\tsuper(`OS sandbox is not enforcing this agent session: ${reason}`);\n\t\tthis.name = \"SandboxUnavailableError\";\n\t}\n}\n\n/**\n * Validate the single writable boundary before any platform backend runs. We accept\n * no relative, missing, or non-directory workspace because a backend must never\n * silently broaden an ambiguous path into host authority.\n */\nexport function createSandboxPolicy(options: {\n\tworkspace: string;\n\tallowedHosts?: readonly string[];\n\tadditionalWritableRoots?: readonly string[];\n}): SandboxPolicyResult {\n\tconst workspace = validateRoot(options.workspace, \"workspace\");\n\tif (typeof workspace !== \"string\") return workspace;\n\n\tconst additionalWritableRoots: string[] = [];\n\tfor (const candidate of options.additionalWritableRoots ?? []) {\n\t\tconst resolved = validateRoot(candidate, \"writable root\");\n\t\tif (typeof resolved !== \"string\") return resolved;\n\t\tadditionalWritableRoots.push(resolved);\n\t}\n\n\treturn {\n\t\tkind: \"valid\",\n\t\tpolicy: { workspace, allowedHosts: options.allowedHosts ?? [], additionalWritableRoots },\n\t};\n}\n\n/**\n * Resolve one writable root, or explain why it cannot be one.\n *\n * Every root goes through the same check for the same reason the workspace always did: a\n * backend must never silently broaden an ambiguous path into host authority, and an extra\n * root named on the command line is no less capable of doing that than the first one.\n */\nfunction validateRoot(candidate: string, label: string): string | { kind: \"invalid\"; reason: string } {\n\tif (!isAbsolute(candidate)) {\n\t\treturn { kind: \"invalid\", reason: `Sandbox ${label} must be an absolute path: ${candidate}` };\n\t}\n\tif (!existsSync(candidate)) {\n\t\treturn { kind: \"invalid\", reason: `Sandbox ${label} does not exist: ${candidate}` };\n\t}\n\tif (!statSync(candidate).isDirectory()) {\n\t\treturn { kind: \"invalid\", reason: `Sandbox ${label} is not a directory: ${candidate}` };\n\t}\n\treturn realpathSync(candidate);\n}\n\n/** Throws rather than returning an executable fallback when OS enforcement is absent. */\nexport function requireSandboxEnforcement(status: SandboxStatus): void {\n\tif (status.kind === \"enforced\") return;\n\tthrow new SandboxUnavailableError(status.reason);\n}\n"]}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { existsSync, realpathSync, statSync } from "node:fs";
|
|
2
|
-
import { isAbsolute } from "node:path";
|
|
3
|
-
export class SandboxUnavailableError extends Error {
|
|
4
|
-
constructor(reason) {
|
|
5
|
-
super(`OS sandbox is not enforcing this agent session: ${reason}`);
|
|
6
|
-
this.name = "SandboxUnavailableError";
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Validate the single writable boundary before any platform backend runs. We accept
|
|
11
|
-
* no relative, missing, or non-directory workspace because a backend must never
|
|
12
|
-
* silently broaden an ambiguous path into host authority.
|
|
13
|
-
*/
|
|
14
|
-
export function createSandboxPolicy(options) {
|
|
15
|
-
const workspace = validateRoot(options.workspace, "workspace");
|
|
16
|
-
if (typeof workspace !== "string")
|
|
17
|
-
return workspace;
|
|
18
|
-
const additionalWritableRoots = [];
|
|
19
|
-
for (const candidate of options.additionalWritableRoots ?? []) {
|
|
20
|
-
const resolved = validateRoot(candidate, "writable root");
|
|
21
|
-
if (typeof resolved !== "string")
|
|
22
|
-
return resolved;
|
|
23
|
-
additionalWritableRoots.push(resolved);
|
|
24
|
-
}
|
|
25
|
-
return {
|
|
26
|
-
kind: "valid",
|
|
27
|
-
policy: { workspace, allowedHosts: options.allowedHosts ?? [], additionalWritableRoots },
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Resolve one writable root, or explain why it cannot be one.
|
|
32
|
-
*
|
|
33
|
-
* Every root goes through the same check for the same reason the workspace always did: a
|
|
34
|
-
* backend must never silently broaden an ambiguous path into host authority, and an extra
|
|
35
|
-
* root named on the command line is no less capable of doing that than the first one.
|
|
36
|
-
*/
|
|
37
|
-
function validateRoot(candidate, label) {
|
|
38
|
-
if (!isAbsolute(candidate)) {
|
|
39
|
-
return { kind: "invalid", reason: `Sandbox ${label} must be an absolute path: ${candidate}` };
|
|
40
|
-
}
|
|
41
|
-
if (!existsSync(candidate)) {
|
|
42
|
-
return { kind: "invalid", reason: `Sandbox ${label} does not exist: ${candidate}` };
|
|
43
|
-
}
|
|
44
|
-
if (!statSync(candidate).isDirectory()) {
|
|
45
|
-
return { kind: "invalid", reason: `Sandbox ${label} is not a directory: ${candidate}` };
|
|
46
|
-
}
|
|
47
|
-
return realpathSync(candidate);
|
|
48
|
-
}
|
|
49
|
-
/** Throws rather than returning an executable fallback when OS enforcement is absent. */
|
|
50
|
-
export function requireSandboxEnforcement(status) {
|
|
51
|
-
if (status.kind === "enforced")
|
|
52
|
-
return;
|
|
53
|
-
throw new SandboxUnavailableError(status.reason);
|
|
54
|
-
}
|
|
55
|
-
//# sourceMappingURL=policy.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"policy.js","sourceRoot":"","sources":["../../../src/core/sandbox/policy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAoBvC,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IACjD,YAAY,MAAc,EAAE;QAC3B,KAAK,CAAC,mDAAmD,MAAM,EAAE,CAAC,CAAC;QACnE,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IAAA,CACtC;CACD;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAInC,EAAuB;IACvB,MAAM,SAAS,GAAG,YAAY,CAAC,OAAO,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;IAC/D,IAAI,OAAO,SAAS,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAEpD,MAAM,uBAAuB,GAAa,EAAE,CAAC;IAC7C,KAAK,MAAM,SAAS,IAAI,OAAO,CAAC,uBAAuB,IAAI,EAAE,EAAE,CAAC;QAC/D,MAAM,QAAQ,GAAG,YAAY,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;QAC1D,IAAI,OAAO,QAAQ,KAAK,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAClD,uBAAuB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACxC,CAAC;IAED,OAAO;QACN,IAAI,EAAE,OAAO;QACb,MAAM,EAAE,EAAE,SAAS,EAAE,YAAY,EAAE,OAAO,CAAC,YAAY,IAAI,EAAE,EAAE,uBAAuB,EAAE;KACxF,CAAC;AAAA,CACF;AAED;;;;;;GAMG;AACH,SAAS,YAAY,CAAC,SAAiB,EAAE,KAAa,EAAgD;IACrG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC5B,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,KAAK,8BAA8B,SAAS,EAAE,EAAE,CAAC;IAC/F,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC5B,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,KAAK,oBAAoB,SAAS,EAAE,EAAE,CAAC;IACrF,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;QACxC,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,KAAK,wBAAwB,SAAS,EAAE,EAAE,CAAC;IACzF,CAAC;IACD,OAAO,YAAY,CAAC,SAAS,CAAC,CAAC;AAAA,CAC/B;AAED,yFAAyF;AACzF,MAAM,UAAU,yBAAyB,CAAC,MAAqB,EAAQ;IACtE,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU;QAAE,OAAO;IACvC,MAAM,IAAI,uBAAuB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AAAA,CACjD","sourcesContent":["import { existsSync, realpathSync, statSync } from \"node:fs\";\nimport { isAbsolute } from \"node:path\";\n\nexport interface SandboxPolicy {\n\t/**\n\t * The one directory the session is anchored to. State, sessions, and the concurrency\n\t * lease all live under it, and the child starts here, so it stays singular even when\n\t * more roots are writable.\n\t */\n\treadonly workspace: string;\n\treadonly allowedHosts: readonly string[];\n\t/** Further writable directories, named on the command line and validated the same way. */\n\treadonly additionalWritableRoots: readonly string[];\n}\n\nexport type SandboxPolicyResult =\n\t| { readonly kind: \"valid\"; readonly policy: SandboxPolicy }\n\t| { readonly kind: \"invalid\"; readonly reason: string };\n\nexport type SandboxStatus = { readonly kind: \"enforced\" } | { readonly kind: \"unavailable\"; readonly reason: string };\n\nexport class SandboxUnavailableError extends Error {\n\tconstructor(reason: string) {\n\t\tsuper(`OS sandbox is not enforcing this agent session: ${reason}`);\n\t\tthis.name = \"SandboxUnavailableError\";\n\t}\n}\n\n/**\n * Validate the single writable boundary before any platform backend runs. We accept\n * no relative, missing, or non-directory workspace because a backend must never\n * silently broaden an ambiguous path into host authority.\n */\nexport function createSandboxPolicy(options: {\n\tworkspace: string;\n\tallowedHosts?: readonly string[];\n\tadditionalWritableRoots?: readonly string[];\n}): SandboxPolicyResult {\n\tconst workspace = validateRoot(options.workspace, \"workspace\");\n\tif (typeof workspace !== \"string\") return workspace;\n\n\tconst additionalWritableRoots: string[] = [];\n\tfor (const candidate of options.additionalWritableRoots ?? []) {\n\t\tconst resolved = validateRoot(candidate, \"writable root\");\n\t\tif (typeof resolved !== \"string\") return resolved;\n\t\tadditionalWritableRoots.push(resolved);\n\t}\n\n\treturn {\n\t\tkind: \"valid\",\n\t\tpolicy: { workspace, allowedHosts: options.allowedHosts ?? [], additionalWritableRoots },\n\t};\n}\n\n/**\n * Resolve one writable root, or explain why it cannot be one.\n *\n * Every root goes through the same check for the same reason the workspace always did: a\n * backend must never silently broaden an ambiguous path into host authority, and an extra\n * root named on the command line is no less capable of doing that than the first one.\n */\nfunction validateRoot(candidate: string, label: string): string | { kind: \"invalid\"; reason: string } {\n\tif (!isAbsolute(candidate)) {\n\t\treturn { kind: \"invalid\", reason: `Sandbox ${label} must be an absolute path: ${candidate}` };\n\t}\n\tif (!existsSync(candidate)) {\n\t\treturn { kind: \"invalid\", reason: `Sandbox ${label} does not exist: ${candidate}` };\n\t}\n\tif (!statSync(candidate).isDirectory()) {\n\t\treturn { kind: \"invalid\", reason: `Sandbox ${label} is not a directory: ${candidate}` };\n\t}\n\treturn realpathSync(candidate);\n}\n\n/** Throws rather than returning an executable fallback when OS enforcement is absent. */\nexport function requireSandboxEnforcement(status: SandboxStatus): void {\n\tif (status.kind === \"enforced\") return;\n\tthrow new SandboxUnavailableError(status.reason);\n}\n"]}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A named, saved combination of what the OS boundary permits.
|
|
3
|
-
*
|
|
4
|
-
* Apex already has a permission rule model with five modes and eight-source precedence
|
|
5
|
-
* (ADR 0004). That governs the tool gate at `beforeToolCall`, inside the child. This
|
|
6
|
-
* governs the supervisor's launch contract, outside it. They are deliberately separate
|
|
7
|
-
* surfaces, and conflating them is exactly the misreading that makes `bypassPermissions`
|
|
8
|
-
* look like a sandbox escape -- so a profile here carries only boundary inputs and has no
|
|
9
|
-
* way to express a tool-gate mode.
|
|
10
|
-
*
|
|
11
|
-
* It also has no way to express "no boundary". That decision stays on the command line
|
|
12
|
-
* where a human has to type it, per ADR 0005's 2026-08-28 amendment; a profile can widen
|
|
13
|
-
* what is reachable and writable, never remove the boundary that constrains them.
|
|
14
|
-
*
|
|
15
|
-
* Resolved from global settings only, with `projectTrusted: false`, for the reason ADR
|
|
16
|
-
* 0016 gives for every supervisor policy input: a repository that could define the
|
|
17
|
-
* boundary it runs under would be granting itself authority.
|
|
18
|
-
*/
|
|
19
|
-
export interface SandboxProfile {
|
|
20
|
-
readonly allowedHosts: readonly string[];
|
|
21
|
-
readonly additionalWritableRoots: readonly string[];
|
|
22
|
-
}
|
|
23
|
-
/** Resolve one named profile, or undefined when the name is absent or was never given. */
|
|
24
|
-
export declare function resolveSandboxProfile(name: string | undefined, cwd: string, agentDir: string): SandboxProfile | undefined;
|
|
25
|
-
//# sourceMappingURL=profiles.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/profiles.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,uBAAuB,EAAE,SAAS,MAAM,EAAE,CAAC;CACpD;AAMD,0FAA0F;AAC1F,wBAAgB,qBAAqB,CACpC,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,GACd,cAAc,GAAG,SAAS,CAc5B","sourcesContent":["import { SettingsManager } from \"../settings-manager.ts\";\n\n/**\n * A named, saved combination of what the OS boundary permits.\n *\n * Apex already has a permission rule model with five modes and eight-source precedence\n * (ADR 0004). That governs the tool gate at `beforeToolCall`, inside the child. This\n * governs the supervisor's launch contract, outside it. They are deliberately separate\n * surfaces, and conflating them is exactly the misreading that makes `bypassPermissions`\n * look like a sandbox escape -- so a profile here carries only boundary inputs and has no\n * way to express a tool-gate mode.\n *\n * It also has no way to express \"no boundary\". That decision stays on the command line\n * where a human has to type it, per ADR 0005's 2026-08-28 amendment; a profile can widen\n * what is reachable and writable, never remove the boundary that constrains them.\n *\n * Resolved from global settings only, with `projectTrusted: false`, for the reason ADR\n * 0016 gives for every supervisor policy input: a repository that could define the\n * boundary it runs under would be granting itself authority.\n */\nexport interface SandboxProfile {\n\treadonly allowedHosts: readonly string[];\n\treadonly additionalWritableRoots: readonly string[];\n}\n\nfunction stringArray(value: unknown): string[] {\n\treturn Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === \"string\") : [];\n}\n\n/** Resolve one named profile, or undefined when the name is absent or was never given. */\nexport function resolveSandboxProfile(\n\tname: string | undefined,\n\tcwd: string,\n\tagentDir: string,\n): SandboxProfile | undefined {\n\tif (!name) return undefined;\n\tconst settings = SettingsManager.create(cwd, agentDir, { projectTrusted: false });\n\tconst profiles = settings.getSandboxProfiles();\n\tconst profile = profiles?.[name];\n\tif (!profile || typeof profile !== \"object\") return undefined;\n\tconst record = profile as Record<string, unknown>;\n\t// Only these two keys are read. An unknown key in a profile is ignored rather than\n\t// rejected, so a profile written for a later version does not break this one, and a\n\t// key this version does not understand can never take effect by accident.\n\treturn {\n\t\tallowedHosts: stringArray(record.allowedHosts),\n\t\tadditionalWritableRoots: stringArray(record.additionalWritableRoots),\n\t};\n}\n"]}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { SettingsManager } from "../settings-manager.js";
|
|
2
|
-
function stringArray(value) {
|
|
3
|
-
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
4
|
-
}
|
|
5
|
-
/** Resolve one named profile, or undefined when the name is absent or was never given. */
|
|
6
|
-
export function resolveSandboxProfile(name, cwd, agentDir) {
|
|
7
|
-
if (!name)
|
|
8
|
-
return undefined;
|
|
9
|
-
const settings = SettingsManager.create(cwd, agentDir, { projectTrusted: false });
|
|
10
|
-
const profiles = settings.getSandboxProfiles();
|
|
11
|
-
const profile = profiles?.[name];
|
|
12
|
-
if (!profile || typeof profile !== "object")
|
|
13
|
-
return undefined;
|
|
14
|
-
const record = profile;
|
|
15
|
-
// Only these two keys are read. An unknown key in a profile is ignored rather than
|
|
16
|
-
// rejected, so a profile written for a later version does not break this one, and a
|
|
17
|
-
// key this version does not understand can never take effect by accident.
|
|
18
|
-
return {
|
|
19
|
-
allowedHosts: stringArray(record.allowedHosts),
|
|
20
|
-
additionalWritableRoots: stringArray(record.additionalWritableRoots),
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
//# sourceMappingURL=profiles.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"profiles.js","sourceRoot":"","sources":["../../../src/core/sandbox/profiles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAyBzD,SAAS,WAAW,CAAC,KAAc,EAAY;IAC9C,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAmB,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAAA,CACvG;AAED,0FAA0F;AAC1F,MAAM,UAAU,qBAAqB,CACpC,IAAwB,EACxB,GAAW,EACX,QAAgB,EACa;IAC7B,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5B,MAAM,QAAQ,GAAG,eAAe,CAAC,MAAM,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,CAAC;IAClF,MAAM,QAAQ,GAAG,QAAQ,CAAC,kBAAkB,EAAE,CAAC;IAC/C,MAAM,OAAO,GAAG,QAAQ,EAAE,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC9D,MAAM,MAAM,GAAG,OAAkC,CAAC;IAClD,mFAAmF;IACnF,oFAAoF;IACpF,0EAA0E;IAC1E,OAAO;QACN,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,YAAY,CAAC;QAC9C,uBAAuB,EAAE,WAAW,CAAC,MAAM,CAAC,uBAAuB,CAAC;KACpE,CAAC;AAAA,CACF","sourcesContent":["import { SettingsManager } from \"../settings-manager.ts\";\n\n/**\n * A named, saved combination of what the OS boundary permits.\n *\n * Apex already has a permission rule model with five modes and eight-source precedence\n * (ADR 0004). That governs the tool gate at `beforeToolCall`, inside the child. This\n * governs the supervisor's launch contract, outside it. They are deliberately separate\n * surfaces, and conflating them is exactly the misreading that makes `bypassPermissions`\n * look like a sandbox escape -- so a profile here carries only boundary inputs and has no\n * way to express a tool-gate mode.\n *\n * It also has no way to express \"no boundary\". That decision stays on the command line\n * where a human has to type it, per ADR 0005's 2026-08-28 amendment; a profile can widen\n * what is reachable and writable, never remove the boundary that constrains them.\n *\n * Resolved from global settings only, with `projectTrusted: false`, for the reason ADR\n * 0016 gives for every supervisor policy input: a repository that could define the\n * boundary it runs under would be granting itself authority.\n */\nexport interface SandboxProfile {\n\treadonly allowedHosts: readonly string[];\n\treadonly additionalWritableRoots: readonly string[];\n}\n\nfunction stringArray(value: unknown): string[] {\n\treturn Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === \"string\") : [];\n}\n\n/** Resolve one named profile, or undefined when the name is absent or was never given. */\nexport function resolveSandboxProfile(\n\tname: string | undefined,\n\tcwd: string,\n\tagentDir: string,\n): SandboxProfile | undefined {\n\tif (!name) return undefined;\n\tconst settings = SettingsManager.create(cwd, agentDir, { projectTrusted: false });\n\tconst profiles = settings.getSandboxProfiles();\n\tconst profile = profiles?.[name];\n\tif (!profile || typeof profile !== \"object\") return undefined;\n\tconst record = profile as Record<string, unknown>;\n\t// Only these two keys are read. An unknown key in a profile is ignored rather than\n\t// rejected, so a profile written for a later version does not break this one, and a\n\t// key this version does not understand can never take effect by accident.\n\treturn {\n\t\tallowedHosts: stringArray(record.allowedHosts),\n\t\tadditionalWritableRoots: stringArray(record.additionalWritableRoots),\n\t};\n}\n"]}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/** The absolute path a sandbox filesystem refusal named, if the output named one. */
|
|
2
|
-
export declare function extractRefusedPath(output: string): string | undefined;
|
|
3
|
-
/** True when the output looks like the boundary refused a write rather than the command failing. */
|
|
4
|
-
export declare function looksLikeSandboxRefusal(output: string): boolean;
|
|
5
|
-
/**
|
|
6
|
-
* The directory an escalation should make writable for a refused path.
|
|
7
|
-
*
|
|
8
|
-
* The parent, not the file: the command was creating something that does not exist, and a
|
|
9
|
-
* bind of a non-existent path is not a mount bwrap can make.
|
|
10
|
-
*/
|
|
11
|
-
export declare function escalationRootFor(refusedPath: string): string;
|
|
12
|
-
export interface EscalationOutcome {
|
|
13
|
-
readonly code: number;
|
|
14
|
-
readonly stdout: string;
|
|
15
|
-
readonly stderr: string;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Ask the supervisor to run one command with one extra writable root.
|
|
19
|
-
*
|
|
20
|
-
* Resolves undefined for every refusal and every failure alike, because the caller's
|
|
21
|
-
* behaviour is identical in both cases: report the original error. Only a command that
|
|
22
|
-
* actually ran returns anything.
|
|
23
|
-
*/
|
|
24
|
-
export declare function requestCommandEscalation(request: {
|
|
25
|
-
command: string;
|
|
26
|
-
writableRoot: string;
|
|
27
|
-
}, socketPath?: string | undefined): Promise<EscalationOutcome | undefined>;
|
|
28
|
-
//# sourceMappingURL=command-client.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"command-client.d.ts","sourceRoot":"","sources":["../../../../src/core/sandbox/rpc/command-client.ts"],"names":[],"mappings":"AAsBA,qFAAqF;AACrF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOrE;AAED,oGAAoG;AACpG,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACvC,OAAO,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,EAClD,UAAU,qBAAkD,GAC1D,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAgCxC","sourcesContent":["import * as net from \"node:net\";\nimport { dirname, isAbsolute } from \"node:path\";\nimport { COMMAND_ESCALATION_SOCKET_VARIABLE } from \"./command-proxy.ts\";\n\n/**\n * The child's side of per-command escalation.\n *\n * The path this extracts is a guess, and it is allowed to be, because nothing safe rests\n * on it. The supervisor shows the human the exact command and the exact root before\n * anything runs, so a bad guess is refused by someone reading it rather than silently\n * granted. What the guess buys is not having to make the user work out which directory to\n * name; when it finds nothing, no escalation is offered and the command fails as it did\n * before.\n */\n\n/** The wordings the kernel and the shell actually produce for a mount refusal. */\nconst REFUSAL_PATTERNS: readonly RegExp[] = [\n\t/cannot (?:create|touch|remove|open) ['\"]?([^'\":]+)['\"]?: (?:Read-only file system|Permission denied)/i,\n\t/^[^:]*: ?([/][^\\s:]+): (?:Operation not permitted|Permission denied|Read-only file system)/im,\n\t/(?:EROFS|EACCES|EPERM)[^'\"]*['\"]([/][^'\"]+)['\"]/,\n];\n\n/** The absolute path a sandbox filesystem refusal named, if the output named one. */\nexport function extractRefusedPath(output: string): string | undefined {\n\tfor (const pattern of REFUSAL_PATTERNS) {\n\t\tconst match = pattern.exec(output);\n\t\tconst candidate = match?.[1]?.trim();\n\t\tif (candidate && isAbsolute(candidate)) return candidate;\n\t}\n\treturn undefined;\n}\n\n/** True when the output looks like the boundary refused a write rather than the command failing. */\nexport function looksLikeSandboxRefusal(output: string): boolean {\n\treturn /Read-only file system|Operation not permitted|Permission denied/i.test(output);\n}\n\n/**\n * The directory an escalation should make writable for a refused path.\n *\n * The parent, not the file: the command was creating something that does not exist, and a\n * bind of a non-existent path is not a mount bwrap can make.\n */\nexport function escalationRootFor(refusedPath: string): string {\n\treturn dirname(refusedPath);\n}\n\nexport interface EscalationOutcome {\n\treadonly code: number;\n\treadonly stdout: string;\n\treadonly stderr: string;\n}\n\n/**\n * Ask the supervisor to run one command with one extra writable root.\n *\n * Resolves undefined for every refusal and every failure alike, because the caller's\n * behaviour is identical in both cases: report the original error. Only a command that\n * actually ran returns anything.\n */\nexport function requestCommandEscalation(\n\trequest: { command: string; writableRoot: string },\n\tsocketPath = process.env[COMMAND_ESCALATION_SOCKET_VARIABLE],\n): Promise<EscalationOutcome | undefined> {\n\tif (!socketPath) return Promise.resolve(undefined);\n\treturn new Promise((resolve) => {\n\t\tconst socket = net.connect(socketPath);\n\t\tlet buffer = \"\";\n\t\tconst giveUp = () => {\n\t\t\tsocket.destroy();\n\t\t\tresolve(undefined);\n\t\t};\n\t\tsocket.on(\"connect\", () => socket.write(`${JSON.stringify({ op: \"run\", ...request })}\\n`));\n\t\tsocket.on(\"data\", (chunk) => {\n\t\t\tbuffer += chunk.toString();\n\t\t\tconst newline = buffer.indexOf(\"\\n\");\n\t\t\tif (newline < 0) return;\n\t\t\tsocket.destroy();\n\t\t\ttry {\n\t\t\t\tconst response = JSON.parse(buffer.slice(0, newline)) as Record<string, unknown>;\n\t\t\t\tif (response.ok === true && typeof response.code === \"number\") {\n\t\t\t\t\tresolve({\n\t\t\t\t\t\tcode: response.code,\n\t\t\t\t\t\tstdout: typeof response.stdout === \"string\" ? response.stdout : \"\",\n\t\t\t\t\t\tstderr: typeof response.stderr === \"string\" ? response.stderr : \"\",\n\t\t\t\t\t});\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Fall through to the same undefined a refusal produces.\n\t\t\t}\n\t\t\tresolve(undefined);\n\t\t});\n\t\tsocket.on(\"error\", giveUp);\n\t});\n}\n"]}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import * as net from "node:net";
|
|
2
|
-
import { dirname, isAbsolute } from "node:path";
|
|
3
|
-
import { COMMAND_ESCALATION_SOCKET_VARIABLE } from "./command-proxy.js";
|
|
4
|
-
/**
|
|
5
|
-
* The child's side of per-command escalation.
|
|
6
|
-
*
|
|
7
|
-
* The path this extracts is a guess, and it is allowed to be, because nothing safe rests
|
|
8
|
-
* on it. The supervisor shows the human the exact command and the exact root before
|
|
9
|
-
* anything runs, so a bad guess is refused by someone reading it rather than silently
|
|
10
|
-
* granted. What the guess buys is not having to make the user work out which directory to
|
|
11
|
-
* name; when it finds nothing, no escalation is offered and the command fails as it did
|
|
12
|
-
* before.
|
|
13
|
-
*/
|
|
14
|
-
/** The wordings the kernel and the shell actually produce for a mount refusal. */
|
|
15
|
-
const REFUSAL_PATTERNS = [
|
|
16
|
-
/cannot (?:create|touch|remove|open) ['"]?([^'":]+)['"]?: (?:Read-only file system|Permission denied)/i,
|
|
17
|
-
/^[^:]*: ?([/][^\s:]+): (?:Operation not permitted|Permission denied|Read-only file system)/im,
|
|
18
|
-
/(?:EROFS|EACCES|EPERM)[^'"]*['"]([/][^'"]+)['"]/,
|
|
19
|
-
];
|
|
20
|
-
/** The absolute path a sandbox filesystem refusal named, if the output named one. */
|
|
21
|
-
export function extractRefusedPath(output) {
|
|
22
|
-
for (const pattern of REFUSAL_PATTERNS) {
|
|
23
|
-
const match = pattern.exec(output);
|
|
24
|
-
const candidate = match?.[1]?.trim();
|
|
25
|
-
if (candidate && isAbsolute(candidate))
|
|
26
|
-
return candidate;
|
|
27
|
-
}
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
/** True when the output looks like the boundary refused a write rather than the command failing. */
|
|
31
|
-
export function looksLikeSandboxRefusal(output) {
|
|
32
|
-
return /Read-only file system|Operation not permitted|Permission denied/i.test(output);
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* The directory an escalation should make writable for a refused path.
|
|
36
|
-
*
|
|
37
|
-
* The parent, not the file: the command was creating something that does not exist, and a
|
|
38
|
-
* bind of a non-existent path is not a mount bwrap can make.
|
|
39
|
-
*/
|
|
40
|
-
export function escalationRootFor(refusedPath) {
|
|
41
|
-
return dirname(refusedPath);
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Ask the supervisor to run one command with one extra writable root.
|
|
45
|
-
*
|
|
46
|
-
* Resolves undefined for every refusal and every failure alike, because the caller's
|
|
47
|
-
* behaviour is identical in both cases: report the original error. Only a command that
|
|
48
|
-
* actually ran returns anything.
|
|
49
|
-
*/
|
|
50
|
-
export function requestCommandEscalation(request, socketPath = process.env[COMMAND_ESCALATION_SOCKET_VARIABLE]) {
|
|
51
|
-
if (!socketPath)
|
|
52
|
-
return Promise.resolve(undefined);
|
|
53
|
-
return new Promise((resolve) => {
|
|
54
|
-
const socket = net.connect(socketPath);
|
|
55
|
-
let buffer = "";
|
|
56
|
-
const giveUp = () => {
|
|
57
|
-
socket.destroy();
|
|
58
|
-
resolve(undefined);
|
|
59
|
-
};
|
|
60
|
-
socket.on("connect", () => socket.write(`${JSON.stringify({ op: "run", ...request })}\n`));
|
|
61
|
-
socket.on("data", (chunk) => {
|
|
62
|
-
buffer += chunk.toString();
|
|
63
|
-
const newline = buffer.indexOf("\n");
|
|
64
|
-
if (newline < 0)
|
|
65
|
-
return;
|
|
66
|
-
socket.destroy();
|
|
67
|
-
try {
|
|
68
|
-
const response = JSON.parse(buffer.slice(0, newline));
|
|
69
|
-
if (response.ok === true && typeof response.code === "number") {
|
|
70
|
-
resolve({
|
|
71
|
-
code: response.code,
|
|
72
|
-
stdout: typeof response.stdout === "string" ? response.stdout : "",
|
|
73
|
-
stderr: typeof response.stderr === "string" ? response.stderr : "",
|
|
74
|
-
});
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
// Fall through to the same undefined a refusal produces.
|
|
80
|
-
}
|
|
81
|
-
resolve(undefined);
|
|
82
|
-
});
|
|
83
|
-
socket.on("error", giveUp);
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
//# sourceMappingURL=command-client.js.map
|