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.
Files changed (53) hide show
  1. package/README.md +80 -5
  2. package/dist/adapters.d.ts +12 -0
  3. package/dist/adapters.d.ts.map +1 -1
  4. package/dist/adapters.js +139 -42
  5. package/dist/adapters.js.map +1 -1
  6. package/dist/api.d.ts +40 -17
  7. package/dist/api.d.ts.map +1 -1
  8. package/dist/api.js +187 -11
  9. package/dist/api.js.map +1 -1
  10. package/dist/bridge-watchdog.d.ts +10 -0
  11. package/dist/bridge-watchdog.d.ts.map +1 -0
  12. package/dist/bridge-watchdog.js +45 -0
  13. package/dist/bridge-watchdog.js.map +1 -0
  14. package/dist/config.d.ts +9 -1
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js +53 -2
  17. package/dist/config.js.map +1 -1
  18. package/dist/constants.d.ts +6 -1
  19. package/dist/constants.d.ts.map +1 -1
  20. package/dist/constants.js +11 -1
  21. package/dist/constants.js.map +1 -1
  22. package/dist/daemon.d.ts +34 -2
  23. package/dist/daemon.d.ts.map +1 -1
  24. package/dist/daemon.js +495 -100
  25. package/dist/daemon.js.map +1 -1
  26. package/dist/errors.d.ts +4 -1
  27. package/dist/errors.d.ts.map +1 -1
  28. package/dist/errors.js +3 -1
  29. package/dist/errors.js.map +1 -1
  30. package/dist/index.d.ts +3 -2
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +17 -6
  33. package/dist/index.js.map +1 -1
  34. package/dist/join.d.ts +1 -0
  35. package/dist/join.d.ts.map +1 -1
  36. package/dist/join.js +13 -0
  37. package/dist/join.js.map +1 -1
  38. package/dist/outbox.d.ts +22 -0
  39. package/dist/outbox.d.ts.map +1 -0
  40. package/dist/outbox.js +189 -0
  41. package/dist/outbox.js.map +1 -0
  42. package/dist/process-lock.d.ts +31 -0
  43. package/dist/process-lock.d.ts.map +1 -0
  44. package/dist/process-lock.js +272 -0
  45. package/dist/process-lock.js.map +1 -0
  46. package/dist/prompt.d.ts.map +1 -1
  47. package/dist/prompt.js +7 -0
  48. package/dist/prompt.js.map +1 -1
  49. package/dist/runtime-proxy.d.ts +18 -0
  50. package/dist/runtime-proxy.d.ts.map +1 -0
  51. package/dist/runtime-proxy.js +221 -0
  52. package/dist/runtime-proxy.js.map +1 -0
  53. 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 a CrewX workspace. The runner stays on your machine, so the agent uses its existing login, configuration, tools, and access to the selected working directory.
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 the decoded credential in memory and supplies the CrewX URL and token to child agents as `CREWX_URL` and `CREWX_TOKEN` so they can use the scoped `crewx task`, `doc`, `memory`, and `integration` commands. A locally executed agent has the same trust boundary as the user account running it.
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 inside a safe fallback directory. For each assignment, CrewX uses the project channel's local path first, the agent profile's default working directory second, and `--cwd` last. Use `--once` for one poll and `--poll-interval 5000` to change the polling cadence.
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** runs an explicitly selected 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. Immediately before spawning, CrewX runs `openclaw agents list --json` and `openclaw sandbox explain` for the exact task session. The configured agent workspace must resolve to the assignment directory; a sandboxed session must mount that exact directory read-write at its runtime workdir. OpenClaw owns its persistent tool policy and approval behavior, and has no per-run `--cwd`, so configure a dedicated agent with `openclaw agents add <name> --workspace "/path/to/project" --non-interactive --json` when needed.
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 arbitrary executable safe. The selected program inherits the runner's operating-system permissions and environment, including CrewX connection variables in join mode. Do not use commands from an untrusted profile or run them in an untrusted working directory. Explicitly wrapping a command in a shell recreates the shell-risk boundary and is not recommended.
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:
@@ -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;
@@ -1 +1 @@
1
- {"version":3,"file":"adapters.d.ts","sourceRoot":"","sources":["../src/adapters.ts"],"names":[],"mappings":"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"}
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 new CliError(`${label} does not resolve to an accessible local directory: ${value}`, 1, { cause: error });
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: { ...process.env, ...options.environment },
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 new CliError('Unable to start openclaw. Is it installed and on PATH?', 1, { cause: error });
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
- }, 15_000);
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 new CliError(`OpenClaw preflight timed out: openclaw ${args.join(' ')}`);
419
- if (exceeded)
420
- throw new CliError('OpenClaw preflight exceeded its 1048576 byte output limit.');
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 new CliError(`OpenClaw preflight failed: ${stderr.trim() || `exit ${String(code)}`}`);
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 new CliError('OpenClaw preflight returned invalid JSON.', 1, { cause: error });
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 new CliError('OpenClaw requires an explicit configured agent ID.');
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
- const agents = await openClawJsonProbe(['agents', 'list', '--json'], options);
445
- if (!Array.isArray(agents))
446
- throw new CliError('OpenClaw returned an invalid agent list.');
447
- const selected = agents.map(record).find((agent) => agent?.id === agentId);
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 new CliError(`OpenClaw agent "${agentId}" is not configured. Run \`openclaw agents list --json\` to choose one.`);
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 new CliError(`OpenClaw agent "${agentId}" does not report a workspace.`);
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 new CliError(`OpenClaw agent "${agentId}" uses workspace "${configuredWorkspace}", but this CrewX assignment requires "${desiredWorkspace}". OpenClaw has no per-run --cwd. Create a dedicated agent with: openclaw agents add <name> --workspace "${desiredWorkspace}" --non-interactive --json`);
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 new CliError('OpenClaw sandbox preflight did not describe the requested agent session.');
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 new CliError('OpenClaw sandbox policy does not expose the exact CrewX assignment directory.');
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 new CliError('OpenClaw must grant rw workspace access for CrewX coding assignments.');
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 new CliError('OpenClaw sandbox policy does not mount the CrewX assignment directory read-write at its runtime workdir.');
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 new CliError('OpenClaw did not report its direct runtime working directory.');
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 new CliError('OpenClaw direct runtime workdir does not match the CrewX assignment directory.');
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: { ...process.env, ...options.environment },
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 = 5_000) {
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 = adapter === 'hermes'
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: process.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, timeoutMs));
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(timeoutMs)}ms`;
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
- }, timeoutMs);
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(timeoutMs)}ms` };
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'