@claw-link/gateway-host 0.3.10 → 0.3.13
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/bin/cli.js +20 -0
- package/package.json +1 -1
- package/scripts/install.js +128 -2
- package/src/adapters/claude.js +7 -2
- package/src/adapters/cli.js +36 -5
- package/src/adapters/codex.js +5 -2
- package/src/config.js +5 -0
- package/src/worker.js +6 -3
package/bin/cli.js
CHANGED
|
@@ -36,6 +36,22 @@ async function main() {
|
|
|
36
36
|
case 'model':
|
|
37
37
|
return require('../scripts/install').setModelCmd();
|
|
38
38
|
|
|
39
|
+
case 'set-permission':
|
|
40
|
+
case 'set-perm':
|
|
41
|
+
case 'permission':
|
|
42
|
+
return require('../scripts/install').setPermissionCmd();
|
|
43
|
+
|
|
44
|
+
case 'set-concurrency':
|
|
45
|
+
case 'concurrency':
|
|
46
|
+
return require('../scripts/install').setConcurrencyCmd();
|
|
47
|
+
|
|
48
|
+
case 'link':
|
|
49
|
+
return require('../scripts/install').linkCmd();
|
|
50
|
+
|
|
51
|
+
case 'set-workdir':
|
|
52
|
+
case 'workdir':
|
|
53
|
+
return require('../scripts/install').setWorkdirCmd();
|
|
54
|
+
|
|
39
55
|
case 'agents':
|
|
40
56
|
case 'list':
|
|
41
57
|
return require('../scripts/install').listAgents();
|
|
@@ -98,9 +114,13 @@ async function main() {
|
|
|
98
114
|
'Usage: clhost <command>',
|
|
99
115
|
' install Install the background service (one-time)',
|
|
100
116
|
' add-agent Add an agent by pasting its Host Token (pulls runtime from ClawLink)',
|
|
117
|
+
' link Link MANY agents at once with a dashboard code (clhost link <clk_link_…>)',
|
|
118
|
+
' set-workdir Move an agent\'s workspace directory (clhost set-workdir <id> <dir>)',
|
|
101
119
|
' rm-agent Remove a mapped agent (clhost rm-agent <id>)',
|
|
102
120
|
' retoken Re-enter a new Host Token for an existing agent (clhost retoken <id>)',
|
|
103
121
|
' set-model Set the model an agent’s runtime uses (clhost set-model <id> <model>)',
|
|
122
|
+
' set-permission Permission profile for writable turns (clhost set-permission <id> trusted|standard)',
|
|
123
|
+
' set-concurrency Parallel jobs for an agent, isolated per-job dirs (clhost set-concurrency <id> <n>)',
|
|
104
124
|
' agents List mapped agents',
|
|
105
125
|
' status Show bridge, service state, and mapped agents',
|
|
106
126
|
' doctor Diagnose config, runtime binaries, bridge + token/machine binding',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claw-link/gateway-host",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
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
|
@@ -226,7 +226,7 @@ function printAgents(cfg) {
|
|
|
226
226
|
for (const a of agents) {
|
|
227
227
|
const id = a.agent_id || '(unresolved)';
|
|
228
228
|
const tok = a.host_token ? `…${String(a.host_token).slice(-4)}` : '—';
|
|
229
|
-
console.log(` • ${id} runtime=${a.runtime || 'pull'} token=${tok}` + (a.model ? ` model=${a.model}` : '') + (a.work_dir ? ` work=${a.work_dir}` : ''));
|
|
229
|
+
console.log(` • ${id} runtime=${a.runtime || 'pull'} token=${tok}` + (a.model ? ` model=${a.model}` : '') + (a.permission_profile ? ` perm=${a.permission_profile}` : '') + (Number(a.concurrency) > 1 ? ` conc=${a.concurrency}` : '') + (a.work_dir ? ` work=${a.work_dir}` : ''));
|
|
230
230
|
}
|
|
231
231
|
// Show a concrete, copy-pasteable example (commands also accept any id prefix).
|
|
232
232
|
const example = agents[0].agent_id || '<id>';
|
|
@@ -310,6 +310,132 @@ async function setModelCmd() {
|
|
|
310
310
|
console.log(` ✓ ${clear ? `Cleared model for ${agent.agent_id} — using the ${agent.runtime} default.` : `Set ${agent.agent_id} model → ${modelArg} (${agent.runtime}).`}` + note);
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
+
// `set-permission <id-or-prefix> <trusted|standard>` — set the agent's permission profile for
|
|
314
|
+
// WRITABLE (configure/harness) turns. 'trusted' lets the runtime run shell commands without
|
|
315
|
+
// prompts (claude bypassPermissions / codex danger-full-access) — required for dev/infra/CI
|
|
316
|
+
// agents that npm/mvn/docker/git. Customer chats stay read-only regardless. Restarts to apply.
|
|
317
|
+
async function setPermissionCmd() {
|
|
318
|
+
const cfg = config.load();
|
|
319
|
+
if (!cfg || !(cfg.agents || []).length) { console.log('No agents configured. Run: clhost add-agent'); return; }
|
|
320
|
+
const target = (process.argv[3] || '').trim();
|
|
321
|
+
const profile = (process.argv[4] || '').trim().toLowerCase();
|
|
322
|
+
if (!target || !['trusted', 'standard'].includes(profile)) {
|
|
323
|
+
console.log('Usage: clhost set-permission <agent-id|prefix> <trusted|standard>');
|
|
324
|
+
console.log(' trusted Writable turns run shell without prompts (dev/infra/CI agents).');
|
|
325
|
+
console.log(' standard Default: writable turns may edit files only; no unprompted shell.\n');
|
|
326
|
+
printAgents(cfg);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const agent = cfg.agents.find((a) => a.agent_id && (a.agent_id === target || a.agent_id.startsWith(target)));
|
|
330
|
+
if (!agent) { console.log(` No agent matched '${target}'.`); return; }
|
|
331
|
+
agent.permission_profile = profile === 'trusted' ? 'trusted' : null;
|
|
332
|
+
config.save(cfg);
|
|
333
|
+
if (profile === 'trusted') {
|
|
334
|
+
console.log(' ⚠ trusted = this agent can run ANY shell command on this machine during harness/');
|
|
335
|
+
console.log(' configure turns. Only use for agents whose workspaces + briefs you control.');
|
|
336
|
+
}
|
|
337
|
+
const r = await gracefulServiceRestartCLI();
|
|
338
|
+
const note = !r.restarted ? ' Run `clhost restart` to apply.'
|
|
339
|
+
: r.confirmed ? ' Service restarted.' : ' Restarting — check `clhost status`.';
|
|
340
|
+
console.log(` ✓ ${agent.agent_id} permission profile → ${profile}.` + note);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// `set-concurrency <id-or-prefix> <n>` — how many jobs this agent runs IN PARALLEL. With n > 1,
|
|
344
|
+
// each job gets an isolated working dir (work_dir/.jobs/<job-id>) so parallel runs can't trample
|
|
345
|
+
// each other; skill files (CLAUDE.md/AGENTS.md etc.) still apply via the parent directory.
|
|
346
|
+
// Scale dev agents in a harness by pointing MULTIPLE nodes at the same agent + raising this.
|
|
347
|
+
async function setConcurrencyCmd() {
|
|
348
|
+
const cfg = config.load();
|
|
349
|
+
if (!cfg || !(cfg.agents || []).length) { console.log('No agents configured. Run: clhost add-agent'); return; }
|
|
350
|
+
const target = (process.argv[3] || '').trim();
|
|
351
|
+
const n = Number((process.argv[4] || '').trim());
|
|
352
|
+
if (!target || !Number.isInteger(n) || n < 1 || n > 16) {
|
|
353
|
+
console.log('Usage: clhost set-concurrency <agent-id|prefix> <1-16>');
|
|
354
|
+
console.log(' n parallel jobs for this agent. n>1 isolates each job in work_dir/.jobs/<id>.\n');
|
|
355
|
+
printAgents(cfg);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const agent = cfg.agents.find((a) => a.agent_id && (a.agent_id === target || a.agent_id.startsWith(target)));
|
|
359
|
+
if (!agent) { console.log(` No agent matched '${target}'.`); return; }
|
|
360
|
+
agent.concurrency = n;
|
|
361
|
+
config.save(cfg);
|
|
362
|
+
const r = await gracefulServiceRestartCLI();
|
|
363
|
+
const note = !r.restarted ? ' Run `clhost restart` to apply.'
|
|
364
|
+
: r.confirmed ? ' Service restarted.' : ' Restarting — check `clhost status`.';
|
|
365
|
+
console.log(` ✓ ${agent.agent_id} concurrency → ${n}${n > 1 ? ' (per-job isolated working dirs)' : ''}.` + note);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// `link <code>` — redeem a one-time Host Link code from the dashboard (Harness Studio →
|
|
369
|
+
// "Connect host"): the server rotates Host Tokens for every agent in the code and returns them
|
|
370
|
+
// once; we map them all into this host's config in one shot. No token pasting, no CSVs.
|
|
371
|
+
async function linkCmd() {
|
|
372
|
+
const code = (process.argv[3] || '').trim();
|
|
373
|
+
if (!code.startsWith('clk_link_')) {
|
|
374
|
+
console.log('Usage: clhost link <clk_link_…>');
|
|
375
|
+
console.log(' Generate a code in ClawLink → Harness Studio → "Connect host" (valid 10 min, one use).');
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const cfg = config.load() || config.defaultConfig();
|
|
379
|
+
ensureBridgeUrl(cfg);
|
|
380
|
+
const url = cfg.bridge_url.replace(/host-bridge\/?$/, 'host-link');
|
|
381
|
+
console.log(' Redeeming link code…');
|
|
382
|
+
let resp;
|
|
383
|
+
try {
|
|
384
|
+
const r = await fetch(url, {
|
|
385
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
386
|
+
body: JSON.stringify({ action: 'redeem', code }),
|
|
387
|
+
});
|
|
388
|
+
resp = await r.json();
|
|
389
|
+
if (!r.ok || resp.ok === false) throw new Error(resp.error || `HTTP ${r.status}`);
|
|
390
|
+
} catch (e) {
|
|
391
|
+
console.log(` ✗ Could not redeem: ${e.message}`);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const detected = detectRuntimes();
|
|
396
|
+
let added = 0, updated = 0;
|
|
397
|
+
for (const a of resp.agents || []) {
|
|
398
|
+
const existing = (cfg.agents || []).find((x) => x.agent_id === a.agent_id);
|
|
399
|
+
const entry = existing || { agent_id: a.agent_id };
|
|
400
|
+
entry.host_token = a.host_token;
|
|
401
|
+
entry.runtime = a.runtime;
|
|
402
|
+
if (!entry.binary && a.runtime !== 'openclaw' && detected[a.runtime]) entry.binary = detected[a.runtime];
|
|
403
|
+
if (!entry.work_dir) entry.work_dir = config.defaultWorkDir(a.runtime, a.agent_id);
|
|
404
|
+
// Baseline skill files — silent, only-missing, never overwrites (safe for real projects).
|
|
405
|
+
try {
|
|
406
|
+
const wd = config.expandHome(entry.work_dir);
|
|
407
|
+
fs.mkdirSync(wd, { recursive: true });
|
|
408
|
+
scaffold.copyBaseline(a.runtime, wd);
|
|
409
|
+
} catch { /* best effort */ }
|
|
410
|
+
if (existing) updated++; else { cfg.agents = [...(cfg.agents || []), entry]; added++; }
|
|
411
|
+
console.log(` ✓ ${a.name} (${a.runtime}) → ${entry.work_dir}`);
|
|
412
|
+
}
|
|
413
|
+
config.save(cfg);
|
|
414
|
+
console.log(`\n Linked ${added} new, ${updated} re-tokened agent(s).`);
|
|
415
|
+
const r2 = await gracefulServiceRestartCLI();
|
|
416
|
+
console.log(r2.restarted ? ' ✓ Host restarted and registering.' : ' ⚠ Run `clhost restart` (or start) to register.');
|
|
417
|
+
console.log('\n Point coding agents at their project folders: clhost set-workdir <id> <dir>');
|
|
418
|
+
console.log(' Then check: clhost agents · clhost status\n');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// `set-workdir <id-or-prefix> <dir>` — move an agent's workspace (coding runtimes run inside it).
|
|
422
|
+
async function setWorkdirCmd() {
|
|
423
|
+
const cfg = config.load();
|
|
424
|
+
if (!cfg || !(cfg.agents || []).length) { console.log('No agents configured.'); return; }
|
|
425
|
+
const target = (process.argv[3] || '').trim();
|
|
426
|
+
const dir = (process.argv[4] || '').trim();
|
|
427
|
+
if (!target || !dir) { console.log('Usage: clhost set-workdir <agent-id|prefix> <directory>'); printAgents(cfg); return; }
|
|
428
|
+
const agent = cfg.agents.find((a) => a.agent_id && (a.agent_id === target || a.agent_id.startsWith(target)));
|
|
429
|
+
if (!agent) { console.log(` No agent matched '${target}'.`); return; }
|
|
430
|
+
const resolved = config.expandHome(dir);
|
|
431
|
+
if (!fs.existsSync(resolved)) { console.log(` ✗ ${dir} does not exist.`); return; }
|
|
432
|
+
agent.work_dir = resolved;
|
|
433
|
+
try { scaffold.copyBaseline(agent.runtime, resolved); } catch { /* best effort */ }
|
|
434
|
+
config.save(cfg);
|
|
435
|
+
const r = await gracefulServiceRestartCLI();
|
|
436
|
+
console.log(` ✓ ${agent.agent_id} workspace → ${resolved}.` + (r.restarted ? ' Service restarted.' : ' Run `clhost restart` to apply.'));
|
|
437
|
+
}
|
|
438
|
+
|
|
313
439
|
// `start | stop | restart` — control the installed background service.
|
|
314
440
|
function serviceControl(action) {
|
|
315
441
|
const platform = process.platform;
|
|
@@ -739,7 +865,7 @@ async function update() {
|
|
|
739
865
|
}
|
|
740
866
|
|
|
741
867
|
module.exports = {
|
|
742
|
-
run, installOnly, addAgentCmd, removeAgentCmd, retokenCmd, setModelCmd, listAgents, status,
|
|
868
|
+
run, installOnly, addAgentCmd, removeAgentCmd, retokenCmd, setModelCmd, setPermissionCmd, setConcurrencyCmd, linkCmd, setWorkdirCmd, listAgents, status,
|
|
743
869
|
serviceControl, serviceControlCmd, installService, SERVICE_LABEL,
|
|
744
870
|
versionCmd, doctor, logs, update,
|
|
745
871
|
};
|
package/src/adapters/claude.js
CHANGED
|
@@ -21,8 +21,13 @@ module.exports = makeCliAdapter({
|
|
|
21
21
|
// Admin "configure" turns may write workspace files (auto-accept edits, no shell
|
|
22
22
|
// prompt); customer/discovery turns are strictly read-only (no Write/Edit/Bash) so
|
|
23
23
|
// they can never modify the agent's config or hang on a permission prompt.
|
|
24
|
-
|
|
24
|
+
// 'trusted' profile (clhost set-permission <id> trusted) upgrades WRITABLE turns to
|
|
25
|
+
// bypassPermissions so dev/infra agents can run shell (npm/mvn/docker/git) without
|
|
26
|
+
// prompts — customer turns stay read-only regardless.
|
|
27
|
+
permissions: (scope, profile) =>
|
|
25
28
|
(scope === 'configure' || scope === 'sub_configure')
|
|
26
|
-
?
|
|
29
|
+
? (profile === 'trusted'
|
|
30
|
+
? ['--permission-mode', 'bypassPermissions']
|
|
31
|
+
: ['--permission-mode', 'acceptEdits'])
|
|
27
32
|
: ['--disallowedTools', 'Write Edit MultiEdit NotebookEdit Bash'],
|
|
28
33
|
});
|
package/src/adapters/cli.js
CHANGED
|
@@ -113,9 +113,26 @@ function makeCodexJsonParser(capture) {
|
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
// Best-effort cleanup of per-job working dirs older than 48h (parallel workers create one per job).
|
|
117
|
+
const JOB_DIR_TTL_MS = 48 * 60 * 60 * 1000;
|
|
118
|
+
let lastPruneAt = 0;
|
|
119
|
+
function pruneJobDirs(jobsRoot, logger) {
|
|
120
|
+
const now = Date.now();
|
|
121
|
+
if (now - lastPruneAt < 10 * 60 * 1000) return; // at most every 10 min across all agents
|
|
122
|
+
lastPruneAt = now;
|
|
123
|
+
try {
|
|
124
|
+
for (const name of fs.readdirSync(jobsRoot)) {
|
|
125
|
+
const p = path.join(jobsRoot, name);
|
|
126
|
+
try {
|
|
127
|
+
if (now - fs.statSync(p).mtimeMs > JOB_DIR_TTL_MS) fs.rmSync(p, { recursive: true, force: true });
|
|
128
|
+
} catch { /* skip entry */ }
|
|
129
|
+
}
|
|
130
|
+
} catch { /* no .jobs dir yet */ }
|
|
131
|
+
}
|
|
132
|
+
|
|
116
133
|
/**
|
|
117
134
|
* @param cfg { name, binaries:[], defaultArgs(fullPrompt, resumeId, promptViaStdin, permArgs, model):[],
|
|
118
|
-
* streamJson?:bool, makeParser?:(capture)=>parseFn, permissions?:(scope)=>[],
|
|
135
|
+
* streamJson?:bool, makeParser?:(capture)=>parseFn, permissions?:(scope, profile)=>[],
|
|
119
136
|
* promptViaStdin?:bool, timeoutMs?:number }
|
|
120
137
|
*/
|
|
121
138
|
function makeCliAdapter(cfg) {
|
|
@@ -136,9 +153,21 @@ function makeCliAdapter(cfg) {
|
|
|
136
153
|
// the spawn never fail with ENOENT (coding workspaces are validated at setup).
|
|
137
154
|
try { fs.mkdirSync(agent.work_dir, { recursive: true }); } catch { /* ignore */ }
|
|
138
155
|
|
|
139
|
-
//
|
|
156
|
+
// Parallel workers: when the agent runs jobs CONCURRENTLY (concurrency > 1), each job gets an
|
|
157
|
+
// ISOLATED working dir under .jobs/<job-id> so two runs never trample each other's files
|
|
158
|
+
// (git index, node_modules, build output). Claude/Codex read CLAUDE.md / AGENTS.md up the
|
|
159
|
+
// directory tree, so the workspace skill files still apply. Serial agents keep the shared
|
|
160
|
+
// work_dir (cache warmth). Old job dirs are pruned best-effort after 48h.
|
|
161
|
+
let runDir = agent.work_dir;
|
|
162
|
+
if (Number(agent.concurrency) > 1) {
|
|
163
|
+
runDir = path.join(agent.work_dir, '.jobs', String(job.id).replace(/[^a-zA-Z0-9-]/g, '').slice(0, 12) || 'job');
|
|
164
|
+
try { fs.mkdirSync(runDir, { recursive: true }); } catch { /* fall back to shared dir */ }
|
|
165
|
+
pruneJobDirs(path.join(agent.work_dir, '.jobs'), logger);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Download any attachments into the working dir and reference their LOCAL paths, so the CLI
|
|
140
169
|
// (which can't reach a URL) can open them via its Read tool.
|
|
141
|
-
const attBlock = await localizeAttachments(job.attachments,
|
|
170
|
+
const attBlock = await localizeAttachments(job.attachments, runDir, logger);
|
|
142
171
|
const base = systemPrompt ? `${systemPrompt}\n\n---\n\nUser request:\n${userPrompt}` : userPrompt;
|
|
143
172
|
const fullPrompt = base + attBlock;
|
|
144
173
|
|
|
@@ -151,8 +180,10 @@ function makeCliAdapter(cfg) {
|
|
|
151
180
|
// workspace-write like an admin "configure" turn — even though they're carried on the
|
|
152
181
|
// 'customer' scope. Real customer chats stay read-only.
|
|
153
182
|
const writable = !!job.harness || job.scope === 'configure' || job.scope === 'sub_configure';
|
|
183
|
+
// 'trusted' profile only ever applies to WRITABLE turns (harness/configure) — customer
|
|
184
|
+
// chats stay read-only regardless, so a public conversation can never run shell commands.
|
|
154
185
|
const permArgs = (!Array.isArray(agent.args_template) && cfg.permissions)
|
|
155
|
-
? cfg.permissions(writable ? 'configure' : (job.scope || 'customer'))
|
|
186
|
+
? cfg.permissions(writable ? 'configure' : (job.scope || 'customer'), agent.permission_profile || null)
|
|
156
187
|
: [];
|
|
157
188
|
// Optional per-agent model override (e.g. `clhost set-model <id> sonnet`). Each adapter
|
|
158
189
|
// places it with the runtime's correct flag; ignored when an args_template takes over.
|
|
@@ -168,7 +199,7 @@ function makeCliAdapter(cfg) {
|
|
|
168
199
|
: cfg.streamJson ? makeStreamJsonParser(capture)
|
|
169
200
|
: genericLineParser;
|
|
170
201
|
for await (const evt of spawnStreaming({
|
|
171
|
-
binary, argv: argvFor(rid), cwd:
|
|
202
|
+
binary, argv: argvFor(rid), cwd: runDir, timeoutMs,
|
|
172
203
|
stdinInput: cfg.promptViaStdin ? fullPrompt : null, parseLine, signal,
|
|
173
204
|
})) {
|
|
174
205
|
if (evt.type === 'chunk') emittedAny = true;
|
package/src/adapters/codex.js
CHANGED
|
@@ -29,8 +29,11 @@ module.exports = makeCliAdapter({
|
|
|
29
29
|
// Admin "configure" → workspace-write (may write its work dir); customer/discovery
|
|
30
30
|
// → read-only. approval_policy=never so it never blocks on an approval prompt.
|
|
31
31
|
// Set via `-c` config overrides (the only form `exec resume` also accepts).
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
// 'trusted' profile upgrades WRITABLE turns to danger-full-access (network + full fs) so
|
|
33
|
+
// dev agents can npm/mvn/docker — customer turns stay read-only regardless.
|
|
34
|
+
permissions: (scope, profile) => {
|
|
35
|
+
const writable = scope === 'configure' || scope === 'sub_configure';
|
|
36
|
+
const mode = writable ? (profile === 'trusted' ? 'danger-full-access' : 'workspace-write') : 'read-only';
|
|
34
37
|
return ['-c', `sandbox_mode="${mode}"`, '-c', 'approval_policy="never"'];
|
|
35
38
|
},
|
|
36
39
|
});
|
package/src/config.js
CHANGED
|
@@ -74,6 +74,11 @@ function normalizeAgent(a) {
|
|
|
74
74
|
// Optional model override for coding runtimes (claude/codex/cursor/hermes). null = the
|
|
75
75
|
// runtime's own default. Set with `clhost set-model <id> <model>`.
|
|
76
76
|
model: a.model || null,
|
|
77
|
+
// Permission profile for writable (configure/harness) turns: 'trusted' lets the runtime run
|
|
78
|
+
// shell commands without prompts (claude --permission-mode bypassPermissions, codex
|
|
79
|
+
// danger-full-access) — needed for dev/infra/CI agents that npm/mvn/docker/git. Customer
|
|
80
|
+
// chats stay read-only REGARDLESS of profile. Set with `clhost set-permission <id> trusted`.
|
|
81
|
+
permission_profile: a.permission_profile === 'trusted' ? 'trusted' : null,
|
|
77
82
|
args_template: Array.isArray(a.args_template) ? a.args_template : null,
|
|
78
83
|
local_gateway_url: a.local_gateway_url || null,
|
|
79
84
|
local_gateway_token: a.local_gateway_token || null,
|
package/src/worker.js
CHANGED
|
@@ -159,6 +159,9 @@ async function runAgentLoop(agentCfg, cfg, p2p) {
|
|
|
159
159
|
let inFlight = 0;
|
|
160
160
|
const starts = []; // job start timestamps for the rolling rate-limit window
|
|
161
161
|
let claimFails = 0; // consecutive claim() errors → exponential backoff w/ jitter (transient 5xx)
|
|
162
|
+
// Hard job ceiling: config `job_max_ms` overrides the env/default — long dev/build turns
|
|
163
|
+
// (harness agents running mvn/npm/docker) can legitimately exceed the 15-min default.
|
|
164
|
+
const jobMaxMs = Number(cfg.job_max_ms) > 0 ? Number(cfg.job_max_ms) : JOB_MAX_MS;
|
|
162
165
|
|
|
163
166
|
while (!stopping) {
|
|
164
167
|
const now = Date.now();
|
|
@@ -178,12 +181,12 @@ async function runAgentLoop(agentCfg, cfg, p2p) {
|
|
|
178
181
|
inFlight++; starts.push(Date.now()); activeJobIds.add(job.id);
|
|
179
182
|
// Watchdog: guarantees the slot is reclaimed and the job is failed back even
|
|
180
183
|
// if the adapter never settles (no per-adapter timeout by default).
|
|
181
|
-
const watchdog = new Promise((resolve) => setTimeout(() => resolve('__watchdog__'),
|
|
184
|
+
const watchdog = new Promise((resolve) => setTimeout(() => resolve('__watchdog__'), jobMaxMs).unref());
|
|
182
185
|
Promise.race([processJob(bridge, agentCfg, job, p2p), watchdog])
|
|
183
186
|
.then(async (r) => {
|
|
184
187
|
if (r === '__watchdog__') {
|
|
185
|
-
logger.error(`job ${job.id} exceeded ${
|
|
186
|
-
try { await bridge.fail(job.id, `host watchdog: job exceeded ${
|
|
188
|
+
logger.error(`job ${job.id} exceeded ${jobMaxMs}ms — reclaiming slot`);
|
|
189
|
+
try { await bridge.fail(job.id, `host watchdog: job exceeded ${jobMaxMs}ms`); } catch { /* ignore */ }
|
|
187
190
|
}
|
|
188
191
|
})
|
|
189
192
|
.finally(() => { inFlight--; activeJobIds.delete(job.id); });
|