crewx-agent-cli 0.2.3 → 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 +79 -4
  2. package/dist/adapters.d.ts +12 -0
  3. package/dist/adapters.d.ts.map +1 -1
  4. package/dist/adapters.js +112 -40
  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 +32 -1
  23. package/dist/daemon.d.ts.map +1 -1
  24. package/dist/daemon.js +479 -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
 
@@ -58,6 +111,13 @@ Both join mode and `crewx daemon` run in the foreground. Start either command in
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
@@ -9,6 +9,13 @@ import { CliError } from './errors.js';
9
9
  const DEFAULT_RUNTIME_PROBE_TIMEOUT_MS = 5_000;
10
10
  const OPENCLAW_RUNTIME_PROBE_TIMEOUT_MS = 30_000;
11
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
+ }
12
19
  const CLAUDE_STANDARD_CREWX_TOOLS = [
13
20
  'WebSearch',
14
21
  'WebFetch',
@@ -351,12 +358,23 @@ function record(value) {
351
358
  ? value
352
359
  : undefined;
353
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
+ }
354
372
  async function resolvedPath(value, label) {
355
373
  try {
356
374
  return await realpath(value);
357
375
  }
358
376
  catch (error) {
359
- 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);
360
378
  }
361
379
  }
362
380
  async function openClawJsonProbe(args, options, timeoutMs = OPENCLAW_RUNTIME_PROBE_TIMEOUT_MS) {
@@ -365,14 +383,14 @@ async function openClawJsonProbe(args, options, timeoutMs = OPENCLAW_RUNTIME_PRO
365
383
  try {
366
384
  child = spawnProcess('openclaw', args, {
367
385
  cwd: options.cwd,
368
- env: { ...process.env, ...options.environment },
386
+ env: runtimeEnvironment(options.environment),
369
387
  shell: false,
370
388
  detached: process.platform !== 'win32',
371
389
  stdio: ['pipe', 'pipe', 'pipe'],
372
390
  });
373
391
  }
374
392
  catch (error) {
375
- 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);
376
394
  }
377
395
  let stdout = '';
378
396
  let stderr = '';
@@ -417,18 +435,20 @@ async function openClawJsonProbe(args, options, timeoutMs = OPENCLAW_RUNTIME_PRO
417
435
  throw error;
418
436
  }),
419
437
  ]));
420
- if (timedOut)
421
- throw new CliError(`OpenClaw preflight timed out: openclaw ${args.join(' ')}`);
422
- if (exceeded)
423
- 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
+ }
424
444
  if (code !== 0) {
425
- 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)}`}`);
426
446
  }
427
447
  try {
428
448
  return JSON.parse(stdout);
429
449
  }
430
450
  catch (error) {
431
- throw new CliError('OpenClaw preflight returned invalid JSON.', 1, { cause: error });
451
+ throw openClawError('openclaw_probe_invalid_json', 'OpenClaw preflight returned invalid JSON.', error);
432
452
  }
433
453
  }
434
454
  finally {
@@ -441,12 +461,14 @@ async function openClawJsonProbe(args, options, timeoutMs = OPENCLAW_RUNTIME_PRO
441
461
  }
442
462
  export async function verifyOpenClawRuntime(options) {
443
463
  const agentId = options.profile.runtimeAgentId?.trim();
444
- if (!agentId)
445
- 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
+ }
446
467
  const desiredWorkspace = await resolvedPath(options.cwd, 'CrewX assignment directory');
447
468
  let agents = await openClawJsonProbe(['agents', 'list', '--json'], options);
448
- if (!Array.isArray(agents))
449
- throw new CliError('OpenClaw returned an invalid agent list.');
469
+ if (!Array.isArray(agents)) {
470
+ throw openClawError('openclaw_agent_list_invalid', 'OpenClaw returned an invalid agent list.');
471
+ }
450
472
  let selected = agents.map(record).find((agent) => agent?.id === agentId);
451
473
  if (!selected && agentId.startsWith('crewx-')) {
452
474
  await openClawJsonProbe([
@@ -459,41 +481,74 @@ export async function verifyOpenClawRuntime(options) {
459
481
  '--json',
460
482
  ], options, OPENCLAW_SETUP_TIMEOUT_MS);
461
483
  agents = await openClawJsonProbe(['agents', 'list', '--json'], options);
462
- if (!Array.isArray(agents))
463
- throw new CliError('OpenClaw returned an invalid agent list after setup.');
484
+ if (!Array.isArray(agents)) {
485
+ throw openClawError('openclaw_agent_list_invalid', 'OpenClaw returned an invalid agent list after setup.');
486
+ }
464
487
  selected = agents.map(record).find((agent) => agent?.id === agentId);
465
488
  if (!selected) {
466
- throw new CliError(`CrewX created OpenClaw agent "${agentId}", but OpenClaw did not return it.`);
489
+ throw openClawError('openclaw_agent_setup_failed', `CrewX created OpenClaw agent "${agentId}", but OpenClaw did not return it.`);
467
490
  }
468
491
  }
469
492
  if (!selected) {
470
- 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.`);
471
494
  }
472
495
  if (typeof selected.workspace !== 'string' || !selected.workspace.trim()) {
473
- 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.`);
474
497
  }
475
498
  const configuredWorkspace = await resolvedPath(selected.workspace, `OpenClaw agent "${agentId}" workspace`);
476
499
  if (configuredWorkspace !== desiredWorkspace) {
477
- 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.`);
478
533
  }
479
534
  const sessionLeaf = options.profile.sessionKey ?? 'main';
480
535
  const fullSessionKey = `agent:${agentId}:${sessionLeaf}`;
481
536
  const explanation = record(await openClawJsonProbe(['sandbox', 'explain', '--session', fullSessionKey, '--json'], options));
482
537
  const sandbox = record(explanation?.sandbox);
483
538
  if (explanation?.agentId !== agentId || explanation?.sessionKey !== fullSessionKey || !sandbox) {
484
- 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.');
485
540
  }
486
541
  const effectiveHostRoot = typeof sandbox.effectiveHostWorkspaceRoot === 'string'
487
542
  ? await resolvedPath(sandbox.effectiveHostWorkspaceRoot, 'OpenClaw effective workspace')
488
543
  : undefined;
489
544
  if (effectiveHostRoot !== desiredWorkspace) {
490
- 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.');
491
546
  }
492
547
  if (sandbox.sessionIsSandboxed === true) {
493
548
  if (sandbox.backend !== 'docker' ||
494
549
  sandbox.workspaceAccess !== 'rw' ||
495
550
  typeof sandbox.runtimeWorkdir !== 'string') {
496
- 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.');
497
552
  }
498
553
  const mounts = Array.isArray(sandbox.workspaceMounts) ? sandbox.workspaceMounts.map(record) : [];
499
554
  let writableWorkspaceMount = false;
@@ -508,16 +563,16 @@ export async function verifyOpenClawRuntime(options) {
508
563
  }
509
564
  }
510
565
  if (!writableWorkspaceMount) {
511
- 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.');
512
567
  }
513
568
  }
514
569
  else {
515
570
  if (typeof sandbox.runtimeWorkdir !== 'string') {
516
- 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.');
517
572
  }
518
573
  const runtimeWorkdir = await resolvedPath(sandbox.runtimeWorkdir, 'OpenClaw runtime working directory');
519
574
  if (runtimeWorkdir !== desiredWorkspace) {
520
- 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.');
521
576
  }
522
577
  }
523
578
  }
@@ -553,6 +608,32 @@ function createLineConsumer(callback) {
553
608
  },
554
609
  };
555
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
+ }
556
637
  async function materializeInvocation(invocation, prompt) {
557
638
  if (invocation.prompt.type === 'stdin') {
558
639
  return { args: invocation.args, stdin: prompt, cleanup: async () => undefined };
@@ -603,10 +684,7 @@ async function runSpawnAdapter(options) {
603
684
  try {
604
685
  child = spawnProcess(invocation.command, materialized.args, {
605
686
  cwd: options.cwd,
606
- env: {
607
- ...process.env,
608
- ...options.environment,
609
- },
687
+ env: runtimeEnvironment(options.environment, options.controlEnvironment),
610
688
  shell: false,
611
689
  detached: process.platform !== 'win32',
612
690
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -767,7 +845,7 @@ async function runHermesAcp(options) {
767
845
  try {
768
846
  child = spawnProcess('hermes', ['acp'], {
769
847
  cwd: options.cwd,
770
- env: { ...process.env, ...options.environment },
848
+ env: runtimeEnvironment(options.environment, options.controlEnvironment),
771
849
  shell: false,
772
850
  detached: process.platform !== 'win32',
773
851
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -1006,20 +1084,14 @@ export async function runAdapter(options) {
1006
1084
  return runSpawnAdapter(options);
1007
1085
  }
1008
1086
  export async function probeAdapter(adapter, spawnProcess = spawn, timeoutMs) {
1009
- const effectiveTimeoutMs = timeoutMs
1010
- ?? (adapter === 'openclaw'
1011
- ? OPENCLAW_RUNTIME_PROBE_TIMEOUT_MS
1012
- : DEFAULT_RUNTIME_PROBE_TIMEOUT_MS);
1087
+ const plan = adapterProbePlan(adapter);
1088
+ const effectiveTimeoutMs = timeoutMs ?? plan.timeoutMs;
1013
1089
  const command = adapter;
1014
- const probeArgs = adapter === 'hermes'
1015
- ? ['acp', '--check']
1016
- : adapter === 'openclaw'
1017
- ? ['agents', 'list', '--json']
1018
- : ['--version'];
1090
+ const probeArgs = plan.args;
1019
1091
  let child;
1020
1092
  try {
1021
1093
  child = spawnProcess(command, probeArgs, {
1022
- env: process.env,
1094
+ env: runtimeEnvironment(),
1023
1095
  shell: false,
1024
1096
  detached: process.platform !== 'win32',
1025
1097
  stdio: ['pipe', 'pipe', 'pipe'],