@claw-link/gateway-host 0.2.4 → 0.2.6
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 +17 -8
- package/SECURITY.md +16 -9
- package/bin/cli.js +8 -1
- package/package.json +1 -1
- package/scripts/install.js +92 -4
- package/src/adapters/base.js +19 -6
- package/src/adapters/claude.js +2 -1
- package/src/adapters/cli.js +4 -2
- package/src/adapters/codex.js +12 -3
- package/src/paths.js +22 -11
package/README.md
CHANGED
|
@@ -64,13 +64,14 @@ tool's own settings).
|
|
|
64
64
|
**Permissions by scope** — each job carries a scope, and the Claude adapter runs with
|
|
65
65
|
matching permissions automatically:
|
|
66
66
|
|
|
67
|
-
| Trigger | Scope | Claude
|
|
68
|
-
|
|
69
|
-
| Admin **Setup via Chat** / apply skill | `configure` | `--permission-mode acceptEdits`
|
|
70
|
-
| Customer chat + discovery | `customer` | `--disallowedTools "Write Edit MultiEdit NotebookEdit Bash"`
|
|
67
|
+
| Trigger | Scope | Claude | Codex |
|
|
68
|
+
|---------|-------|--------|-------|
|
|
69
|
+
| Admin **Setup via Chat** / apply skill | `configure` | `--permission-mode acceptEdits` (may write workspace files) | `--sandbox workspace-write --ask-for-approval never` |
|
|
70
|
+
| Customer chat + discovery | `customer` | `--disallowedTools "Write Edit MultiEdit NotebookEdit Bash"` (**read-only**) | `--sandbox read-only --ask-for-approval never` |
|
|
71
71
|
|
|
72
72
|
So an agent's files are editable from the admin panel but never from customer/discovery
|
|
73
|
-
chat. (Operator `args_template` overrides this for advanced setups.
|
|
73
|
+
chat. (Operator `args_template` overrides this for advanced setups. Hermes/Cursor/OpenClaw
|
|
74
|
+
manage their own permissions.)
|
|
74
75
|
|
|
75
76
|
## Commands
|
|
76
77
|
|
|
@@ -80,6 +81,7 @@ After install the `clhost` command is available (aliases: `gateway-host`, `claw-
|
|
|
80
81
|
clhost install # install the background service (one-time)
|
|
81
82
|
clhost add-agent # add an agent by pasting its Host Token
|
|
82
83
|
clhost rm-agent <id># remove a mapped agent
|
|
84
|
+
clhost retoken <id> # re-enter a new token after rotating (keeps workspace/binary)
|
|
83
85
|
clhost agents # list mapped agents
|
|
84
86
|
clhost status # bridge + service state + mapped agents
|
|
85
87
|
clhost start # start the background service
|
|
@@ -111,11 +113,18 @@ cached back in). `~/.clawlink-host/config.json` (chmod 600):
|
|
|
111
113
|
}
|
|
112
114
|
```
|
|
113
115
|
|
|
116
|
+
## Platforms
|
|
117
|
+
|
|
118
|
+
macOS (launchd), Linux (systemd user service), and **Windows** (a hidden, auto-restarting
|
|
119
|
+
Scheduled Task; logs to `%USERPROFILE%\.clawlink-host\host.log`). On Windows the worker
|
|
120
|
+
handles npm `.cmd` CLI shims automatically; for the strongest isolation prefer Claude
|
|
121
|
+
(prompt via stdin) or a native `.exe` runtime.
|
|
122
|
+
|
|
114
123
|
## Security
|
|
115
124
|
|
|
116
|
-
Outbound-only, per-agent token (stored server-side as a hash only), **machine-bound**
|
|
117
|
-
|
|
118
|
-
only the configured binary. See [SECURITY.md](./SECURITY.md).
|
|
125
|
+
Outbound-only, per-agent token (stored server-side as a hash only), **machine-bound** on
|
|
126
|
+
first connect to a **stable hardware id** (a stolen token fails from any other host), no
|
|
127
|
+
shell injection, runs only the configured binary. See [SECURITY.md](./SECURITY.md).
|
|
119
128
|
|
|
120
129
|
---
|
|
121
130
|
|
package/SECURITY.md
CHANGED
|
@@ -12,18 +12,25 @@ a secret, never run an attacker-controlled command, never open a port.
|
|
|
12
12
|
exactly one `agent_id`. Rotate any time from the dashboard (the old token dies
|
|
13
13
|
immediately).
|
|
14
14
|
2. **Machine binding** — on first connect the worker sends a **hash** of the host's
|
|
15
|
-
machine
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
**stable hardware machine id** (macOS `IOPlatformUUID`, Linux `/etc/machine-id`,
|
|
16
|
+
Windows `MachineGuid`; a persisted random id as fallback); ClawLink binds the agent
|
|
17
|
+
to that machine. Every later call must present the same id, so a stolen token is
|
|
18
|
+
rejected from any other host. Only the hash is sent — the raw id never leaves the
|
|
19
|
+
machine. The id is **stable across reboots, network changes, and version upgrades**
|
|
20
|
+
(earlier builds hashed MAC addresses, which rotate and caused false mismatches).
|
|
21
|
+
Rotating the token clears the binding (also how you move to a new host). Treat this
|
|
22
|
+
as a strong second factor on top of the token, not a replacement.
|
|
21
23
|
3. **Outbound-only** — the worker **dials out** to the `host-bridge` HTTPS endpoint
|
|
22
24
|
and short-polls for work. It opens **no inbound port**, needs **no Tailscale
|
|
23
25
|
Funnel**, and works behind NAT. There is nothing to attack from the network.
|
|
24
|
-
4. **No shell injection** — every CLI is launched with
|
|
25
|
-
argvArray, { shell: false })
|
|
26
|
-
token or via stdin —
|
|
26
|
+
4. **No shell injection** — on macOS/Linux every CLI is launched with
|
|
27
|
+
`child_process.spawn(binary, argvArray, { shell: false })`; untrusted prompt text is
|
|
28
|
+
a single argv token or comes via stdin — **never** concatenated into a command
|
|
29
|
+
string. On Windows, npm CLI shims are `.cmd`/`.bat` files that Node can only spawn
|
|
30
|
+
through the shell, so those go through `cmd.exe` with Node's hardened argument
|
|
31
|
+
escaping; native `.exe` runtimes still use `shell: false`. For Windows, prefer a
|
|
32
|
+
runtime that reads the prompt via **stdin** (Claude) or a native `.exe` so untrusted
|
|
33
|
+
text never enters a Windows command line.
|
|
27
34
|
5. **No arbitrary command execution** — the worker runs **only** the runtime binary
|
|
28
35
|
you configured. The job payload it receives carries `{ content, system_prompt,
|
|
29
36
|
attachments, session }` — it is structurally incapable of carrying a command, and
|
package/bin/cli.js
CHANGED
|
@@ -27,6 +27,11 @@ async function main() {
|
|
|
27
27
|
case 'rm':
|
|
28
28
|
return require('../scripts/install').removeAgentCmd();
|
|
29
29
|
|
|
30
|
+
case 'retoken':
|
|
31
|
+
case 're-token':
|
|
32
|
+
case 'update-token':
|
|
33
|
+
return require('../scripts/install').retokenCmd();
|
|
34
|
+
|
|
30
35
|
case 'agents':
|
|
31
36
|
case 'list':
|
|
32
37
|
return require('../scripts/install').listAgents();
|
|
@@ -48,7 +53,8 @@ async function main() {
|
|
|
48
53
|
'To rotate a Host Token (also how you move an agent to a new machine):',
|
|
49
54
|
' 1. ClawLink → Agents → your agent → Host Gateway → "Rotate token"',
|
|
50
55
|
' (this clears the machine binding so a new host can claim it).',
|
|
51
|
-
' 2. Copy the new token, then run: clhost
|
|
56
|
+
' 2. Copy the new token, then run: clhost retoken <id>',
|
|
57
|
+
' (keeps the agent\'s workspace/binary; or `clhost add-agent` for a new one).',
|
|
52
58
|
' The old token + old machine stop working immediately.',
|
|
53
59
|
].join('\n'));
|
|
54
60
|
return;
|
|
@@ -63,6 +69,7 @@ async function main() {
|
|
|
63
69
|
' install Install the background service (one-time)',
|
|
64
70
|
' add-agent Add an agent by pasting its Host Token (pulls runtime from ClawLink)',
|
|
65
71
|
' rm-agent Remove a mapped agent (clhost rm-agent <id>)',
|
|
72
|
+
' retoken Re-enter a new Host Token for an existing agent (clhost retoken <id>)',
|
|
66
73
|
' agents List mapped agents',
|
|
67
74
|
' status Show bridge, service state, and mapped agents',
|
|
68
75
|
' start Start the background service',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claw-link/gateway-host",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "ClawLink Host Gateway — a secure, outbound-only worker that bridges a local agent CLI (OpenClaw, Hermes, Claude, Codex, Cursor) to your ClawLink agents. No inbound ports; authenticated per-agent by a Host Token.",
|
|
5
5
|
"homepage": "https://claw-link.co",
|
|
6
6
|
"bin": {
|
package/scripts/install.js
CHANGED
|
@@ -175,11 +175,54 @@ function printAgents(cfg) {
|
|
|
175
175
|
const tok = a.host_token ? `…${String(a.host_token).slice(-4)}` : '—';
|
|
176
176
|
console.log(` • ${id}… runtime=${a.runtime || 'pull'} token=${tok}` + (a.work_dir ? ` work=${a.work_dir}` : ''));
|
|
177
177
|
}
|
|
178
|
+
console.log('\n Update a token after rotating it: clhost retoken <id>');
|
|
178
179
|
}
|
|
179
180
|
|
|
180
181
|
// `agents` — list mapped agents.
|
|
181
182
|
function listAgents() { printAgents(config.load() || {}); }
|
|
182
183
|
|
|
184
|
+
// `retoken [id-or-prefix]` — re-enter a new Host Token for an EXISTING agent after
|
|
185
|
+
// rotating it in the dashboard, keeping the same work_dir/binary/runtime. The new
|
|
186
|
+
// token must resolve to the same agent_id; re-binds this machine on verify.
|
|
187
|
+
async function retokenCmd() {
|
|
188
|
+
const cfg = config.load();
|
|
189
|
+
if (!cfg || !(cfg.agents || []).length) { console.log('No agents configured. Run: clhost add-agent'); return; }
|
|
190
|
+
ensureBridgeUrl(cfg);
|
|
191
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
192
|
+
try {
|
|
193
|
+
let target = (process.argv[3] || '').trim();
|
|
194
|
+
if (!target && cfg.agents.length === 1) target = cfg.agents[0].agent_id || '';
|
|
195
|
+
if (!target) { printAgents(cfg); target = await ask(rl, '\n Agent ID (or prefix) to re-token'); }
|
|
196
|
+
if (!target) { console.log(' (cancelled)'); return; }
|
|
197
|
+
const agent = cfg.agents.find((a) => a.agent_id && (a.agent_id === target || a.agent_id.startsWith(target)));
|
|
198
|
+
if (!agent) { console.log(` No agent matched '${target}'.`); return; }
|
|
199
|
+
|
|
200
|
+
const token = await ask(rl, ` New Host Token for ${String(agent.agent_id).slice(0, 8)}… (clk_host_…)`);
|
|
201
|
+
if (!token) { console.log(' (cancelled)'); return; }
|
|
202
|
+
|
|
203
|
+
console.log(' Verifying new token + binding this machine…');
|
|
204
|
+
let resp;
|
|
205
|
+
try {
|
|
206
|
+
resp = await new Bridge(cfg.bridge_url, { host_token: token }, cfg.instance_id).register(null, VERSION);
|
|
207
|
+
} catch (e) { console.log(` ✗ Could not verify: ${e.message}`); return; }
|
|
208
|
+
|
|
209
|
+
if (resp.agent_id && resp.agent_id !== agent.agent_id) {
|
|
210
|
+
console.log(` ✗ That token belongs to a different agent (${String(resp.agent_id).slice(0, 8)}…), not ${String(agent.agent_id).slice(0, 8)}….`);
|
|
211
|
+
console.log(' To map a different agent, use: clhost add-agent');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
agent.host_token = token;
|
|
215
|
+
if (resp.runtime && resp.runtime !== agent.runtime) {
|
|
216
|
+
console.log(` ℹ runtime changed: ${agent.runtime} → ${resp.runtime}`);
|
|
217
|
+
agent.runtime = resp.runtime;
|
|
218
|
+
}
|
|
219
|
+
config.save(cfg);
|
|
220
|
+
const restarted = serviceControl('restart');
|
|
221
|
+
console.log(`\n ✓ Updated token for ${agent.agent_id} (…${token.slice(-4)}).` +
|
|
222
|
+
(restarted ? ' Service restarted.' : ' Run `clhost restart` to apply.') + '\n');
|
|
223
|
+
} finally { rl.close(); }
|
|
224
|
+
}
|
|
225
|
+
|
|
183
226
|
// `start | stop | restart` — control the installed background service.
|
|
184
227
|
function serviceControl(action) {
|
|
185
228
|
const platform = process.platform;
|
|
@@ -262,14 +305,59 @@ WantedBy=default.target
|
|
|
262
305
|
return execSafe('systemctl --user enable --now clawlink-host') !== null;
|
|
263
306
|
}
|
|
264
307
|
if (platform === 'win32') {
|
|
265
|
-
|
|
266
|
-
const taskCmd = `"${nodeBin()}" "${cliEntry()}" run`;
|
|
267
|
-
return execSafe(`schtasks /Create /F /SC ONLOGON /TN ClawLinkHost /TR ${JSON.stringify(taskCmd)}`) !== null;
|
|
308
|
+
return installServiceWindows();
|
|
268
309
|
}
|
|
269
310
|
} catch { /* fall through */ }
|
|
270
311
|
return false;
|
|
271
312
|
}
|
|
272
313
|
|
|
314
|
+
// Windows: a hidden, auto-restarting Scheduled Task created from an XML definition
|
|
315
|
+
// (avoids schtasks /TR quoting issues with spaces in paths). A .cmd launcher pipes
|
|
316
|
+
// the worker's output to host.log; a .vbs runs that launcher with NO console window.
|
|
317
|
+
function installServiceWindows() {
|
|
318
|
+
const logPath = path.join(config.HOME_DIR, 'host.log');
|
|
319
|
+
const cmdPath = path.join(config.HOME_DIR, 'run-host.cmd');
|
|
320
|
+
const vbsPath = path.join(config.HOME_DIR, 'run-host.vbs');
|
|
321
|
+
const xmlPath = path.join(config.HOME_DIR, 'task.xml');
|
|
322
|
+
fs.mkdirSync(config.HOME_DIR, { recursive: true });
|
|
323
|
+
|
|
324
|
+
// 1) launcher that runs the worker and redirects stdout+stderr to the log
|
|
325
|
+
fs.writeFileSync(cmdPath,
|
|
326
|
+
`@echo off\r\n"${nodeBin()}" "${cliEntry()}" run >> "${logPath}" 2>&1\r\n`);
|
|
327
|
+
|
|
328
|
+
// 2) VBScript that runs the launcher hidden (0) and waits (True) so the task
|
|
329
|
+
// process stays alive while the worker runs. "" -> a literal quote in VBScript.
|
|
330
|
+
fs.writeFileSync(vbsPath,
|
|
331
|
+
`CreateObject("WScript.Shell").Run "cmd /c ""${cmdPath}""", 0, True\r\n`);
|
|
332
|
+
|
|
333
|
+
// 3) Task XML: logon trigger, hidden, no time limit, restart-on-failure (parity
|
|
334
|
+
// with launchd KeepAlive / systemd Restart=always). UTF-16 + BOM (schtasks /XML).
|
|
335
|
+
const xml = `<?xml version="1.0" encoding="UTF-16"?>
|
|
336
|
+
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
337
|
+
<RegistrationInfo><Description>ClawLink Host Gateway</Description></RegistrationInfo>
|
|
338
|
+
<Triggers><LogonTrigger><Enabled>true</Enabled></LogonTrigger></Triggers>
|
|
339
|
+
<Principals><Principal id="Author"><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
|
|
340
|
+
<Settings>
|
|
341
|
+
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
342
|
+
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
343
|
+
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
344
|
+
<StartWhenAvailable>true</StartWhenAvailable>
|
|
345
|
+
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
|
346
|
+
<Hidden>true</Hidden>
|
|
347
|
+
<Enabled>true</Enabled>
|
|
348
|
+
<RestartOnFailure><Interval>PT1M</Interval><Count>999</Count></RestartOnFailure>
|
|
349
|
+
</Settings>
|
|
350
|
+
<Actions Context="Author"><Exec><Command>wscript.exe</Command><Arguments>//B //Nologo "${vbsPath}"</Arguments></Exec></Actions>
|
|
351
|
+
</Task>`;
|
|
352
|
+
fs.writeFileSync(xmlPath, '' + xml, 'utf16le');
|
|
353
|
+
|
|
354
|
+
execSafe('schtasks /End /TN ClawLinkHost');
|
|
355
|
+
execSafe('schtasks /Delete /F /TN ClawLinkHost');
|
|
356
|
+
const created = execSafe(`schtasks /Create /F /TN ClawLinkHost /XML "${xmlPath}"`) !== null;
|
|
357
|
+
if (created) execSafe('schtasks /Run /TN ClawLinkHost');
|
|
358
|
+
return created;
|
|
359
|
+
}
|
|
360
|
+
|
|
273
361
|
function serviceRunning() {
|
|
274
362
|
const platform = process.platform;
|
|
275
363
|
if (platform === 'darwin') {
|
|
@@ -294,6 +382,6 @@ function status() {
|
|
|
294
382
|
}
|
|
295
383
|
|
|
296
384
|
module.exports = {
|
|
297
|
-
run, installOnly, addAgentCmd, removeAgentCmd, listAgents, status,
|
|
385
|
+
run, installOnly, addAgentCmd, removeAgentCmd, retokenCmd, listAgents, status,
|
|
298
386
|
serviceControl, serviceControlCmd, installService, SERVICE_LABEL,
|
|
299
387
|
};
|
package/src/adapters/base.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
// Shared spawn helper for CLI-wrapping adapters.
|
|
3
3
|
//
|
|
4
|
-
// SECURITY: commands are
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// SECURITY: commands are invoked with an argv ARRAY (never a concatenated command
|
|
5
|
+
// string). Placeholders in args_template ({prompt}, {sessionId}, {systemPrompt}) are
|
|
6
|
+
// substituted PER ARRAY ELEMENT — each becomes exactly one argv token. On POSIX we
|
|
7
|
+
// always use shell:false so untrusted prompt text can never reach a shell. On Windows,
|
|
8
|
+
// `.cmd`/`.bat` shims (e.g. npm-installed CLIs) MUST go through the shell because Node
|
|
9
|
+
// refuses to spawn them otherwise; Node then applies its hardened cmd.exe escaping to
|
|
10
|
+
// each argv token. For Windows, prefer a runtime that takes the prompt via STDIN
|
|
11
|
+
// (Claude does) or a native `.exe` so untrusted text never enters a Windows argv at
|
|
12
|
+
// all. The host only ever runs the configured runtime binary.
|
|
9
13
|
|
|
10
14
|
const { spawn } = require('child_process');
|
|
11
15
|
const logger = require('../logger');
|
|
@@ -44,11 +48,20 @@ function buildArgv(template, vars) {
|
|
|
44
48
|
async function* spawnStreaming({ binary, argv, cwd, env, timeoutMs = 600000, stdinInput = null, parseLine }) {
|
|
45
49
|
if (!binary) throw new Error('no runtime binary configured');
|
|
46
50
|
|
|
51
|
+
// Windows: npm CLIs are `.cmd`/`.bat` shims, and modern Node REFUSES to spawn those
|
|
52
|
+
// with shell:false (EINVAL, post-CVE-2024-27980). Such shims (and bare names that
|
|
53
|
+
// resolve via PATHEXT) must go through the shell; Node then applies its hardened
|
|
54
|
+
// cmd.exe argument escaping. Native `.exe`/`.com` are spawned directly (no shell).
|
|
55
|
+
// On POSIX we ALWAYS use shell:false (untrusted prompt can never reach a shell).
|
|
56
|
+
const isWin = process.platform === 'win32';
|
|
57
|
+
const useShell = isWin && !/\.(exe|com)$/i.test(String(binary));
|
|
58
|
+
|
|
47
59
|
const child = spawn(binary, argv, {
|
|
48
60
|
cwd: cwd || undefined,
|
|
49
61
|
// Enriched PATH so a CLI in ~/.local/bin etc. resolves under a service's minimal env.
|
|
50
62
|
env: enrichedEnv(env),
|
|
51
|
-
shell: false,
|
|
63
|
+
shell: useShell, // false on POSIX (no shell, ever); on Windows true only for .cmd/.bat shims
|
|
64
|
+
windowsHide: true, // no flashing console window on Windows
|
|
52
65
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
53
66
|
});
|
|
54
67
|
|
package/src/adapters/claude.js
CHANGED
|
@@ -10,10 +10,11 @@ module.exports = makeCliAdapter({
|
|
|
10
10
|
streamJson: true,
|
|
11
11
|
// Prompt via stdin keeps it out of argv/process listings.
|
|
12
12
|
promptViaStdin: true,
|
|
13
|
-
defaultArgs: (_fullPrompt, resumeId) => [
|
|
13
|
+
defaultArgs: (_fullPrompt, resumeId, _stdin, perm = []) => [
|
|
14
14
|
'-p',
|
|
15
15
|
'--output-format', 'stream-json',
|
|
16
16
|
'--verbose',
|
|
17
|
+
...perm,
|
|
17
18
|
...(resumeId ? ['--resume', resumeId] : []),
|
|
18
19
|
],
|
|
19
20
|
// Admin "configure" turns may write workspace files (auto-accept edits, no shell
|
package/src/adapters/cli.js
CHANGED
|
@@ -78,12 +78,14 @@ function makeCliAdapter(cfg) {
|
|
|
78
78
|
|
|
79
79
|
// Scope-based permissions: admin "configure" turns may write workspace files
|
|
80
80
|
// (GUARDRAILS/SOUL/etc.); customer/discovery turns are read-only and cannot
|
|
81
|
-
// edit or run shell.
|
|
81
|
+
// edit or run shell. permArgs is handed to defaultArgs so each adapter can place
|
|
82
|
+
// it correctly (e.g. Codex must put flags BEFORE its `--` end-of-options).
|
|
83
|
+
// Operator args_template overrides take full control.
|
|
82
84
|
const scope = job.scope || 'customer';
|
|
83
85
|
const permArgs = (!Array.isArray(agent.args_template) && cfg.permissions) ? cfg.permissions(scope) : [];
|
|
84
86
|
const argvFor = (rid) => Array.isArray(agent.args_template)
|
|
85
87
|
? buildArgv(agent.args_template, { prompt: fullPrompt, sessionId: rid || '', systemPrompt })
|
|
86
|
-
:
|
|
88
|
+
: cfg.defaultArgs(fullPrompt, rid, cfg.promptViaStdin, permArgs);
|
|
87
89
|
|
|
88
90
|
let emittedAny = false;
|
|
89
91
|
const runOnce = async function* (rid) {
|
package/src/adapters/codex.js
CHANGED
|
@@ -7,7 +7,16 @@ module.exports = makeCliAdapter({
|
|
|
7
7
|
name: 'codex',
|
|
8
8
|
binaries: ['codex'],
|
|
9
9
|
promptViaStdin: false,
|
|
10
|
-
// `--` ends option parsing so an untrusted prompt starting
|
|
11
|
-
// as a positional argument, never as a flag (
|
|
12
|
-
|
|
10
|
+
// Flags go BEFORE `--`; `--` ends option parsing so an untrusted prompt starting
|
|
11
|
+
// with '-' is treated as a positional argument, never as a flag (arg-injection
|
|
12
|
+
// hardening). `--ask-for-approval never` ensures it never blocks waiting for an
|
|
13
|
+
// interactive approval in headless mode.
|
|
14
|
+
defaultArgs: (fullPrompt, _resumeId, _stdin, perm = []) => ['exec', ...perm, '--', fullPrompt],
|
|
15
|
+
// Admin "configure" → workspace-write sandbox (may write files in its work dir);
|
|
16
|
+
// customer/discovery → read-only sandbox (cannot modify anything). Never the
|
|
17
|
+
// danger/full-access sandbox.
|
|
18
|
+
permissions: (scope) =>
|
|
19
|
+
(scope === 'configure' || scope === 'sub_configure')
|
|
20
|
+
? ['--sandbox', 'workspace-write', '--ask-for-approval', 'never']
|
|
21
|
+
: ['--sandbox', 'read-only', '--ask-for-approval', 'never'],
|
|
13
22
|
});
|
package/src/paths.js
CHANGED
|
@@ -10,17 +10,28 @@ const path = require('path');
|
|
|
10
10
|
|
|
11
11
|
function enrichedPath() {
|
|
12
12
|
const home = os.homedir();
|
|
13
|
-
const extras = [
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
path.join(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
'
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
const extras = [path.dirname(process.execPath)]; // dir of the running node (npm-global CLIs)
|
|
14
|
+
if (process.platform === 'win32') {
|
|
15
|
+
const { APPDATA, LOCALAPPDATA, ProgramFiles, SystemRoot } = process.env;
|
|
16
|
+
if (APPDATA) extras.push(path.join(APPDATA, 'npm')); // npm global shims
|
|
17
|
+
if (LOCALAPPDATA) {
|
|
18
|
+
extras.push(path.join(LOCALAPPDATA, 'Programs')); // per-user installers (e.g. claude)
|
|
19
|
+
extras.push(path.join(LOCALAPPDATA, 'Microsoft', 'WindowsApps'));
|
|
20
|
+
}
|
|
21
|
+
if (ProgramFiles) extras.push(path.join(ProgramFiles, 'nodejs'));
|
|
22
|
+
if (SystemRoot) { extras.push(path.join(SystemRoot, 'System32')); extras.push(SystemRoot); }
|
|
23
|
+
} else {
|
|
24
|
+
extras.push(
|
|
25
|
+
path.join(home, '.local', 'bin'),
|
|
26
|
+
path.join(home, 'bin'),
|
|
27
|
+
path.join(home, '.npm-global', 'bin'),
|
|
28
|
+
path.join(home, '.bun', 'bin'),
|
|
29
|
+
path.join(home, '.cargo', 'bin'),
|
|
30
|
+
'/opt/homebrew/bin',
|
|
31
|
+
'/usr/local/bin',
|
|
32
|
+
'/usr/bin', '/bin', '/usr/sbin', '/sbin',
|
|
33
|
+
);
|
|
34
|
+
}
|
|
24
35
|
const seen = new Set();
|
|
25
36
|
const parts = [...extras, ...String(process.env.PATH || '').split(path.delimiter)]
|
|
26
37
|
.filter((p) => p && !seen.has(p) && seen.add(p));
|