crewx-agent-cli 0.2.2 → 0.2.4
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 +80 -5
- package/dist/adapters.d.ts +12 -0
- package/dist/adapters.d.ts.map +1 -1
- package/dist/adapters.js +139 -42
- package/dist/adapters.js.map +1 -1
- package/dist/api.d.ts +40 -17
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +187 -11
- package/dist/api.js.map +1 -1
- package/dist/bridge-watchdog.d.ts +10 -0
- package/dist/bridge-watchdog.d.ts.map +1 -0
- package/dist/bridge-watchdog.js +45 -0
- package/dist/bridge-watchdog.js.map +1 -0
- package/dist/config.d.ts +9 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +53 -2
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +6 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +11 -1
- package/dist/constants.js.map +1 -1
- package/dist/daemon.d.ts +34 -2
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +495 -100
- package/dist/daemon.js.map +1 -1
- package/dist/errors.d.ts +4 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +3 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -6
- package/dist/index.js.map +1 -1
- package/dist/join.d.ts +1 -0
- package/dist/join.d.ts.map +1 -1
- package/dist/join.js +13 -0
- package/dist/join.js.map +1 -1
- package/dist/outbox.d.ts +22 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +189 -0
- package/dist/outbox.js.map +1 -0
- package/dist/process-lock.d.ts +31 -0
- package/dist/process-lock.d.ts.map +1 -0
- package/dist/process-lock.js +272 -0
- package/dist/process-lock.js.map +1 -0
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +7 -0
- package/dist/prompt.js.map +1 -1
- package/dist/runtime-proxy.d.ts +18 -0
- package/dist/runtime-proxy.d.ts.map +1 -0
- package/dist/runtime-proxy.js +221 -0
- package/dist/runtime-proxy.js.map +1 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# CrewX CLI
|
|
2
2
|
|
|
3
|
-
Connect an existing Codex, Claude Code, Pi, Hermes, or OpenClaw installation to
|
|
3
|
+
Connect an existing Codex, Claude Code, Pi, Hermes, or OpenClaw installation to
|
|
4
|
+
a CrewX workspace. The runner stays on your machine, so the agent uses its
|
|
5
|
+
existing login, configuration, tools, and access to the selected working
|
|
6
|
+
directory.
|
|
7
|
+
|
|
8
|
+
For a persistent agent, CrewX recommends a connected machine managed by
|
|
9
|
+
`crewx-bridge`. **Via terminal** is the portable foreground alternative
|
|
10
|
+
documented below.
|
|
4
11
|
|
|
5
12
|
## One-command join
|
|
6
13
|
|
|
@@ -23,7 +30,18 @@ npx --yes crewx-agent-cli@latest --join <join-code> --adapter codex
|
|
|
23
30
|
|
|
24
31
|
The versioned join payload is base64url-encoded JSON containing the server URL, adapter, and raw `crwx_…` bearer token. Base64url is transport encoding, not encryption. CrewX shows the generated token once, but the credential inside the command is reusable until its configured expiry or revocation. Anyone who obtains the command can decode and use it, and the command may remain in shell history. Treat it as a secret and revoke the token from the agent profile if it is exposed.
|
|
25
32
|
|
|
26
|
-
The server stores only the token digest. During a join-mode run, the CLI keeps
|
|
33
|
+
The server stores only the token digest. During a join-mode run, the CLI keeps
|
|
34
|
+
the decoded credential in memory, but it does not pass that reusable credential
|
|
35
|
+
to the coding runtime. For each assignment it opens an ephemeral loopback
|
|
36
|
+
capability proxy and supplies the runtime only that proxy's URL, random bearer,
|
|
37
|
+
and CLI path. The proxy allows the selected task, document, memory, and
|
|
38
|
+
integration operations with strict route/method, size, timeout, and redirect
|
|
39
|
+
limits, then closes when the assignment ends. A locally executed agent still
|
|
40
|
+
has the operating-system permissions of the user account running it. The proxy
|
|
41
|
+
prevents accidental credential inheritance and limits the normal tool surface;
|
|
42
|
+
it is not isolation from hostile code running as the same OS user, which may
|
|
43
|
+
inspect that user's files and processes. Use a separate OS account, container,
|
|
44
|
+
VM, or host for an untrusted runtime or materially different trust boundary.
|
|
27
45
|
|
|
28
46
|
## Persistent installation
|
|
29
47
|
|
|
@@ -46,7 +64,42 @@ crewx daemon --adapter codex
|
|
|
46
64
|
|
|
47
65
|
The saved connection lives at `$XDG_CONFIG_HOME/crewx/config.json` (or `~/.config/crewx/config.json`) with mode `0600`. `CREWX_URL`, `CREWX_TOKEN`, and `CREWX_ADAPTER` override saved settings and are useful in explicitly secured services and CI. The CLI includes operating-system certificate authorities in Node's trust store, so locally trusted HTTPS sites such as Laravel Herd work without disabling certificate verification.
|
|
48
66
|
|
|
49
|
-
Both join mode and `crewx daemon` run in the foreground. Start either command
|
|
67
|
+
Both join mode and `crewx daemon` run in the foreground. Start either command
|
|
68
|
+
inside a safe fallback directory. For each assignment, CrewX uses the project
|
|
69
|
+
channel's local path first, the agent profile's default working directory
|
|
70
|
+
second, and `--cwd` last. Use `--once` for one poll and
|
|
71
|
+
`--poll-interval 5000` to set the active/base polling cadence. During an idle
|
|
72
|
+
period the runner backs off with jitter to at most 8 seconds, honors server
|
|
73
|
+
retry guidance, and resets to the base cadence as soon as work appears.
|
|
74
|
+
|
|
75
|
+
Assignments carry an opaque claim-fencing token. The runner includes it on
|
|
76
|
+
heartbeats, messages, errors, and lifecycle events; CrewX rotates it when work
|
|
77
|
+
is reassigned so a replaced runner cannot apply late results. The token is
|
|
78
|
+
redacted from diagnostics and never enters the prompt or coding-runtime
|
|
79
|
+
environment. Final assistant messages, errors, and the terminal lifecycle event
|
|
80
|
+
are persisted as one owner-only outbox batch with stable event UUIDs before
|
|
81
|
+
delivery. A process restart replays that batch until the server acknowledges
|
|
82
|
+
it, while bounded backpressure coalesces replaceable output and never silently
|
|
83
|
+
evicts a terminal outcome. Every outbound event batch is capped against the
|
|
84
|
+
exact serialized HTTP body, including its envelope and separators; one event
|
|
85
|
+
that cannot fit fails visibly instead of creating an endlessly retried request.
|
|
86
|
+
|
|
87
|
+
The outbox is stored below
|
|
88
|
+
`~/Library/Application Support/CrewX Agent/outbox/` on macOS or
|
|
89
|
+
`${XDG_STATE_HOME:-~/.local/state}/crewx-agent/outbox/` on Linux. Its directory
|
|
90
|
+
is mode `0700`, files are mode `0600`, and unsafe ownership, modes, or symlinks
|
|
91
|
+
are rejected. New join codes carry an opaque, non-secret runner scope generated
|
|
92
|
+
once for the agent profile. It keys the outbox and its full-lifetime
|
|
93
|
+
single-writer lock, so a rotated connection token reuses the same durable
|
|
94
|
+
state. The token remains the sole authority. A second live daemon for the same
|
|
95
|
+
profile is rejected; after an unclean exit, a replacement verifies process
|
|
96
|
+
identity before recovering the lock. Legacy join codes without a runner scope
|
|
97
|
+
remain supported and derive an isolated fallback key from the token.
|
|
98
|
+
|
|
99
|
+
Bridge-started runners receive their per-run bearer through a one-shot
|
|
100
|
+
inherited pipe that closes immediately after startup, rather than through the
|
|
101
|
+
exec environment. This reduces accidental exposure but does not change the OS
|
|
102
|
+
account trust boundary.
|
|
50
103
|
|
|
51
104
|
## Built-in adapters
|
|
52
105
|
|
|
@@ -54,10 +107,17 @@ Both join mode and `crewx daemon` run in the foreground. Start either command in
|
|
|
54
107
|
- **Claude Code** runs `claude --print --output-format stream-json --verbose`. CrewX maps its profile to plan mode, non-interactive workspace edits, or skipped permission prompts and passes the prompt on stdin.
|
|
55
108
|
- **Pi** runs `pi --mode json --no-approve`. CrewX deliberately ignores project-local Pi settings, extensions, skills, prompts, and themes on each daemon run. Read-only and chat-only profiles receive a restricted tool allowlist. Global Pi configuration still applies. Pi itself is not an operating-system sandbox.
|
|
56
109
|
- **Hermes** runs `hermes acp` and speaks ACP v1 over stdio for session creation, model selection, prompt delivery, progress, permission requests, and cancellation. Workspace-write maps to Hermes `accept_edits`; full access maps to `dont_ask`. Hermes does not expose a trustworthy read-only or chat-only ACP mode, so CrewX rejects those profiles instead of presenting a false boundary. CrewX passes a configured model to Hermes for resolution.
|
|
57
|
-
- **OpenClaw**
|
|
110
|
+
- **OpenClaw** uses a persistent agent with JSON output, a collision-resistant CrewX assignment session key, and a mode-`0600` temporary prompt file that is removed after the run. New CrewX profiles receive a managed OpenClaw ID; when that ID is missing locally, CrewX creates it inside the Bridge-approved assignment folder with OpenClaw's non-interactive agent command. Advanced profiles may select an existing OpenClaw agent instead. Before connecting and before every assignment, CrewX verifies the exact agent workspace and task-session sandbox. A sandboxed session must mount the approved directory read-write at its runtime workdir. OpenClaw continues to own its persistent tool policy and approval behavior.
|
|
58
111
|
|
|
59
112
|
`crewx doctor` probes all five binaries. Runtime profile changes are read before every assignment, so supported model, permission, chat-only, working-directory, token, and deactivation changes do not require reconnecting. Chat-only and deactivated agents cannot receive task runs.
|
|
60
113
|
|
|
114
|
+
The assignment capability proxy enforces the server profile locally:
|
|
115
|
+
`read_only` and chat-only profiles may use only GET operations; `standard` and
|
|
116
|
+
`full_access` profiles may use the explicitly allowlisted task, document,
|
|
117
|
+
memory, and integration mutations. A 401 or 403 from the assignment control
|
|
118
|
+
endpoint immediately aborts the active process group and terminates the daemon
|
|
119
|
+
instead of allowing revoked work to continue.
|
|
120
|
+
|
|
61
121
|
## Custom commands
|
|
62
122
|
|
|
63
123
|
Advanced users can replace the built-in invocation without editing CrewX:
|
|
@@ -72,7 +132,13 @@ crewx run codex "Review this repository" --coding-cmd 'my-agent {prompt} --quiet
|
|
|
72
132
|
|
|
73
133
|
CrewX uses a quote-aware argv parser and launches the resulting executable with `shell: false`. The prompt replaces one exact `{prompt}` argument or is appended as one argument when the placeholder is absent; it is never interpolated into a shell string. Pipes, redirection, variable expansion, and command substitution are therefore not interpreted by CrewX.
|
|
74
134
|
|
|
75
|
-
This prevents prompt text from becoming shell syntax, but it does not make an
|
|
135
|
+
This prevents prompt text from becoming shell syntax, but it does not make an
|
|
136
|
+
arbitrary executable safe. The selected program inherits the runner's
|
|
137
|
+
operating-system permissions and ordinary provider environment, but all
|
|
138
|
+
inherited `CREWX_*` variables are removed and replaced only by the
|
|
139
|
+
assignment-lifetime loopback capability. Do not use commands from an untrusted
|
|
140
|
+
profile or run them in an untrusted working directory. Explicitly wrapping a
|
|
141
|
+
command in a shell recreates the shell-risk boundary and is not recommended.
|
|
76
142
|
|
|
77
143
|
## Shared work commands
|
|
78
144
|
|
|
@@ -99,6 +165,15 @@ crewx integration search google_drive "quarterly plan" --json
|
|
|
99
165
|
|
|
100
166
|
Agents may update only tasks assigned to their own profile. Documents must be agent-readable and unprotected, and every document update requires the current version from `doc list`; stale updates fail instead of overwriting newer work. Agents may edit only memories they recorded, while channel-scoped memory is limited to channels they can see. Provider access tokens are encrypted by the web application and are never returned to the CLI; integration commands expose normalized search results only.
|
|
101
167
|
|
|
168
|
+
Task, document, and memory lists consume the server's bounded cursor pages
|
|
169
|
+
automatically. Each HTTP response remains below the CLI's 2 MiB safety limit,
|
|
170
|
+
while existing callers still receive one combined array up to 10,000 items or
|
|
171
|
+
16 MiB of encoded data. Larger results fail with an instruction to narrow the
|
|
172
|
+
filters or use `listTaskPage`, `listDocumentPage`, or `listMemoryPage` for
|
|
173
|
+
explicit page-by-page processing. The monotonic cursor is the last seen
|
|
174
|
+
resource ID, so concurrent inserts do not shift later pages. Legacy servers
|
|
175
|
+
without pagination metadata remain a supported one-page response.
|
|
176
|
+
|
|
102
177
|
## One-shot local runs
|
|
103
178
|
|
|
104
179
|
The same adapters can be exercised without a CrewX connection:
|
package/dist/adapters.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { type ChildProcessWithoutNullStreams, type SpawnOptionsWithoutStdio } from 'node:child_process';
|
|
2
2
|
import { type AgentAdapter } from 'crewx-agent-protocol';
|
|
3
|
+
export declare function adapterProbePlan(adapter: AgentAdapter): {
|
|
4
|
+
args: readonly string[];
|
|
5
|
+
timeoutMs: number;
|
|
6
|
+
};
|
|
3
7
|
export interface AdapterInvocation {
|
|
4
8
|
command: string;
|
|
5
9
|
args: string[];
|
|
@@ -50,6 +54,7 @@ export interface RunAdapterOptions {
|
|
|
50
54
|
profile?: AgentRuntimeProfile | undefined;
|
|
51
55
|
codingCommand?: string | undefined;
|
|
52
56
|
environment?: NodeJS.ProcessEnv | undefined;
|
|
57
|
+
controlEnvironment?: NodeJS.ProcessEnv | undefined;
|
|
53
58
|
timeoutMs?: number | undefined;
|
|
54
59
|
maxOutputBytes?: number | undefined;
|
|
55
60
|
signal?: AbortSignal | undefined;
|
|
@@ -58,6 +63,13 @@ export interface RunAdapterOptions {
|
|
|
58
63
|
onMessage?: ((message: string, role: 'assistant' | 'tool') => void) | undefined;
|
|
59
64
|
onStderr?: ((line: string) => void) | undefined;
|
|
60
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Coding runtimes inherit provider credentials and ordinary process settings,
|
|
68
|
+
* but never inherit the daemon's CrewX control-plane credentials. The only
|
|
69
|
+
* CrewX variables admitted are short-lived, capability-scoped proxy values
|
|
70
|
+
* supplied separately by the assignment executor.
|
|
71
|
+
*/
|
|
72
|
+
export declare function runtimeEnvironment(environment?: NodeJS.ProcessEnv, controlEnvironment?: NodeJS.ProcessEnv, inherited?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
61
73
|
export declare function runAdapter(options: RunAdapterOptions): Promise<AdapterRunResult>;
|
|
62
74
|
export declare function probeAdapter(adapter: AgentAdapter, spawnProcess?: SpawnProcess, timeoutMs?: number): Promise<{
|
|
63
75
|
available: boolean;
|
package/dist/adapters.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapters.d.ts","sourceRoot":"","sources":["../src/adapters.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,8BAA8B,EAAE,KAAK,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAM/G,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"adapters.d.ts","sourceRoot":"","sources":["../src/adapters.ts"],"names":[],"mappings":"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"}
|
package/dist/adapters.js
CHANGED
|
@@ -6,6 +6,16 @@ import { join } from 'node:path';
|
|
|
6
6
|
import { AgentAdapterSchema } from 'crewx-agent-protocol';
|
|
7
7
|
import { CLI_VERSION, DEFAULT_RUN_TIMEOUT_MS, MAX_AGENT_OUTPUT_BYTES } from './constants.js';
|
|
8
8
|
import { CliError } from './errors.js';
|
|
9
|
+
const DEFAULT_RUNTIME_PROBE_TIMEOUT_MS = 5_000;
|
|
10
|
+
const OPENCLAW_RUNTIME_PROBE_TIMEOUT_MS = 30_000;
|
|
11
|
+
const OPENCLAW_SETUP_TIMEOUT_MS = 60_000;
|
|
12
|
+
export function adapterProbePlan(adapter) {
|
|
13
|
+
return adapter === 'hermes'
|
|
14
|
+
? { args: ['acp', '--check'], timeoutMs: DEFAULT_RUNTIME_PROBE_TIMEOUT_MS }
|
|
15
|
+
: adapter === 'openclaw'
|
|
16
|
+
? { args: ['agents', 'list', '--json'], timeoutMs: OPENCLAW_RUNTIME_PROBE_TIMEOUT_MS }
|
|
17
|
+
: { args: ['--version'], timeoutMs: DEFAULT_RUNTIME_PROBE_TIMEOUT_MS };
|
|
18
|
+
}
|
|
9
19
|
const CLAUDE_STANDARD_CREWX_TOOLS = [
|
|
10
20
|
'WebSearch',
|
|
11
21
|
'WebFetch',
|
|
@@ -348,28 +358,39 @@ function record(value) {
|
|
|
348
358
|
? value
|
|
349
359
|
: undefined;
|
|
350
360
|
}
|
|
361
|
+
function openClawError(code, message, cause) {
|
|
362
|
+
return new CliError(message, 1, {
|
|
363
|
+
code,
|
|
364
|
+
...(cause !== undefined ? { cause } : {}),
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
function publicOpenClawDiagnostic(value) {
|
|
368
|
+
return value
|
|
369
|
+
.replace(/(^|[\s"'`=:(])(?:\/(?!\/)[^\s"'`),;]+)+/g, "$1<local-path>")
|
|
370
|
+
.replace(/(^|[\s"'`=:(])[A-Za-z]:\\[^\s"'`),;]+/g, "$1<local-path>");
|
|
371
|
+
}
|
|
351
372
|
async function resolvedPath(value, label) {
|
|
352
373
|
try {
|
|
353
374
|
return await realpath(value);
|
|
354
375
|
}
|
|
355
376
|
catch (error) {
|
|
356
|
-
throw
|
|
377
|
+
throw openClawError('openclaw_workspace_unavailable', `${label} does not resolve to an accessible local directory.`, error);
|
|
357
378
|
}
|
|
358
379
|
}
|
|
359
|
-
async function openClawJsonProbe(args, options) {
|
|
380
|
+
async function openClawJsonProbe(args, options, timeoutMs = OPENCLAW_RUNTIME_PROBE_TIMEOUT_MS) {
|
|
360
381
|
const spawnProcess = options.spawnProcess ?? spawn;
|
|
361
382
|
let child;
|
|
362
383
|
try {
|
|
363
384
|
child = spawnProcess('openclaw', args, {
|
|
364
385
|
cwd: options.cwd,
|
|
365
|
-
env:
|
|
386
|
+
env: runtimeEnvironment(options.environment),
|
|
366
387
|
shell: false,
|
|
367
388
|
detached: process.platform !== 'win32',
|
|
368
389
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
369
390
|
});
|
|
370
391
|
}
|
|
371
392
|
catch (error) {
|
|
372
|
-
throw
|
|
393
|
+
throw openClawError('openclaw_unavailable', 'Unable to start openclaw. Is it installed and on PATH?', error);
|
|
373
394
|
}
|
|
374
395
|
let stdout = '';
|
|
375
396
|
let stderr = '';
|
|
@@ -405,7 +426,7 @@ async function openClawJsonProbe(args, options) {
|
|
|
405
426
|
const timeout = setTimeout(() => {
|
|
406
427
|
timedOut = true;
|
|
407
428
|
stopProbe();
|
|
408
|
-
},
|
|
429
|
+
}, timeoutMs);
|
|
409
430
|
timeout.unref();
|
|
410
431
|
try {
|
|
411
432
|
const [code] = (await Promise.race([
|
|
@@ -414,18 +435,20 @@ async function openClawJsonProbe(args, options) {
|
|
|
414
435
|
throw error;
|
|
415
436
|
}),
|
|
416
437
|
]));
|
|
417
|
-
if (timedOut)
|
|
418
|
-
throw
|
|
419
|
-
|
|
420
|
-
|
|
438
|
+
if (timedOut) {
|
|
439
|
+
throw openClawError('openclaw_probe_timeout', `OpenClaw preflight timed out while running ${[args[0], args[1]].filter(Boolean).join(' ')}.`);
|
|
440
|
+
}
|
|
441
|
+
if (exceeded) {
|
|
442
|
+
throw openClawError('openclaw_probe_output_too_large', 'OpenClaw preflight exceeded its 1048576 byte output limit.');
|
|
443
|
+
}
|
|
421
444
|
if (code !== 0) {
|
|
422
|
-
throw
|
|
445
|
+
throw openClawError('openclaw_probe_failed', `OpenClaw preflight failed: ${publicOpenClawDiagnostic(stderr.trim()) || `exit ${String(code)}`}`);
|
|
423
446
|
}
|
|
424
447
|
try {
|
|
425
448
|
return JSON.parse(stdout);
|
|
426
449
|
}
|
|
427
450
|
catch (error) {
|
|
428
|
-
throw
|
|
451
|
+
throw openClawError('openclaw_probe_invalid_json', 'OpenClaw preflight returned invalid JSON.', error);
|
|
429
452
|
}
|
|
430
453
|
}
|
|
431
454
|
finally {
|
|
@@ -438,41 +461,94 @@ async function openClawJsonProbe(args, options) {
|
|
|
438
461
|
}
|
|
439
462
|
export async function verifyOpenClawRuntime(options) {
|
|
440
463
|
const agentId = options.profile.runtimeAgentId?.trim();
|
|
441
|
-
if (!agentId)
|
|
442
|
-
throw
|
|
464
|
+
if (!agentId) {
|
|
465
|
+
throw openClawError('openclaw_agent_id_missing', 'OpenClaw requires an explicit configured agent ID.');
|
|
466
|
+
}
|
|
443
467
|
const desiredWorkspace = await resolvedPath(options.cwd, 'CrewX assignment directory');
|
|
444
|
-
|
|
445
|
-
if (!Array.isArray(agents))
|
|
446
|
-
throw
|
|
447
|
-
|
|
468
|
+
let agents = await openClawJsonProbe(['agents', 'list', '--json'], options);
|
|
469
|
+
if (!Array.isArray(agents)) {
|
|
470
|
+
throw openClawError('openclaw_agent_list_invalid', 'OpenClaw returned an invalid agent list.');
|
|
471
|
+
}
|
|
472
|
+
let selected = agents.map(record).find((agent) => agent?.id === agentId);
|
|
473
|
+
if (!selected && agentId.startsWith('crewx-')) {
|
|
474
|
+
await openClawJsonProbe([
|
|
475
|
+
'agents',
|
|
476
|
+
'add',
|
|
477
|
+
agentId,
|
|
478
|
+
'--workspace',
|
|
479
|
+
desiredWorkspace,
|
|
480
|
+
'--non-interactive',
|
|
481
|
+
'--json',
|
|
482
|
+
], options, OPENCLAW_SETUP_TIMEOUT_MS);
|
|
483
|
+
agents = await openClawJsonProbe(['agents', 'list', '--json'], options);
|
|
484
|
+
if (!Array.isArray(agents)) {
|
|
485
|
+
throw openClawError('openclaw_agent_list_invalid', 'OpenClaw returned an invalid agent list after setup.');
|
|
486
|
+
}
|
|
487
|
+
selected = agents.map(record).find((agent) => agent?.id === agentId);
|
|
488
|
+
if (!selected) {
|
|
489
|
+
throw openClawError('openclaw_agent_setup_failed', `CrewX created OpenClaw agent "${agentId}", but OpenClaw did not return it.`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
448
492
|
if (!selected) {
|
|
449
|
-
throw
|
|
493
|
+
throw openClawError('openclaw_agent_missing', `OpenClaw agent "${agentId}" is not configured. Run \`openclaw agents list --json\` to choose one.`);
|
|
450
494
|
}
|
|
451
495
|
if (typeof selected.workspace !== 'string' || !selected.workspace.trim()) {
|
|
452
|
-
throw
|
|
496
|
+
throw openClawError('openclaw_workspace_missing', `OpenClaw agent "${agentId}" does not report a workspace.`);
|
|
453
497
|
}
|
|
454
498
|
const configuredWorkspace = await resolvedPath(selected.workspace, `OpenClaw agent "${agentId}" workspace`);
|
|
455
499
|
if (configuredWorkspace !== desiredWorkspace) {
|
|
456
|
-
throw
|
|
500
|
+
throw openClawError('openclaw_workspace_mismatch', `OpenClaw agent "${agentId}" is bound to a different local workspace than the CrewX-approved folder. OpenClaw has no per-run --cwd; create or select a dedicated OpenClaw agent for the approved folder, then retry.`);
|
|
501
|
+
}
|
|
502
|
+
const models = record(await openClawJsonProbe(['models', 'status', '--agent', agentId, '--json'], options, OPENCLAW_SETUP_TIMEOUT_MS));
|
|
503
|
+
if (!models || (typeof models.agentId === 'string' && models.agentId !== agentId)) {
|
|
504
|
+
throw openClawError('openclaw_model_status_invalid', `OpenClaw did not return model status for agent "${agentId}". Run \`openclaw models status --agent "${agentId}" --json\`.`);
|
|
505
|
+
}
|
|
506
|
+
const resolvedModel = typeof models.resolvedDefault === 'string' && models.resolvedDefault.trim()
|
|
507
|
+
? models.resolvedDefault.trim()
|
|
508
|
+
: typeof models.defaultModel === 'string' && models.defaultModel.trim()
|
|
509
|
+
? models.defaultModel.trim()
|
|
510
|
+
: undefined;
|
|
511
|
+
if (!resolvedModel) {
|
|
512
|
+
throw openClawError('openclaw_model_missing', `OpenClaw agent "${agentId}" has no default model. Configure one, then run \`openclaw models status --agent "${agentId}" --json\`.`);
|
|
513
|
+
}
|
|
514
|
+
const allowedModels = Array.isArray(models.allowed)
|
|
515
|
+
? models.allowed.filter((value) => typeof value === 'string')
|
|
516
|
+
: [];
|
|
517
|
+
if (allowedModels.length > 0 && !allowedModels.includes(resolvedModel)) {
|
|
518
|
+
throw openClawError('openclaw_model_unavailable', `OpenClaw agent "${agentId}" resolves to model "${resolvedModel}", but that model is not allowed. Run \`openclaw models status --agent "${agentId}" --json\` and select an allowed model.`);
|
|
519
|
+
}
|
|
520
|
+
const auth = record(models.auth);
|
|
521
|
+
const missingProviders = Array.isArray(auth?.missingProvidersInUse)
|
|
522
|
+
? auth.missingProvidersInUse.filter((value) => typeof value === 'string')
|
|
523
|
+
: [];
|
|
524
|
+
if (missingProviders.length > 0) {
|
|
525
|
+
throw openClawError('openclaw_auth_missing', `OpenClaw agent "${agentId}" is missing authentication for ${missingProviders.join(', ')}. Run \`openclaw models auth --agent "${agentId}" --help\`, configure the provider, then verify with \`openclaw models status --agent "${agentId}" --json\`.`);
|
|
526
|
+
}
|
|
527
|
+
const authRoutes = Array.isArray(auth?.runtimeAuthRoutes)
|
|
528
|
+
? auth.runtimeAuthRoutes.map(record).filter((route) => route !== undefined)
|
|
529
|
+
: [];
|
|
530
|
+
if (authRoutes.length > 0 &&
|
|
531
|
+
!authRoutes.some((route) => route.status === 'usable')) {
|
|
532
|
+
throw openClawError('openclaw_auth_unusable', `OpenClaw agent "${agentId}" has no usable authentication route for "${resolvedModel}" (credentials may be expired or cooling down). Run \`openclaw models auth --agent "${agentId}" list --json\`, repair authentication, then retry.`);
|
|
457
533
|
}
|
|
458
534
|
const sessionLeaf = options.profile.sessionKey ?? 'main';
|
|
459
535
|
const fullSessionKey = `agent:${agentId}:${sessionLeaf}`;
|
|
460
536
|
const explanation = record(await openClawJsonProbe(['sandbox', 'explain', '--session', fullSessionKey, '--json'], options));
|
|
461
537
|
const sandbox = record(explanation?.sandbox);
|
|
462
538
|
if (explanation?.agentId !== agentId || explanation?.sessionKey !== fullSessionKey || !sandbox) {
|
|
463
|
-
throw
|
|
539
|
+
throw openClawError('openclaw_sandbox_invalid', 'OpenClaw sandbox preflight did not describe the requested agent session.');
|
|
464
540
|
}
|
|
465
541
|
const effectiveHostRoot = typeof sandbox.effectiveHostWorkspaceRoot === 'string'
|
|
466
542
|
? await resolvedPath(sandbox.effectiveHostWorkspaceRoot, 'OpenClaw effective workspace')
|
|
467
543
|
: undefined;
|
|
468
544
|
if (effectiveHostRoot !== desiredWorkspace) {
|
|
469
|
-
throw
|
|
545
|
+
throw openClawError('openclaw_sandbox_workspace_mismatch', 'OpenClaw sandbox policy does not expose the exact CrewX assignment directory.');
|
|
470
546
|
}
|
|
471
547
|
if (sandbox.sessionIsSandboxed === true) {
|
|
472
548
|
if (sandbox.backend !== 'docker' ||
|
|
473
549
|
sandbox.workspaceAccess !== 'rw' ||
|
|
474
550
|
typeof sandbox.runtimeWorkdir !== 'string') {
|
|
475
|
-
throw
|
|
551
|
+
throw openClawError('openclaw_sandbox_read_only', 'OpenClaw must grant rw workspace access for CrewX coding assignments.');
|
|
476
552
|
}
|
|
477
553
|
const mounts = Array.isArray(sandbox.workspaceMounts) ? sandbox.workspaceMounts.map(record) : [];
|
|
478
554
|
let writableWorkspaceMount = false;
|
|
@@ -487,16 +563,16 @@ export async function verifyOpenClawRuntime(options) {
|
|
|
487
563
|
}
|
|
488
564
|
}
|
|
489
565
|
if (!writableWorkspaceMount) {
|
|
490
|
-
throw
|
|
566
|
+
throw openClawError('openclaw_sandbox_mount_missing', 'OpenClaw sandbox policy does not mount the CrewX assignment directory read-write at its runtime workdir.');
|
|
491
567
|
}
|
|
492
568
|
}
|
|
493
569
|
else {
|
|
494
570
|
if (typeof sandbox.runtimeWorkdir !== 'string') {
|
|
495
|
-
throw
|
|
571
|
+
throw openClawError('openclaw_runtime_workdir_missing', 'OpenClaw did not report its direct runtime working directory.');
|
|
496
572
|
}
|
|
497
573
|
const runtimeWorkdir = await resolvedPath(sandbox.runtimeWorkdir, 'OpenClaw runtime working directory');
|
|
498
574
|
if (runtimeWorkdir !== desiredWorkspace) {
|
|
499
|
-
throw
|
|
575
|
+
throw openClawError('openclaw_runtime_workdir_mismatch', 'OpenClaw direct runtime workdir does not match the CrewX assignment directory.');
|
|
500
576
|
}
|
|
501
577
|
}
|
|
502
578
|
}
|
|
@@ -532,6 +608,32 @@ function createLineConsumer(callback) {
|
|
|
532
608
|
},
|
|
533
609
|
};
|
|
534
610
|
}
|
|
611
|
+
const ALLOWED_RUNTIME_CONTROL_VARIABLES = [
|
|
612
|
+
'CREWX_URL',
|
|
613
|
+
'CREWX_TOKEN',
|
|
614
|
+
'CREWX_CLI_PATH',
|
|
615
|
+
];
|
|
616
|
+
/**
|
|
617
|
+
* Coding runtimes inherit provider credentials and ordinary process settings,
|
|
618
|
+
* but never inherit the daemon's CrewX control-plane credentials. The only
|
|
619
|
+
* CrewX variables admitted are short-lived, capability-scoped proxy values
|
|
620
|
+
* supplied separately by the assignment executor.
|
|
621
|
+
*/
|
|
622
|
+
export function runtimeEnvironment(environment = {}, controlEnvironment = {}, inherited = process.env) {
|
|
623
|
+
const result = {};
|
|
624
|
+
for (const [key, value] of Object.entries({ ...inherited, ...environment })) {
|
|
625
|
+
if (key.toUpperCase().startsWith('CREWX_'))
|
|
626
|
+
continue;
|
|
627
|
+
if (value !== undefined)
|
|
628
|
+
result[key] = value;
|
|
629
|
+
}
|
|
630
|
+
for (const key of ALLOWED_RUNTIME_CONTROL_VARIABLES) {
|
|
631
|
+
const value = controlEnvironment[key];
|
|
632
|
+
if (value !== undefined)
|
|
633
|
+
result[key] = value;
|
|
634
|
+
}
|
|
635
|
+
return result;
|
|
636
|
+
}
|
|
535
637
|
async function materializeInvocation(invocation, prompt) {
|
|
536
638
|
if (invocation.prompt.type === 'stdin') {
|
|
537
639
|
return { args: invocation.args, stdin: prompt, cleanup: async () => undefined };
|
|
@@ -582,10 +684,7 @@ async function runSpawnAdapter(options) {
|
|
|
582
684
|
try {
|
|
583
685
|
child = spawnProcess(invocation.command, materialized.args, {
|
|
584
686
|
cwd: options.cwd,
|
|
585
|
-
env:
|
|
586
|
-
...process.env,
|
|
587
|
-
...options.environment,
|
|
588
|
-
},
|
|
687
|
+
env: runtimeEnvironment(options.environment, options.controlEnvironment),
|
|
589
688
|
shell: false,
|
|
590
689
|
detached: process.platform !== 'win32',
|
|
591
690
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -746,7 +845,7 @@ async function runHermesAcp(options) {
|
|
|
746
845
|
try {
|
|
747
846
|
child = spawnProcess('hermes', ['acp'], {
|
|
748
847
|
cwd: options.cwd,
|
|
749
|
-
env:
|
|
848
|
+
env: runtimeEnvironment(options.environment, options.controlEnvironment),
|
|
750
849
|
shell: false,
|
|
751
850
|
detached: process.platform !== 'win32',
|
|
752
851
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -984,17 +1083,15 @@ export async function runAdapter(options) {
|
|
|
984
1083
|
return runHermesAcp(options);
|
|
985
1084
|
return runSpawnAdapter(options);
|
|
986
1085
|
}
|
|
987
|
-
export async function probeAdapter(adapter, spawnProcess = spawn, timeoutMs
|
|
1086
|
+
export async function probeAdapter(adapter, spawnProcess = spawn, timeoutMs) {
|
|
1087
|
+
const plan = adapterProbePlan(adapter);
|
|
1088
|
+
const effectiveTimeoutMs = timeoutMs ?? plan.timeoutMs;
|
|
988
1089
|
const command = adapter;
|
|
989
|
-
const probeArgs =
|
|
990
|
-
? ['acp', '--check']
|
|
991
|
-
: adapter === 'openclaw'
|
|
992
|
-
? ['agents', 'list', '--json']
|
|
993
|
-
: ['--version'];
|
|
1090
|
+
const probeArgs = plan.args;
|
|
994
1091
|
let child;
|
|
995
1092
|
try {
|
|
996
1093
|
child = spawnProcess(command, probeArgs, {
|
|
997
|
-
env:
|
|
1094
|
+
env: runtimeEnvironment(),
|
|
998
1095
|
shell: false,
|
|
999
1096
|
detached: process.platform !== 'win32',
|
|
1000
1097
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -1016,7 +1113,7 @@ export async function probeAdapter(adapter, spawnProcess = spawn, timeoutMs = 5_
|
|
|
1016
1113
|
});
|
|
1017
1114
|
let timedOut = false;
|
|
1018
1115
|
let terminating = false;
|
|
1019
|
-
const terminationGraceMs = Math.min(1_000, Math.max(25,
|
|
1116
|
+
const terminationGraceMs = Math.min(1_000, Math.max(25, effectiveTimeoutMs));
|
|
1020
1117
|
const abort = () => {
|
|
1021
1118
|
if (lifecycle.isClosed() || child.exitCode !== null || terminating)
|
|
1022
1119
|
return;
|
|
@@ -1034,7 +1131,7 @@ export async function probeAdapter(adapter, spawnProcess = spawn, timeoutMs = 5_
|
|
|
1034
1131
|
destroyChildPipes(child);
|
|
1035
1132
|
const message = outputLimitExceeded
|
|
1036
1133
|
? 'probe output exceeded 1048576 bytes'
|
|
1037
|
-
: `probe timed out after ${String(
|
|
1134
|
+
: `probe timed out after ${String(effectiveTimeoutMs)}ms`;
|
|
1038
1135
|
rejectTermination?.(new CliError(message));
|
|
1039
1136
|
}, terminationGraceMs + 100);
|
|
1040
1137
|
};
|
|
@@ -1060,7 +1157,7 @@ export async function probeAdapter(adapter, spawnProcess = spawn, timeoutMs = 5_
|
|
|
1060
1157
|
const timeout = setTimeout(() => {
|
|
1061
1158
|
timedOut = true;
|
|
1062
1159
|
abort();
|
|
1063
|
-
},
|
|
1160
|
+
}, effectiveTimeoutMs);
|
|
1064
1161
|
try {
|
|
1065
1162
|
const [code] = (await Promise.race([
|
|
1066
1163
|
lifecycle.close,
|
|
@@ -1070,7 +1167,7 @@ export async function probeAdapter(adapter, spawnProcess = spawn, timeoutMs = 5_
|
|
|
1070
1167
|
}),
|
|
1071
1168
|
]));
|
|
1072
1169
|
if (timedOut)
|
|
1073
|
-
return { available: false, error: `probe timed out after ${String(
|
|
1170
|
+
return { available: false, error: `probe timed out after ${String(effectiveTimeoutMs)}ms` };
|
|
1074
1171
|
if (outputLimitExceeded)
|
|
1075
1172
|
return { available: false, error: 'probe output exceeded 1048576 bytes' };
|
|
1076
1173
|
const version = adapter === 'hermes' || adapter === 'openclaw'
|