@kafca/agentdock 0.1.61 → 0.1.62
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/README.md +18 -0
- package/dist/renderer/assets/AutomationList-BgJgAdVO.js +16 -0
- package/dist/renderer/assets/{Badge-BLI4LHse.js → Badge-Bq2hPVcR.js} +1 -1
- package/dist/renderer/assets/{Card-DJeLDdL3.js → Card-BPJNSZce.js} +1 -1
- package/dist/renderer/assets/{Config-D1awc81A.js → Config-aSFqlI88.js} +2 -2
- package/dist/renderer/assets/Dashboard-D3ezFmb7.js +36 -0
- package/dist/renderer/assets/{EmptyState-CkI1RwPX.js → EmptyState-DK496gBq.js} +1 -1
- package/dist/renderer/assets/{HighlightedMarkdown-BUhDlhgW.js → HighlightedMarkdown-Ckre5jNv.js} +1 -1
- package/dist/renderer/assets/{Input-DHYrn-Ne.js → Input-C5C1d0ba.js} +1 -1
- package/dist/renderer/assets/{KnowledgeDetail-Bij-Xo90.js → KnowledgeDetail-N8Ey1ikc.js} +2 -2
- package/dist/renderer/assets/{KnowledgeHome-DmPZ2IlI.js → KnowledgeHome-9aF8nJEf.js} +2 -2
- package/dist/renderer/assets/{Logs-ISY99heA.js → Logs-sgAdwEeS.js} +2 -2
- package/dist/renderer/assets/{Modal-DAQfz0ot.js → Modal-BB10hTRc.js} +3 -3
- package/dist/renderer/assets/{Page-Dw43UYDT.js → Page-DfC319Ck.js} +1 -1
- package/dist/renderer/assets/{Select-DzmN5OGQ.js → Select-RHJBmNER.js} +1 -1
- package/dist/renderer/assets/{ThreadChat-B5CWqqMK.js → ThreadChat-BU1zCdr4.js} +3 -3
- package/dist/renderer/assets/{Workspace-C5g9LBj9.js → Workspace-BoZCz7S8.js} +1 -1
- package/dist/renderer/assets/{arrow-left-BLXh56Ip.js → arrow-left-D2V1eZSD.js} +1 -1
- package/dist/renderer/assets/{book-open-DhPGbc-7.js → book-open-DUcBn7Bb.js} +1 -1
- package/dist/renderer/assets/{channels-DfQrmuYO.js → channels-DMhDSlf2.js} +2 -2
- package/dist/renderer/assets/{chevron-down-CEFtpPDc.js → chevron-down-Ca2FQDB7.js} +1 -1
- package/dist/renderer/assets/en-Bnr4nMzg.js +1 -0
- package/dist/renderer/assets/es-B1GIFh3i.js +1 -0
- package/dist/renderer/assets/{index-BIKL3fTP.css → index-B6WcpkJF.css} +1 -1
- package/dist/renderer/assets/{index-CYhDUAR_.js → index-BlnvlekJ.js} +1 -1
- package/dist/renderer/assets/index-DRJ5wgw_.js +162 -0
- package/dist/renderer/assets/ja-CDWPWqEW.js +1 -0
- package/dist/renderer/assets/{knowledge-uHRQ80sR.js → knowledge-D1-k56eY.js} +1 -1
- package/dist/renderer/assets/{pencil-BD2Y31YC.js → pencil-BhWdv_86.js} +1 -1
- package/dist/renderer/assets/{plus-DDcldp_c.js → plus-BacLMlK3.js} +1 -1
- package/dist/renderer/assets/{save-CMW_cZI9.js → save-Bpoxbzau.js} +1 -1
- package/dist/renderer/assets/{search-BM-0CvJr.js → search-BVJcpdFX.js} +1 -1
- package/dist/renderer/assets/{shield-check-DaMB0FKG.js → shield-check-C2cw_Ip6.js} +1 -1
- package/dist/renderer/assets/{threads-DZgt9tWj.js → threads-DOsBQBg_.js} +1 -1
- package/dist/renderer/assets/{trash-2-DTXVbC5y.js → trash-2-Bs3rEm3g.js} +1 -1
- package/dist/renderer/assets/zh-MSddxMDX.js +1 -0
- package/dist/renderer/assets/zh-TW-C_LNWX0o.js +1 -0
- package/dist/renderer/index.html +2 -2
- package/dist-electron/electron/managed-skills/agent-browser/SKILL.md +41 -0
- package/dist-electron/electron/managed-skills/agent-browser/references/authentication.md +83 -0
- package/dist-electron/electron/managed-skills/agent-browser/references/commands.md +50 -0
- package/dist-electron/electron/managed-skills/agent-browser/references/profiling.md +20 -0
- package/dist-electron/electron/managed-skills/agent-browser/references/proxy-support.md +18 -0
- package/dist-electron/electron/managed-skills/agent-browser/references/session-management.md +17 -0
- package/dist-electron/electron/managed-skills/agent-browser/references/snapshot-refs.md +17 -0
- package/dist-electron/electron/managed-skills/agent-browser/references/video-recording.md +21 -0
- package/dist-electron/electron/managed-skills/agent-browser/templates/authenticated-session.sh +11 -0
- package/dist-electron/electron/managed-skills/agent-browser/templates/capture-workflow.sh +12 -0
- package/dist-electron/electron/managed-skills/agent-browser/templates/form-automation.sh +10 -0
- package/dist-electron/electron/managed-skills/condition-trigger/SKILL.md +21 -0
- package/dist-electron/electron/managed-skills/condition-trigger/scripts/register-condition-trigger.sh +106 -0
- package/dist-electron/electron/managed-skills/knowledge-base/SKILL.md +13 -0
- package/dist-electron/electron/managed-skills/knowledge-base/scripts/search-knowledge.sh +80 -0
- package/dist-electron/packages/contracts/src/automations.js +211 -0
- package/dist-electron/packages/contracts/src/index.js +1 -0
- package/dist-electron/packages/core-sdk/src/automations.js +87 -0
- package/dist-electron/packages/core-sdk/src/client.js +101 -0
- package/dist-electron/packages/core-sdk/src/index.js +2 -1
- package/dist-electron/services/local-ai-core/src/acp/store/automation-script-store.js +415 -0
- package/dist-electron/services/local-ai-core/src/acp/store/automation-store.js +829 -0
- package/dist-electron/services/local-ai-core/src/acp/store/local-core-acp-store.js +158 -0
- package/dist-electron/services/local-ai-core/src/acp/store/schema.js +81 -0
- package/dist-electron/services/local-ai-core/src/automation/automation-action-executor.js +144 -0
- package/dist-electron/services/local-ai-core/src/automation/automation-condition-engine.js +79 -0
- package/dist-electron/services/local-ai-core/src/automation/automation-conversation-executor.js +39 -73
- package/dist-electron/services/local-ai-core/src/automation/automation-monitor-service.js +695 -260
- package/dist-electron/services/local-ai-core/src/automation/automation-script-service.js +507 -0
- package/dist-electron/services/local-ai-core/src/automation/automation-service.js +1019 -0
- package/dist-electron/services/local-ai-core/src/automation/automation-trigger-engine.js +141 -0
- package/dist-electron/services/local-ai-core/src/automation/condition-evaluator.js +60 -17
- package/dist-electron/services/local-ai-core/src/automation/legacy-automation-mappers.js +286 -0
- package/dist-electron/services/local-ai-core/src/automation/scripts/anthropic-sandbox-runner.js +792 -0
- package/dist-electron/services/local-ai-core/src/automation/scripts/sandbox-runner.js +2 -0
- package/dist-electron/services/local-ai-core/src/automation/scripts/script-package.js +475 -0
- package/dist-electron/services/local-ai-core/src/automation/scripts/script-protocol-runner.js +266 -0
- package/dist-electron/services/local-ai-core/src/automation/scripts/secret-resolver.js +37 -0
- package/dist-electron/services/local-ai-core/src/channel/weixin/inbound-poller.js +1 -2
- package/dist-electron/services/local-ai-core/src/channel/weixin/transport.js +9 -8
- package/dist-electron/services/local-ai-core/src/cli/lac.js +229 -0
- package/dist-electron/services/local-ai-core/src/kernel/bootstrap.js +21 -0
- package/dist-electron/services/local-ai-core/src/runtime/deployment-diagnostics.js +76 -0
- package/dist-electron/services/local-ai-core/src/runtime/handlers/automations-handler.js +322 -0
- package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +20 -0
- package/dist-electron/services/local-ai-core/src/runtime/managed-skill-catalog.js +37 -0
- package/dist-electron/services/local-ai-core/src/runtime/server-helpers.js +14 -4
- package/dist-electron/services/local-ai-core/src/runtime/server-routes.js +75 -0
- package/dist-electron/services/local-ai-core/src/runtime/server.js +21 -0
- package/dist-electron/services/local-ai-core/src/runtime/standalone.js +1 -0
- package/dist-electron/services/local-ai-core/src/scheduler/cron.js +151 -39
- package/dist-electron/services/local-ai-core/src/scheduler/scheduled-conversation-executor.js +2 -4
- package/dist-electron/services/local-ai-core/src/scheduler/scheduled-job-application-service.js +64 -12
- package/dist-electron/services/local-ai-core/src/scheduler/scheduler-service.js +71 -4
- package/dist-electron/services/local-ai-core/src/scheduler/thread-resolution.js +10 -0
- package/dist-electron/services/local-ai-core/src/thread/agent-message-policy.js +16 -1
- package/dist-electron/src/pages/Automation/automation-page-model.js +76 -0
- package/dist-electron/tests/contracts/agent-message-policy.test.js +32 -0
- package/dist-electron/tests/contracts/architecture-docs.test.js +30 -0
- package/dist-electron/tests/contracts/automation-contracts.test.js +154 -0
- package/dist-electron/tests/contracts/automation-renderer-model.test.js +47 -0
- package/dist-electron/tests/electron/automation-action-executor.test.js +75 -0
- package/dist-electron/tests/electron/automation-compatibility.test.js +463 -0
- package/dist-electron/tests/electron/automation-deployment.test.js +66 -0
- package/dist-electron/tests/electron/automation-engine.test.js +264 -0
- package/dist-electron/tests/electron/automation-monitor.test.js +1333 -0
- package/dist-electron/tests/electron/automation-sandbox-runner.test.js +333 -0
- package/dist-electron/tests/electron/automation-script-approval.test.js +553 -0
- package/dist-electron/tests/electron/automation-script-package.test.js +499 -0
- package/dist-electron/tests/electron/automation-script-runner.test.js +120 -0
- package/dist-electron/tests/electron/automation-service.test.js +1078 -0
- package/dist-electron/tests/electron/automation-store.test.js +593 -0
- package/dist-electron/tests/electron/condition-trigger-skill.test.js +93 -0
- package/dist-electron/tests/electron/core-client.test.js +99 -0
- package/dist-electron/tests/electron/lac-cli.test.js +110 -0
- package/dist-electron/tests/electron/workspace-task-store.test.js +25 -0
- package/dist-electron/tests/integration/automation-routes.test.js +186 -0
- package/dist-electron/tests/integration/automation-sandbox-runtime.test.js +92 -0
- package/package.json +4 -3
- package/dist/renderer/assets/CronList-CupZoNwW.js +0 -1
- package/dist/renderer/assets/Dashboard-nLZnOtcs.js +0 -41
- package/dist/renderer/assets/MonitorList-BlX82rXF.js +0 -1
- package/dist/renderer/assets/en-CmAr_0e6.js +0 -1
- package/dist/renderer/assets/es-Cs9x52ca.js +0 -1
- package/dist/renderer/assets/index-Dd00oqgY.js +0 -167
- package/dist/renderer/assets/ja-DtvtBRTW.js +0 -1
- package/dist/renderer/assets/play-DBJ8wsSy.js +0 -6
- package/dist/renderer/assets/zh-CcJ9vBZj.js +0 -1
- package/dist/renderer/assets/zh-TW-elI72AC1.js +0 -1
package/dist-electron/services/local-ai-core/src/automation/scripts/anthropic-sandbox-runner.js
ADDED
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isPrivateNetworkAddress = exports.AnthropicSandboxRunner = exports.buildSandboxConfig = exports.SandboxUnavailableError = exports.PRIVATE_NETWORK_DENY_HOSTS = void 0;
|
|
4
|
+
exports.buildSandboxRuntimeConfig = buildSandboxRuntimeConfig;
|
|
5
|
+
exports.createAnthropicSandboxRunner = createAnthropicSandboxRunner;
|
|
6
|
+
const node_child_process_1 = require("node:child_process");
|
|
7
|
+
const promises_1 = require("node:dns/promises");
|
|
8
|
+
const node_fs_1 = require("node:fs");
|
|
9
|
+
const node_net_1 = require("node:net");
|
|
10
|
+
const node_path_1 = require("node:path");
|
|
11
|
+
const node_os_1 = require("node:os");
|
|
12
|
+
const node_crypto_1 = require("node:crypto");
|
|
13
|
+
/**
|
|
14
|
+
* Explicit hostnames are kept in the runtime deny list because Sandbox Runtime
|
|
15
|
+
* evaluates its allow/deny rules before its request hook. The request hook
|
|
16
|
+
* below additionally rejects all RFC1918/link-local/loopback IPs, including
|
|
17
|
+
* addresses not represented by these finite host rules.
|
|
18
|
+
*/
|
|
19
|
+
exports.PRIVATE_NETWORK_DENY_HOSTS = [
|
|
20
|
+
'localhost',
|
|
21
|
+
'localhost.localdomain',
|
|
22
|
+
'ip6-localhost',
|
|
23
|
+
'ip6-loopback',
|
|
24
|
+
'127.0.0.1',
|
|
25
|
+
'127.0.0.2',
|
|
26
|
+
'127.255.255.255',
|
|
27
|
+
'0.0.0.0',
|
|
28
|
+
'::1',
|
|
29
|
+
'[::1]',
|
|
30
|
+
'169.254.169.254',
|
|
31
|
+
'169.254.170.2',
|
|
32
|
+
'10.0.0.1',
|
|
33
|
+
'10.255.255.254',
|
|
34
|
+
'172.16.0.1',
|
|
35
|
+
'172.31.255.254',
|
|
36
|
+
'192.168.0.1',
|
|
37
|
+
'192.168.255.254',
|
|
38
|
+
'100.64.0.1',
|
|
39
|
+
'100.127.255.254',
|
|
40
|
+
'192.0.0.1',
|
|
41
|
+
'192.0.2.1',
|
|
42
|
+
'198.18.0.1',
|
|
43
|
+
'198.51.100.1',
|
|
44
|
+
'203.0.113.1',
|
|
45
|
+
'metadata.google.internal',
|
|
46
|
+
];
|
|
47
|
+
const CAPABILITY_ORDER = ['sandbox_unavailable', 'bwrap', 'socat', 'rg', 'seccomp', 'network.namespace', 'sandbox_runtime', 'apparmor.userns'];
|
|
48
|
+
const DEFAULT_WRITE_DENY_PATHS = [
|
|
49
|
+
'/tmp/claude',
|
|
50
|
+
'/private/tmp/claude',
|
|
51
|
+
(0, node_path_1.join)((0, node_os_1.homedir)(), '.npm/_logs'),
|
|
52
|
+
(0, node_path_1.join)((0, node_os_1.homedir)(), '.claude/debug'),
|
|
53
|
+
];
|
|
54
|
+
const CONTROLLED_TEMP_ROOT = (0, node_path_1.join)((0, node_os_1.tmpdir)(), 'agentdock-automation-runs');
|
|
55
|
+
const MAX_NETWORK_AUDIT_ENTRIES = 100;
|
|
56
|
+
let managerOperation = Promise.resolve();
|
|
57
|
+
async function withManagerLock(operation) {
|
|
58
|
+
const previous = managerOperation;
|
|
59
|
+
let release;
|
|
60
|
+
managerOperation = new Promise((resolveRelease) => { release = resolveRelease; });
|
|
61
|
+
await previous;
|
|
62
|
+
try {
|
|
63
|
+
return await operation();
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
release();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
class SandboxUnavailableError extends Error {
|
|
70
|
+
missing;
|
|
71
|
+
code = 'sandbox_unavailable';
|
|
72
|
+
constructor(missing) {
|
|
73
|
+
super(`sandbox_unavailable${missing.length > 0 ? `: ${missing.join(', ')}` : ''}`);
|
|
74
|
+
this.missing = missing;
|
|
75
|
+
this.name = 'SandboxUnavailableError';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
exports.SandboxUnavailableError = SandboxUnavailableError;
|
|
79
|
+
/**
|
|
80
|
+
* Build the native runtime policy from one script invocation. Keeping this
|
|
81
|
+
* pure makes the security policy testable without starting proxy processes.
|
|
82
|
+
*/
|
|
83
|
+
function buildSandboxRuntimeConfig(input) {
|
|
84
|
+
const cwd = input.cwd || input.workspacePath || process.cwd();
|
|
85
|
+
const packagePath = absolutePath(input.packagePath || input.packageRoot || cwd);
|
|
86
|
+
const tempRoot = absolutePath(input.tempRoot || CONTROLLED_TEMP_ROOT);
|
|
87
|
+
assertSafeTempRoot(tempRoot);
|
|
88
|
+
const tempPath = absolutePath(input.tempDir || input.tempPath || input.temporaryDirectory || (0, node_path_1.join)(tempRoot, 'run'));
|
|
89
|
+
assertContainedPath(tempPath, tempRoot, 'tempDir');
|
|
90
|
+
const mode = resolveNetworkMode(input);
|
|
91
|
+
const manifestCapabilities = input.manifest?.capabilities;
|
|
92
|
+
const allowedReadDirs = uniqueAbsolutePaths([
|
|
93
|
+
...(input.allowedReadDirs || []),
|
|
94
|
+
...(manifestCapabilities?.allowedReadDirs || []),
|
|
95
|
+
]);
|
|
96
|
+
const explicitDenyRead = uniqueAbsolutePaths(input.denyRead || []);
|
|
97
|
+
const interpreterPath = input.interpreterPath ? absolutePath(input.interpreterPath) : process.execPath;
|
|
98
|
+
const allowRead = uniqueAbsolutePaths([packagePath, tempPath, interpreterPath, (0, node_path_1.dirname)(interpreterPath), ...allowedReadDirs])
|
|
99
|
+
.filter((path) => !explicitDenyRead.some((deny) => path === deny || path.startsWith(`${deny}${node_path_1.sep}`) || deny.startsWith(`${path}${node_path_1.sep}`)));
|
|
100
|
+
const systemTemp = (0, node_path_1.resolve)((0, node_os_1.tmpdir)());
|
|
101
|
+
const broadReadDeny = [cwd, (0, node_os_1.homedir)(), (0, node_path_1.dirname)(packagePath), ...(input.userDataPath ? [absolutePath(input.userDataPath)] : [])]
|
|
102
|
+
.filter((path) => path !== node_path_1.sep && path !== systemTemp && !systemTemp.startsWith(`${path}${node_path_1.sep}`));
|
|
103
|
+
if ((0, node_path_1.dirname)(cwd) !== node_path_1.sep && (0, node_path_1.dirname)(cwd) !== systemTemp && !systemTemp.startsWith(`${(0, node_path_1.dirname)(cwd)}${node_path_1.sep}`))
|
|
104
|
+
broadReadDeny.push((0, node_path_1.dirname)(cwd));
|
|
105
|
+
const denyRead = uniqueAbsolutePaths([
|
|
106
|
+
// ASRT uses deny-then-allow semantics. Keep workspace/user data denied,
|
|
107
|
+
// then carve out only the immutable package and declared read roots.
|
|
108
|
+
...broadReadDeny,
|
|
109
|
+
...explicitDenyRead,
|
|
110
|
+
]);
|
|
111
|
+
// Keep every request on the callback path. A non-empty ASRT allowlist would
|
|
112
|
+
// short-circuit the callback for matching hosts and reintroduce DNS/private
|
|
113
|
+
// destination bypasses.
|
|
114
|
+
const allowedDomains = [];
|
|
115
|
+
const deniedDomains = mode === 'none'
|
|
116
|
+
? ['*']
|
|
117
|
+
: [...exports.PRIVATE_NETWORK_DENY_HOSTS];
|
|
118
|
+
return {
|
|
119
|
+
network: {
|
|
120
|
+
// Public mode intentionally leaves the allowlist empty and delegates
|
|
121
|
+
// each host to the ask callback. This keeps numeric private addresses
|
|
122
|
+
// and DNS aliases on the same callback path as HTTPS CONNECT/SOCKS.
|
|
123
|
+
allowedDomains,
|
|
124
|
+
deniedDomains: [...deniedDomains],
|
|
125
|
+
strictAllowlist: mode === 'none',
|
|
126
|
+
allowLocalBinding: false,
|
|
127
|
+
allowUnixSockets: [],
|
|
128
|
+
filterRequest: async (request) => {
|
|
129
|
+
const host = new URL(request.url).hostname;
|
|
130
|
+
if (await isPrivateOrLocalHost(host)) {
|
|
131
|
+
return { action: 'deny', reason: 'private or localhost egress is not permitted' };
|
|
132
|
+
}
|
|
133
|
+
return { action: 'allow' };
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
filesystem: {
|
|
137
|
+
// The staged package is readable but never writable. `allowWrite` is
|
|
138
|
+
// intentionally a singleton: no cwd/package/config writes are allowed.
|
|
139
|
+
denyRead,
|
|
140
|
+
allowRead,
|
|
141
|
+
allowWrite: [tempPath],
|
|
142
|
+
denyWrite: [packagePath, ...DEFAULT_WRITE_DENY_PATHS],
|
|
143
|
+
allowGitConfig: false,
|
|
144
|
+
},
|
|
145
|
+
allowPty: false,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/** Alias kept small and discoverable for callers that name the policy a sandbox config. */
|
|
149
|
+
exports.buildSandboxConfig = buildSandboxRuntimeConfig;
|
|
150
|
+
class AnthropicSandboxRunner {
|
|
151
|
+
platform;
|
|
152
|
+
commandExists;
|
|
153
|
+
managerOverride;
|
|
154
|
+
appArmorUsernsRestricted;
|
|
155
|
+
probeInitialization;
|
|
156
|
+
tempRoot;
|
|
157
|
+
managerPromise;
|
|
158
|
+
probePromise;
|
|
159
|
+
initialized = false;
|
|
160
|
+
constructor(options = {}) {
|
|
161
|
+
this.platform = normalizePlatform(options.platform || process.platform);
|
|
162
|
+
this.commandExists = options.commandExists || defaultCommandExists;
|
|
163
|
+
this.managerOverride = options.manager;
|
|
164
|
+
this.appArmorUsernsRestricted = options.appArmorUsernsRestricted;
|
|
165
|
+
this.probeInitialization = options.probeInitialization !== false;
|
|
166
|
+
this.tempRoot = absolutePath(options.tempRoot || CONTROLLED_TEMP_ROOT);
|
|
167
|
+
assertSafeTempRoot(this.tempRoot);
|
|
168
|
+
(0, node_fs_1.mkdirSync)(this.tempRoot, { recursive: true });
|
|
169
|
+
}
|
|
170
|
+
async probe() {
|
|
171
|
+
if (!this.probePromise) {
|
|
172
|
+
this.probePromise = withManagerLock(() => this.probeInternal()).then((result) => {
|
|
173
|
+
// Do not permanently cache a transient initialization/reset failure;
|
|
174
|
+
// a later diagnostics/run call must be able to recover.
|
|
175
|
+
if (!result.available)
|
|
176
|
+
this.probePromise = undefined;
|
|
177
|
+
return result;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return this.probePromise;
|
|
181
|
+
}
|
|
182
|
+
async run(input) {
|
|
183
|
+
const capability = await this.probe();
|
|
184
|
+
if (!capability.available)
|
|
185
|
+
throw new SandboxUnavailableError(capability.missing);
|
|
186
|
+
return withManagerLock(() => this.runLocked(input));
|
|
187
|
+
}
|
|
188
|
+
async runLocked(input) {
|
|
189
|
+
const suppliedTempPath = input.tempDir || input.tempPath || input.temporaryDirectory;
|
|
190
|
+
const tempPath = absolutePath(suppliedTempPath || (0, node_path_1.join)(this.tempRoot, `run-${(0, node_crypto_1.randomUUID)()}`));
|
|
191
|
+
const ownsTempPath = !suppliedTempPath;
|
|
192
|
+
assertContainedPath(tempPath, this.tempRoot, 'tempDir');
|
|
193
|
+
(0, node_fs_1.mkdirSync)(tempPath, { recursive: true });
|
|
194
|
+
const effectiveInput = { ...input, tempDir: tempPath, tempRoot: this.tempRoot };
|
|
195
|
+
const manager = await this.getManager();
|
|
196
|
+
const config = buildSandboxRuntimeConfig(effectiveInput);
|
|
197
|
+
const networkAudit = [];
|
|
198
|
+
// SandboxManager is a process-global singleton. Always tear down any
|
|
199
|
+
// previous owner before applying this invocation's filesystem policy.
|
|
200
|
+
try {
|
|
201
|
+
await manager.reset?.();
|
|
202
|
+
const mode = resolveNetworkMode(effectiveInput);
|
|
203
|
+
const allowedDomains = effectiveInput.allowedDomains ?? effectiveInput.manifest?.capabilities?.allowedDomains ?? [];
|
|
204
|
+
await manager.initialize(config, async ({ host, port }) => {
|
|
205
|
+
const allowed = mode !== 'none' && !(await isPrivateOrLocalHost(host))
|
|
206
|
+
&& (mode !== 'restricted' || allowedDomains.some((pattern) => matchesDomain(host, pattern)));
|
|
207
|
+
if (networkAudit.length < MAX_NETWORK_AUDIT_ENTRIES) {
|
|
208
|
+
networkAudit.push({ host: sanitizeAuditHost(host), ...(port === undefined ? {} : { port }), allowed, timestamp: new Date().toISOString() });
|
|
209
|
+
}
|
|
210
|
+
return allowed;
|
|
211
|
+
}, false);
|
|
212
|
+
this.initialized = true;
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
this.initialized = false;
|
|
216
|
+
if (ownsTempPath)
|
|
217
|
+
(0, node_fs_1.rmSync)(tempPath, { recursive: true, force: true });
|
|
218
|
+
throw new SandboxUnavailableError(['sandbox_runtime']);
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
if (manager.waitForNetworkInitialization) {
|
|
222
|
+
const ready = await manager.waitForNetworkInitialization();
|
|
223
|
+
if (!ready)
|
|
224
|
+
throw new SandboxUnavailableError(['sandbox_runtime']);
|
|
225
|
+
}
|
|
226
|
+
// The wrapped command is deliberately scoped to this method. It is
|
|
227
|
+
// never returned, logged, or attached to SandboxRunResult.
|
|
228
|
+
if (!manager.wrapWithSandboxArgv)
|
|
229
|
+
throw new SandboxUnavailableError(['sandbox_runtime']);
|
|
230
|
+
const tempEnvKeys = ['TMPDIR', 'TMP', 'TEMP', 'CLAUDE_CODE_TMPDIR', 'CLAUDE_TMPDIR'];
|
|
231
|
+
const previousTempEnv = Object.fromEntries(tempEnvKeys.map((key) => [key, process.env[key]]));
|
|
232
|
+
for (const key of tempEnvKeys)
|
|
233
|
+
process.env[key] = tempPath;
|
|
234
|
+
let wrapped;
|
|
235
|
+
try {
|
|
236
|
+
wrapped = await manager.wrapWithSandboxArgv(effectiveInput.command);
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
for (const key of tempEnvKeys) {
|
|
240
|
+
if (previousTempEnv[key] === undefined)
|
|
241
|
+
delete process.env[key];
|
|
242
|
+
else
|
|
243
|
+
process.env[key] = previousTempEnv[key];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const result = await spawnWrappedCommand(wrapped, effectiveInput);
|
|
247
|
+
return { ...result, ...(networkAudit.length === 0 ? {} : { networkAudit }) };
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
manager.cleanupAfterCommand?.();
|
|
251
|
+
try {
|
|
252
|
+
await manager.reset?.();
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
this.initialized = false;
|
|
256
|
+
if (ownsTempPath)
|
|
257
|
+
(0, node_fs_1.rmSync)(tempPath, { recursive: true, force: true });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async probeInternal() {
|
|
262
|
+
const missing = new Set();
|
|
263
|
+
const unverified = new Set();
|
|
264
|
+
if (this.platform === 'windows') {
|
|
265
|
+
return { available: false, platform: this.platform, missing: ['sandbox_unavailable'] };
|
|
266
|
+
}
|
|
267
|
+
const manager = await this.getManager().catch(() => undefined);
|
|
268
|
+
if (!manager)
|
|
269
|
+
missing.add('sandbox_runtime');
|
|
270
|
+
if (manager?.isSupportedPlatform && !manager.isSupportedPlatform())
|
|
271
|
+
missing.add('sandbox_unavailable');
|
|
272
|
+
if (this.platform === 'linux') {
|
|
273
|
+
try {
|
|
274
|
+
const deps = manager?.checkDependencies?.() || { errors: [], warnings: [] };
|
|
275
|
+
addDependencyNames(missing, [...deps.errors, ...deps.warnings]);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
missing.add('sandbox_runtime');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
if (!this.commandExists('sandbox-exec'))
|
|
283
|
+
missing.add('sandbox-exec');
|
|
284
|
+
if (!this.commandExists('rg'))
|
|
285
|
+
missing.add('rg');
|
|
286
|
+
}
|
|
287
|
+
const behaviorBlockers = this.platform === 'linux'
|
|
288
|
+
? ['sandbox_runtime', 'sandbox_unavailable', 'bwrap', 'socat', 'rg']
|
|
289
|
+
: ['sandbox_runtime', 'sandbox_unavailable', 'sandbox-exec'];
|
|
290
|
+
const behaviorBlocked = behaviorBlockers.some((capability) => missing.has(capability));
|
|
291
|
+
const markLinuxBehaviorUnverified = () => {
|
|
292
|
+
// An earlier failure prevents the only authoritative behavior proof.
|
|
293
|
+
// Keep every unproven isolation capability failed rather than reporting
|
|
294
|
+
// a misleading green row from absence of evidence.
|
|
295
|
+
missing.add('apparmor.userns');
|
|
296
|
+
missing.add('network.namespace');
|
|
297
|
+
missing.add('seccomp');
|
|
298
|
+
unverified.add('apparmor.userns');
|
|
299
|
+
unverified.add('network.namespace');
|
|
300
|
+
unverified.add('seccomp');
|
|
301
|
+
};
|
|
302
|
+
if (this.platform === 'linux' && behaviorBlocked)
|
|
303
|
+
markLinuxBehaviorUnverified();
|
|
304
|
+
if (!this.probeInitialization) {
|
|
305
|
+
missing.add('sandbox_runtime');
|
|
306
|
+
if (this.platform === 'linux')
|
|
307
|
+
markLinuxBehaviorUnverified();
|
|
308
|
+
}
|
|
309
|
+
if (manager && this.probeInitialization && !behaviorBlocked) {
|
|
310
|
+
let attempted = false;
|
|
311
|
+
const probeTempPath = (0, node_path_1.join)(this.tempRoot, `probe-${(0, node_crypto_1.randomUUID)()}`);
|
|
312
|
+
try {
|
|
313
|
+
(0, node_fs_1.mkdirSync)(probeTempPath, { recursive: true });
|
|
314
|
+
const probeConfig = buildSandboxRuntimeConfig({
|
|
315
|
+
command: 'true',
|
|
316
|
+
cwd: process.cwd(),
|
|
317
|
+
packagePath: process.cwd(),
|
|
318
|
+
tempRoot: this.tempRoot,
|
|
319
|
+
tempDir: probeTempPath,
|
|
320
|
+
network: 'none',
|
|
321
|
+
});
|
|
322
|
+
attempted = true;
|
|
323
|
+
await manager.initialize(probeConfig);
|
|
324
|
+
if (manager.waitForNetworkInitialization && !(await manager.waitForNetworkInitialization())) {
|
|
325
|
+
missing.add('sandbox_runtime');
|
|
326
|
+
if (this.platform === 'linux')
|
|
327
|
+
markLinuxBehaviorUnverified();
|
|
328
|
+
}
|
|
329
|
+
else if (!manager.wrapWithSandboxArgv) {
|
|
330
|
+
missing.add('sandbox_runtime');
|
|
331
|
+
if (this.platform === 'linux')
|
|
332
|
+
markLinuxBehaviorUnverified();
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
const wrapped = await manager.wrapWithSandboxArgv('true');
|
|
336
|
+
let result;
|
|
337
|
+
try {
|
|
338
|
+
result = await spawnWrappedCommand(wrapped, {
|
|
339
|
+
command: 'true',
|
|
340
|
+
cwd: process.cwd(),
|
|
341
|
+
packagePath: process.cwd(),
|
|
342
|
+
tempRoot: this.tempRoot,
|
|
343
|
+
tempDir: probeTempPath,
|
|
344
|
+
network: 'none',
|
|
345
|
+
timeoutMs: 5_000,
|
|
346
|
+
stdoutBytes: 16_384,
|
|
347
|
+
stderrBytes: 16_384,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
finally {
|
|
351
|
+
manager.cleanupAfterCommand?.();
|
|
352
|
+
}
|
|
353
|
+
if (result.exitCode !== 0 || result.signal) {
|
|
354
|
+
if (this.platform === 'linux') {
|
|
355
|
+
const failure = classifyLinuxBehaviorFailures(result.stderr, this.isAppArmorUsernsRestricted());
|
|
356
|
+
for (const capability of failure.missing) {
|
|
357
|
+
missing.add(capability);
|
|
358
|
+
}
|
|
359
|
+
for (const capability of failure.unverified)
|
|
360
|
+
unverified.add(capability);
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
missing.add('sandbox_runtime');
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
370
|
+
if (this.platform === 'linux') {
|
|
371
|
+
const failure = classifyLinuxBehaviorFailures(message, this.isAppArmorUsernsRestricted());
|
|
372
|
+
for (const capability of failure.missing) {
|
|
373
|
+
missing.add(capability);
|
|
374
|
+
}
|
|
375
|
+
for (const capability of failure.unverified)
|
|
376
|
+
unverified.add(capability);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
missing.add('sandbox_runtime');
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
if (attempted) {
|
|
384
|
+
try {
|
|
385
|
+
await manager.reset?.();
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
missing.add('sandbox_runtime');
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
(0, node_fs_1.rmSync)(probeTempPath, { recursive: true, force: true });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const orderedUnverified = orderCapabilities(unverified);
|
|
395
|
+
return {
|
|
396
|
+
available: missing.size === 0,
|
|
397
|
+
platform: this.platform,
|
|
398
|
+
missing: orderCapabilities(missing),
|
|
399
|
+
...(orderedUnverified.length === 0 ? {} : { unverified: orderedUnverified }),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
isAppArmorUsernsRestricted() {
|
|
403
|
+
if (typeof this.appArmorUsernsRestricted === 'function')
|
|
404
|
+
return this.appArmorUsernsRestricted();
|
|
405
|
+
if (typeof this.appArmorUsernsRestricted === 'boolean')
|
|
406
|
+
return this.appArmorUsernsRestricted;
|
|
407
|
+
try {
|
|
408
|
+
// This value is diagnostic context only. A dedicated AppArmor profile
|
|
409
|
+
// can grant userns while the global restriction remains enabled, so the
|
|
410
|
+
// wrapped behavioral probe above is always authoritative.
|
|
411
|
+
return (0, node_fs_1.readFileSync)('/proc/sys/kernel/apparmor_restrict_unprivileged_userns', 'utf8').trim() === '1';
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
async getManager() {
|
|
418
|
+
if (this.managerOverride)
|
|
419
|
+
return this.managerOverride;
|
|
420
|
+
if (!this.managerPromise) {
|
|
421
|
+
this.managerPromise = importSandboxRuntimeManager();
|
|
422
|
+
}
|
|
423
|
+
return this.managerPromise;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
exports.AnthropicSandboxRunner = AnthropicSandboxRunner;
|
|
427
|
+
function createAnthropicSandboxRunner(options = {}) {
|
|
428
|
+
return new AnthropicSandboxRunner(options);
|
|
429
|
+
}
|
|
430
|
+
async function importSandboxRuntimeManager() {
|
|
431
|
+
// TypeScript emits CommonJS for Electron. A native dynamic import through
|
|
432
|
+
// Function avoids transpiling this call to require(), since ASRT is ESM.
|
|
433
|
+
const dynamicImport = new Function('specifier', 'return import(specifier)');
|
|
434
|
+
const module = await dynamicImport('@anthropic-ai/sandbox-runtime');
|
|
435
|
+
return module.SandboxManager;
|
|
436
|
+
}
|
|
437
|
+
function resolveNetworkMode(input) {
|
|
438
|
+
return input.networkMode || input.network || input.manifest?.capabilities?.network || 'none';
|
|
439
|
+
}
|
|
440
|
+
function normalizePlatform(value) {
|
|
441
|
+
if (value === 'darwin' || value === 'macos')
|
|
442
|
+
return 'macos';
|
|
443
|
+
if (value === 'win32' || value === 'windows')
|
|
444
|
+
return 'windows';
|
|
445
|
+
if (value === 'linux')
|
|
446
|
+
return 'linux';
|
|
447
|
+
return 'windows';
|
|
448
|
+
}
|
|
449
|
+
function absolutePath(path) {
|
|
450
|
+
if (!(0, node_path_1.isAbsolute)(path))
|
|
451
|
+
throw new Error('Sandbox paths must be absolute.');
|
|
452
|
+
return (0, node_path_1.resolve)(path);
|
|
453
|
+
}
|
|
454
|
+
function assertContainedPath(candidate, root, label) {
|
|
455
|
+
const candidatePath = (0, node_path_1.resolve)(candidate);
|
|
456
|
+
const rootPath = (0, node_path_1.resolve)(root);
|
|
457
|
+
const relativePath = (0, node_path_1.relative)(rootPath, candidatePath);
|
|
458
|
+
if (relativePath === '..' || relativePath.startsWith(`..${node_path_1.sep}`) || (0, node_path_1.isAbsolute)(relativePath)) {
|
|
459
|
+
throw new Error(`${label} must be inside the controlled temporary root.`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function assertSafeTempRoot(root) {
|
|
463
|
+
const normalized = (0, node_path_1.resolve)(root);
|
|
464
|
+
if (normalized === node_path_1.sep || normalized === (0, node_path_1.resolve)((0, node_os_1.tmpdir)()) || normalized === (0, node_path_1.resolve)((0, node_os_1.homedir)())) {
|
|
465
|
+
throw new Error('tempRoot must be a dedicated, non-broad temporary directory.');
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function uniqueAbsolutePaths(paths) {
|
|
469
|
+
return [...new Set(paths.filter(Boolean).map(absolutePath))];
|
|
470
|
+
}
|
|
471
|
+
function addDependencyNames(missing, errors) {
|
|
472
|
+
for (const error of errors) {
|
|
473
|
+
const lower = error.toLowerCase();
|
|
474
|
+
if (lower.includes('bwrap') || lower.includes('bubblewrap'))
|
|
475
|
+
missing.add('bwrap');
|
|
476
|
+
if (lower.includes('socat'))
|
|
477
|
+
missing.add('socat');
|
|
478
|
+
if (lower.includes('ripgrep') || /\brg\b/.test(lower))
|
|
479
|
+
missing.add('rg');
|
|
480
|
+
if (lower.includes('seccomp'))
|
|
481
|
+
missing.add('seccomp');
|
|
482
|
+
if (lower.includes('network namespace') || lower.includes('unshare-net'))
|
|
483
|
+
missing.add('network.namespace');
|
|
484
|
+
if (lower.includes('unsupported platform'))
|
|
485
|
+
missing.add('sandbox_unavailable');
|
|
486
|
+
if (lower.includes('sandbox'))
|
|
487
|
+
missing.add('sandbox_runtime');
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function classifyLinuxBehaviorFailures(message, appArmorUsernsRestricted) {
|
|
491
|
+
const lower = message.toLowerCase();
|
|
492
|
+
if (lower.includes('seccomp') || lower.includes('apply-seccomp')) {
|
|
493
|
+
return { missing: ['seccomp'], unverified: [] };
|
|
494
|
+
}
|
|
495
|
+
if (lower.includes('unshare-net') || lower.includes('network namespace')) {
|
|
496
|
+
return { missing: ['network.namespace', 'seccomp'], unverified: ['seccomp'] };
|
|
497
|
+
}
|
|
498
|
+
if (lower.includes('apparmor')) {
|
|
499
|
+
return {
|
|
500
|
+
missing: ['apparmor.userns', 'network.namespace', 'seccomp'],
|
|
501
|
+
unverified: ['network.namespace', 'seccomp'],
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
if (appArmorUsernsRestricted &&
|
|
505
|
+
(lower.includes('new namespace') || lower.includes('user namespace') || lower.includes('operation not permitted'))) {
|
|
506
|
+
return {
|
|
507
|
+
missing: ['apparmor.userns', 'network.namespace', 'seccomp'],
|
|
508
|
+
unverified: ['network.namespace', 'seccomp'],
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
if (lower.includes('new namespace') || lower.includes('user namespace') || lower.includes('operation not permitted')) {
|
|
512
|
+
return {
|
|
513
|
+
missing: ['apparmor.userns', 'network.namespace', 'seccomp'],
|
|
514
|
+
unverified: ['network.namespace', 'seccomp'],
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
return {
|
|
518
|
+
missing: ['sandbox_runtime', 'apparmor.userns', 'network.namespace', 'seccomp'],
|
|
519
|
+
unverified: ['apparmor.userns', 'network.namespace', 'seccomp'],
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
function orderCapabilities(values) {
|
|
523
|
+
return [...values].sort((left, right) => {
|
|
524
|
+
const leftIndex = CAPABILITY_ORDER.indexOf(left);
|
|
525
|
+
const rightIndex = CAPABILITY_ORDER.indexOf(right);
|
|
526
|
+
return (leftIndex < 0 ? CAPABILITY_ORDER.length : leftIndex) - (rightIndex < 0 ? CAPABILITY_ORDER.length : rightIndex) || left.localeCompare(right);
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
function defaultCommandExists(command) {
|
|
530
|
+
const pathValue = process.env.PATH || '';
|
|
531
|
+
return pathValue.split(node_path_1.delimiter).some((directory) => (0, node_fs_1.existsSync)((0, node_path_1.join)(directory, command)));
|
|
532
|
+
}
|
|
533
|
+
async function spawnWrappedCommand(wrapped, input) {
|
|
534
|
+
if (wrapped.argv.length === 0 || !(0, node_path_1.isAbsolute)(wrapped.argv[0])) {
|
|
535
|
+
throw new SandboxUnavailableError(['sandbox_runtime']);
|
|
536
|
+
}
|
|
537
|
+
const runtimePath = wrapped.env.PATH || '/usr/bin:/bin';
|
|
538
|
+
const allowedEnv = input.allowedEnv ?? input.manifest?.env ?? [];
|
|
539
|
+
const inputEnv = Object.fromEntries(Object.entries(input.env || {}).filter(([key]) => allowedEnv.includes(key) && !isProxyEnvKey(key)));
|
|
540
|
+
const tempPath = absolutePath(input.tempDir || input.tempPath || input.temporaryDirectory || CONTROLLED_TEMP_ROOT);
|
|
541
|
+
const child = (0, node_child_process_1.spawn)(wrapped.argv[0], wrapped.argv.slice(1), {
|
|
542
|
+
cwd: input.cwd || input.workspacePath || process.cwd(),
|
|
543
|
+
env: {
|
|
544
|
+
...sanitizeRuntimeEnv(wrapped.env),
|
|
545
|
+
...sanitizeRuntimeEnv(inputEnv, allowedEnv),
|
|
546
|
+
PATH: runtimePath,
|
|
547
|
+
TMPDIR: tempPath,
|
|
548
|
+
TMP: tempPath,
|
|
549
|
+
TEMP: tempPath,
|
|
550
|
+
CLAUDE_CODE_TMPDIR: tempPath,
|
|
551
|
+
CLAUDE_TMPDIR: tempPath,
|
|
552
|
+
},
|
|
553
|
+
shell: false,
|
|
554
|
+
// A new process group lets timeout/overflow termination include shell
|
|
555
|
+
// descendants rather than leaving a detached child running in the sandbox.
|
|
556
|
+
detached: process.platform !== 'win32',
|
|
557
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
558
|
+
});
|
|
559
|
+
if (input.stdin !== undefined)
|
|
560
|
+
child.stdin?.end(input.stdin, 'utf8');
|
|
561
|
+
else
|
|
562
|
+
child.stdin?.end();
|
|
563
|
+
return collectChildResult(child, input.timeoutMs, input.signal, input);
|
|
564
|
+
}
|
|
565
|
+
function sanitizeRuntimeEnv(runtimeEnv, allowedDataKeys = []) {
|
|
566
|
+
const result = {};
|
|
567
|
+
const blocked = new Set(['BASH_ENV', 'ENV', 'SHELLOPTS', 'NODE_OPTIONS', 'LD_PRELOAD', 'LD_LIBRARY_PATH', 'DYLD_INSERT_LIBRARIES', 'DYLD_LIBRARY_PATH', 'PATH']);
|
|
568
|
+
for (const [key, value] of Object.entries(runtimeEnv)) {
|
|
569
|
+
if (value === undefined)
|
|
570
|
+
continue;
|
|
571
|
+
if (blocked.has(key))
|
|
572
|
+
continue;
|
|
573
|
+
if (allowedDataKeys.includes(key) && !blocked.has(key) && !isProxyEnvKey(key)) {
|
|
574
|
+
result[key] = value;
|
|
575
|
+
}
|
|
576
|
+
else if (key === 'PATH' || key === 'HOME' || key === 'USER' || key === 'SHELL' ||
|
|
577
|
+
key === 'LANG' || key.startsWith('LC_') || key.startsWith('HTTP_PROXY') ||
|
|
578
|
+
key.startsWith('HTTPS_PROXY') || key.startsWith('ALL_PROXY') ||
|
|
579
|
+
key.startsWith('http_proxy') || key.startsWith('https_proxy') || key.startsWith('all_proxy') ||
|
|
580
|
+
key === 'SSL_CERT_FILE' || key === 'GIT_SSL_CAINFO' || key === 'CURL_CA_BUNDLE' ||
|
|
581
|
+
key === 'REQUESTS_CA_BUNDLE' || key === 'PIP_CERT' || key === 'AWS_CA_BUNDLE' ||
|
|
582
|
+
key === 'CARGO_HTTP_CAINFO' || key === 'DENO_CERT' || key === 'CLOUDSDK_CORE_CUSTOM_CA_CERTS_FILE' ||
|
|
583
|
+
key === 'CLAUDE_CODE_TMPDIR' || key === 'CLAUDE_TMPDIR' ||
|
|
584
|
+
key === 'CLAUDE_CODE_HOST_HTTP_PROXY_PORT' || key === 'CLAUDE_CODE_HOST_SOCKS_PROXY_PORT') {
|
|
585
|
+
result[key] = value;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
// NO_PROXY is intentionally omitted: it would let a script bypass the
|
|
589
|
+
// mediated proxy and defeat public/private host policy.
|
|
590
|
+
delete result.NO_PROXY;
|
|
591
|
+
delete result.no_proxy;
|
|
592
|
+
return result;
|
|
593
|
+
}
|
|
594
|
+
function isProxyEnvKey(key) {
|
|
595
|
+
return key === 'HTTP_PROXY' || key === 'HTTPS_PROXY' || key === 'ALL_PROXY' ||
|
|
596
|
+
key === 'http_proxy' || key === 'https_proxy' || key === 'all_proxy' ||
|
|
597
|
+
key === 'NO_PROXY' || key === 'no_proxy';
|
|
598
|
+
}
|
|
599
|
+
function collectChildResult(child, timeoutMs = 30_000, signal, input) {
|
|
600
|
+
return new Promise((resolveResult) => {
|
|
601
|
+
let stdout = '';
|
|
602
|
+
let stderr = '';
|
|
603
|
+
let settled = false;
|
|
604
|
+
let outputLimitExceeded;
|
|
605
|
+
const terminateTree = () => {
|
|
606
|
+
if (child.pid && process.platform !== 'win32') {
|
|
607
|
+
try {
|
|
608
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
catch { /* child may have already exited */ }
|
|
612
|
+
}
|
|
613
|
+
child.kill('SIGKILL');
|
|
614
|
+
};
|
|
615
|
+
const timer = setTimeout(terminateTree, timeoutMs);
|
|
616
|
+
const abort = () => terminateTree();
|
|
617
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
618
|
+
const stdoutParts = [];
|
|
619
|
+
const stderrParts = [];
|
|
620
|
+
let stdoutSize = 0;
|
|
621
|
+
let stderrSize = 0;
|
|
622
|
+
const append = (stream, chunk) => {
|
|
623
|
+
const limit = stream === 'stdout' ? input?.stdoutBytes : input?.stderrBytes;
|
|
624
|
+
const current = stream === 'stdout' ? stdoutSize : stderrSize;
|
|
625
|
+
const remaining = limit === undefined ? undefined : limit - current;
|
|
626
|
+
if (remaining === undefined || remaining >= chunk.byteLength) {
|
|
627
|
+
if (stream === 'stdout') {
|
|
628
|
+
stdoutParts.push(chunk);
|
|
629
|
+
stdoutSize += chunk.byteLength;
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
stderrParts.push(chunk);
|
|
633
|
+
stderrSize += chunk.byteLength;
|
|
634
|
+
}
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const truncated = truncateUtf8(chunk, Math.max(0, remaining));
|
|
638
|
+
if (stream === 'stdout') {
|
|
639
|
+
stdoutParts.push(truncated);
|
|
640
|
+
stdoutSize += truncated.byteLength;
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
stderrParts.push(truncated);
|
|
644
|
+
stderrSize += truncated.byteLength;
|
|
645
|
+
}
|
|
646
|
+
if (!outputLimitExceeded) {
|
|
647
|
+
outputLimitExceeded = stream;
|
|
648
|
+
terminateTree();
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
child.stdout?.on('data', (chunk) => append('stdout', chunk));
|
|
652
|
+
child.stderr?.on('data', (chunk) => append('stderr', chunk));
|
|
653
|
+
const finish = (exitCode, childSignal) => {
|
|
654
|
+
if (settled)
|
|
655
|
+
return;
|
|
656
|
+
settled = true;
|
|
657
|
+
clearTimeout(timer);
|
|
658
|
+
signal?.removeEventListener('abort', abort);
|
|
659
|
+
stdout = Buffer.concat(stdoutParts).toString('utf8');
|
|
660
|
+
stderr = Buffer.concat(stderrParts).toString('utf8');
|
|
661
|
+
resolveResult({ exitCode, signal: childSignal, stdout, stderr, ...(outputLimitExceeded ? { outputLimitExceeded } : {}) });
|
|
662
|
+
};
|
|
663
|
+
child.once('error', (error) => {
|
|
664
|
+
stderrParts.push(Buffer.from(error instanceof Error ? error.message : String(error), 'utf8'));
|
|
665
|
+
finish(null, null);
|
|
666
|
+
});
|
|
667
|
+
child.once('exit', (exitCode, childSignal) => {
|
|
668
|
+
// Retain the normal stdout tail while streams close. If a background
|
|
669
|
+
// descendant inherited them, tear down the process group shortly after
|
|
670
|
+
// wrapper exit instead of allowing an orphan to keep the run open.
|
|
671
|
+
setTimeout(() => {
|
|
672
|
+
if (settled)
|
|
673
|
+
return;
|
|
674
|
+
let groupAlive = false;
|
|
675
|
+
if (child.pid && process.platform !== 'win32') {
|
|
676
|
+
try {
|
|
677
|
+
process.kill(-child.pid, 0);
|
|
678
|
+
groupAlive = true;
|
|
679
|
+
}
|
|
680
|
+
catch { /* group exited; let pipes drain */ }
|
|
681
|
+
}
|
|
682
|
+
if (groupAlive) {
|
|
683
|
+
terminateTree();
|
|
684
|
+
child.stdout?.destroy();
|
|
685
|
+
child.stderr?.destroy();
|
|
686
|
+
finish(exitCode, childSignal);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
// A closed group should normally emit close immediately. Retain tail
|
|
690
|
+
// output, but still bound a pathological inherited pipe hang.
|
|
691
|
+
setTimeout(() => {
|
|
692
|
+
if (settled)
|
|
693
|
+
return;
|
|
694
|
+
child.stdout?.destroy();
|
|
695
|
+
child.stderr?.destroy();
|
|
696
|
+
finish(exitCode, childSignal);
|
|
697
|
+
}, 1_000);
|
|
698
|
+
}, 50);
|
|
699
|
+
});
|
|
700
|
+
child.once('close', (exitCode, childSignal) => finish(exitCode, childSignal));
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
function truncateUtf8(value, limit) {
|
|
704
|
+
if (value.byteLength <= limit)
|
|
705
|
+
return value;
|
|
706
|
+
// Keep the longest valid UTF-8 prefix. Only the final code point can be
|
|
707
|
+
// partial, so four attempts cover UTF-8's maximum sequence width while
|
|
708
|
+
// preserving legitimate U+FFFD characters already present in the stream.
|
|
709
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
710
|
+
for (let end = limit; end >= Math.max(0, limit - 3); end -= 1) {
|
|
711
|
+
try {
|
|
712
|
+
decoder.decode(value.subarray(0, end));
|
|
713
|
+
return value.subarray(0, end);
|
|
714
|
+
}
|
|
715
|
+
catch { /* try a shorter final boundary */ }
|
|
716
|
+
}
|
|
717
|
+
return Buffer.alloc(0);
|
|
718
|
+
}
|
|
719
|
+
async function isPrivateOrLocalHost(host) {
|
|
720
|
+
const normalized = host.toLowerCase().replace(/^\[|\]$/g, '').replace(/\.$/, '');
|
|
721
|
+
if (exports.PRIVATE_NETWORK_DENY_HOSTS.some((entry) => entry.replace(/^\[|\]$/g, '').toLowerCase() === normalized))
|
|
722
|
+
return true;
|
|
723
|
+
const ipVersion = (0, node_net_1.isIP)(normalized);
|
|
724
|
+
if (ipVersion === 4 && isPrivateIpv4(normalized))
|
|
725
|
+
return true;
|
|
726
|
+
if (ipVersion === 6 && isPrivateIpv6(normalized))
|
|
727
|
+
return true;
|
|
728
|
+
if (ipVersion === 0) {
|
|
729
|
+
try {
|
|
730
|
+
const addresses = await (0, promises_1.lookup)(normalized, { all: true, verbatim: true });
|
|
731
|
+
// 198.18/15 is a reserved benchmark range commonly used by local
|
|
732
|
+
// network shims for public DNS answers. Direct numeric destinations in
|
|
733
|
+
// that range remain denied; DNS aliases still deny RFC1918/link-local.
|
|
734
|
+
return addresses.length === 0 || addresses.some((address) => address.family === 4 ? isPrivateIpv4(address.address, false) : isPrivateIpv6(address.address));
|
|
735
|
+
}
|
|
736
|
+
catch {
|
|
737
|
+
// A transient DNS failure is reported by the proxy and does not turn
|
|
738
|
+
// into host-side execution: fail closed on resolver errors.
|
|
739
|
+
return true;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
/** Deterministic policy seam used by capability/network tests. */
|
|
745
|
+
exports.isPrivateNetworkAddress = isPrivateOrLocalHost;
|
|
746
|
+
function matchesDomain(host, pattern) {
|
|
747
|
+
const normalizedHost = host.toLowerCase().replace(/\.$/, '');
|
|
748
|
+
const normalizedPattern = pattern.toLowerCase().replace(/\.$/, '');
|
|
749
|
+
return normalizedPattern === '*' || normalizedPattern === normalizedHost ||
|
|
750
|
+
(normalizedPattern.startsWith('*.') && normalizedHost.endsWith(`.${normalizedPattern.slice(2)}`));
|
|
751
|
+
}
|
|
752
|
+
function sanitizeAuditHost(value) {
|
|
753
|
+
const host = value.toLowerCase().replace(/^\[|\]$/g, '').replace(/\.$/, '');
|
|
754
|
+
return /^[a-z0-9.:-]+$/i.test(host) && !host.includes('..') ? host : 'invalid-host';
|
|
755
|
+
}
|
|
756
|
+
function isPrivateIpv4(address, includeBenchmarkRange = true) {
|
|
757
|
+
const parts = address.split('.').map(Number);
|
|
758
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255))
|
|
759
|
+
return true;
|
|
760
|
+
const value = (((parts[0] * 256 + parts[1]) * 256 + parts[2]) * 256 + parts[3]);
|
|
761
|
+
const first16 = value >>> 16;
|
|
762
|
+
return parts[0] === 10 ||
|
|
763
|
+
(parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) ||
|
|
764
|
+
(parts[0] === 192 && parts[1] === 168) ||
|
|
765
|
+
parts[0] === 127 ||
|
|
766
|
+
(parts[0] === 169 && parts[1] === 254) ||
|
|
767
|
+
(parts[0] === 0) ||
|
|
768
|
+
(first16 >= 0x6440 && first16 <= 0x647f) ||
|
|
769
|
+
(parts[0] === 192 && parts[1] === 0) ||
|
|
770
|
+
(parts[0] === 198 && parts[1] === 51 && parts[2] === 100) ||
|
|
771
|
+
(parts[0] === 203 && parts[1] === 0 && parts[2] === 113) ||
|
|
772
|
+
(includeBenchmarkRange && first16 >= 0xc612 && first16 <= 0xc613);
|
|
773
|
+
}
|
|
774
|
+
function isPrivateIpv6(address) {
|
|
775
|
+
const normalized = address.toLowerCase();
|
|
776
|
+
if (normalized.startsWith('::ffff:')) {
|
|
777
|
+
const mapped = normalized.slice('::ffff:'.length);
|
|
778
|
+
if ((0, node_net_1.isIP)(mapped) === 4)
|
|
779
|
+
return isPrivateIpv4(mapped);
|
|
780
|
+
const groups = mapped.split(':');
|
|
781
|
+
if (groups.length >= 2) {
|
|
782
|
+
const high = Number.parseInt(groups[groups.length - 2], 16);
|
|
783
|
+
const low = Number.parseInt(groups[groups.length - 1], 16);
|
|
784
|
+
if (Number.isInteger(high) && Number.isInteger(low)) {
|
|
785
|
+
const dotted = `${high >>> 8}.${high & 0xff}.${low >>> 8}.${low & 0xff}`;
|
|
786
|
+
return isPrivateIpv4(dotted);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return true;
|
|
790
|
+
}
|
|
791
|
+
return normalized === '::1' || normalized === '::' || normalized.startsWith('fe8') || normalized.startsWith('fe9') || normalized.startsWith('fea') || normalized.startsWith('feb') || normalized.startsWith('fc') || normalized.startsWith('fd');
|
|
792
|
+
}
|