@phnx-labs/agents-cli 1.20.71 → 1.20.73

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 (238) hide show
  1. package/CHANGELOG.md +325 -1
  2. package/README.md +40 -4
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +12 -0
  5. package/dist/commands/activity.js +68 -0
  6. package/dist/commands/apply.d.ts +13 -0
  7. package/dist/commands/apply.js +28 -3
  8. package/dist/commands/browser.js +1 -1
  9. package/dist/commands/cloud.js +21 -11
  10. package/dist/commands/commands.js +2 -0
  11. package/dist/commands/doctor.js +8 -4
  12. package/dist/commands/events.d.ts +11 -10
  13. package/dist/commands/events.js +30 -21
  14. package/dist/commands/exec.d.ts +34 -0
  15. package/dist/commands/exec.js +337 -39
  16. package/dist/commands/feed.d.ts +13 -0
  17. package/dist/commands/feed.js +130 -29
  18. package/dist/commands/hq.d.ts +2 -0
  19. package/dist/commands/hq.js +58 -0
  20. package/dist/commands/lease.d.ts +19 -0
  21. package/dist/commands/lease.js +138 -11
  22. package/dist/commands/login.d.ts +2 -0
  23. package/dist/commands/login.js +123 -0
  24. package/dist/commands/logs.js +39 -10
  25. package/dist/commands/mcp.js +2 -0
  26. package/dist/commands/mine.d.ts +27 -0
  27. package/dist/commands/mine.js +207 -0
  28. package/dist/commands/monitors.js +79 -49
  29. package/dist/commands/plugins.js +1 -0
  30. package/dist/commands/profiles.js +158 -0
  31. package/dist/commands/repo.js +2 -60
  32. package/dist/commands/resource-view.d.ts +2 -0
  33. package/dist/commands/resource-view.js +8 -0
  34. package/dist/commands/resources.d.ts +5 -0
  35. package/dist/commands/resources.js +95 -0
  36. package/dist/commands/routines.d.ts +2 -0
  37. package/dist/commands/routines.js +139 -37
  38. package/dist/commands/secrets.js +234 -47
  39. package/dist/commands/send.d.ts +15 -0
  40. package/dist/commands/send.js +62 -0
  41. package/dist/commands/sessions-tail.d.ts +1 -0
  42. package/dist/commands/sessions-tail.js +20 -9
  43. package/dist/commands/sessions.d.ts +6 -0
  44. package/dist/commands/sessions.js +26 -3
  45. package/dist/commands/setup-mine.d.ts +18 -0
  46. package/dist/commands/setup-mine.js +106 -0
  47. package/dist/commands/setup-share.js +2 -2
  48. package/dist/commands/setup.js +3 -1
  49. package/dist/commands/share.d.ts +8 -1
  50. package/dist/commands/share.js +122 -37
  51. package/dist/commands/skills.js +2 -0
  52. package/dist/commands/ssh.d.ts +9 -0
  53. package/dist/commands/ssh.js +196 -26
  54. package/dist/commands/status.js +8 -2
  55. package/dist/commands/subagents.js +3 -1
  56. package/dist/commands/sync.js +1 -1
  57. package/dist/commands/teams.js +11 -2
  58. package/dist/commands/uninstall.d.ts +11 -0
  59. package/dist/commands/uninstall.js +158 -0
  60. package/dist/commands/utils.d.ts +29 -0
  61. package/dist/commands/utils.js +24 -0
  62. package/dist/commands/versions.js +120 -11
  63. package/dist/index.js +83 -12
  64. package/dist/lib/activity.d.ts +124 -0
  65. package/dist/lib/activity.js +542 -0
  66. package/dist/lib/agents.js +16 -5
  67. package/dist/lib/artifact-actions.d.ts +1 -1
  68. package/dist/lib/artifact-actions.js +1 -1
  69. package/dist/lib/ask-classifier.d.ts +2 -1
  70. package/dist/lib/ask-classifier.js +3 -3
  71. package/dist/lib/auth-health.d.ts +32 -4
  72. package/dist/lib/auth-health.js +40 -1
  73. package/dist/lib/auto-pull.js +8 -1
  74. package/dist/lib/brand.d.ts +40 -0
  75. package/dist/lib/brand.js +122 -0
  76. package/dist/lib/browser/drivers/ssh.js +4 -7
  77. package/dist/lib/browser/service.js +6 -8
  78. package/dist/lib/channels/providers/index.d.ts +2 -0
  79. package/dist/lib/channels/providers/index.js +20 -0
  80. package/dist/lib/channels/providers/mailbox.d.ts +2 -0
  81. package/dist/lib/channels/providers/mailbox.js +26 -0
  82. package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
  83. package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
  84. package/dist/lib/channels/providers/rush.d.ts +6 -0
  85. package/dist/lib/channels/providers/rush.js +55 -0
  86. package/dist/lib/channels/registry.d.ts +42 -0
  87. package/dist/lib/channels/registry.js +20 -0
  88. package/dist/lib/channels/resolve.d.ts +11 -0
  89. package/dist/lib/channels/resolve.js +12 -0
  90. package/dist/lib/cli-resources.d.ts +18 -0
  91. package/dist/lib/cli-resources.js +53 -4
  92. package/dist/lib/cloud/codex.js +6 -3
  93. package/dist/lib/cloud/factory.d.ts +1 -0
  94. package/dist/lib/cloud/factory.js +10 -4
  95. package/dist/lib/cloud/rush.d.ts +3 -1
  96. package/dist/lib/cloud/rush.js +6 -1
  97. package/dist/lib/cloud/types.d.ts +2 -0
  98. package/dist/lib/codex-home.d.ts +35 -0
  99. package/dist/lib/codex-home.js +136 -0
  100. package/dist/lib/crabbox/cli.d.ts +44 -0
  101. package/dist/lib/crabbox/cli.js +120 -20
  102. package/dist/lib/crabbox/lease.d.ts +52 -1
  103. package/dist/lib/crabbox/lease.js +117 -16
  104. package/dist/lib/crabbox/progress.d.ts +31 -0
  105. package/dist/lib/crabbox/progress.js +76 -0
  106. package/dist/lib/crabbox/runtimes.d.ts +4 -0
  107. package/dist/lib/crabbox/runtimes.js +33 -8
  108. package/dist/lib/crabbox/setup-copy.d.ts +87 -0
  109. package/dist/lib/crabbox/setup-copy.js +127 -0
  110. package/dist/lib/daemon.d.ts +14 -5
  111. package/dist/lib/daemon.js +52 -9
  112. package/dist/lib/drive-sync.js +6 -3
  113. package/dist/lib/event-stream.d.ts +36 -0
  114. package/dist/lib/event-stream.js +68 -0
  115. package/dist/lib/events.d.ts +1 -1
  116. package/dist/lib/exec.d.ts +11 -0
  117. package/dist/lib/exec.js +23 -2
  118. package/dist/lib/feed-policy.d.ts +1 -1
  119. package/dist/lib/feed-policy.js +14 -9
  120. package/dist/lib/feed-ranking.d.ts +32 -0
  121. package/dist/lib/feed-ranking.js +224 -0
  122. package/dist/lib/feed.d.ts +35 -2
  123. package/dist/lib/feed.js +66 -3
  124. package/dist/lib/fleet/auth-sync.d.ts +53 -0
  125. package/dist/lib/fleet/auth-sync.js +92 -0
  126. package/dist/lib/fleet/remote-login.d.ts +187 -0
  127. package/dist/lib/fleet/remote-login.js +556 -0
  128. package/dist/lib/format.d.ts +30 -3
  129. package/dist/lib/format.js +34 -5
  130. package/dist/lib/fs-atomic.d.ts +7 -2
  131. package/dist/lib/fs-atomic.js +8 -12
  132. package/dist/lib/git.d.ts +16 -0
  133. package/dist/lib/git.js +79 -2
  134. package/dist/lib/heal.js +11 -3
  135. package/dist/lib/hosts/logs.d.ts +12 -0
  136. package/dist/lib/hosts/logs.js +18 -0
  137. package/dist/lib/hosts/progress.d.ts +28 -10
  138. package/dist/lib/hosts/progress.js +79 -22
  139. package/dist/lib/hosts/remote-cmd.js +4 -0
  140. package/dist/lib/hq/floor.d.ts +87 -0
  141. package/dist/lib/hq/floor.js +226 -0
  142. package/dist/lib/menubar/install-menubar.js +14 -20
  143. package/dist/lib/notify.d.ts +1 -0
  144. package/dist/lib/notify.js +3 -3
  145. package/dist/lib/open-url.d.ts +2 -0
  146. package/dist/lib/open-url.js +19 -0
  147. package/dist/lib/openclaw-keychain.d.ts +56 -0
  148. package/dist/lib/openclaw-keychain.js +236 -0
  149. package/dist/lib/overdue.js +10 -0
  150. package/dist/lib/permissions.d.ts +44 -1
  151. package/dist/lib/permissions.js +284 -7
  152. package/dist/lib/project-launch.d.ts +6 -12
  153. package/dist/lib/project-launch.js +13 -228
  154. package/dist/lib/project-resources.d.ts +7 -0
  155. package/dist/lib/project-resources.js +291 -0
  156. package/dist/lib/pty-client.d.ts +27 -0
  157. package/dist/lib/pty-client.js +136 -10
  158. package/dist/lib/refresh.js +14 -10
  159. package/dist/lib/resource-profiles.d.ts +26 -0
  160. package/dist/lib/resource-profiles.js +157 -0
  161. package/dist/lib/resources/permissions.js +7 -1
  162. package/dist/lib/resources/types.d.ts +1 -1
  163. package/dist/lib/resources.d.ts +1 -1
  164. package/dist/lib/resources.js +32 -3
  165. package/dist/lib/routines.d.ts +11 -0
  166. package/dist/lib/routines.js +56 -15
  167. package/dist/lib/runner.d.ts +1 -0
  168. package/dist/lib/runner.js +66 -5
  169. package/dist/lib/secrets/bundles.d.ts +16 -3
  170. package/dist/lib/secrets/bundles.js +206 -37
  171. package/dist/lib/secrets/icloud-import.d.ts +2 -2
  172. package/dist/lib/secrets/icloud-import.js +9 -6
  173. package/dist/lib/secrets/mcp.js +1 -1
  174. package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
  175. package/dist/lib/secrets/vault-age-helper.js +34 -0
  176. package/dist/lib/secrets/vault.d.ts +49 -0
  177. package/dist/lib/secrets/vault.js +397 -0
  178. package/dist/lib/self-heal/checks/path.js +3 -1
  179. package/dist/lib/self-heal/checks/shadowing.js +10 -2
  180. package/dist/lib/self-heal/checks/shims.js +19 -11
  181. package/dist/lib/session/cloud.d.ts +2 -2
  182. package/dist/lib/session/cloud.js +2 -2
  183. package/dist/lib/session/db.d.ts +4 -0
  184. package/dist/lib/session/db.js +44 -7
  185. package/dist/lib/session/discover.d.ts +2 -0
  186. package/dist/lib/session/discover.js +114 -3
  187. package/dist/lib/session/stream-render.d.ts +3 -0
  188. package/dist/lib/session/stream-render.js +130 -0
  189. package/dist/lib/session/types.d.ts +6 -0
  190. package/dist/lib/share/analytics.d.ts +13 -0
  191. package/dist/lib/share/analytics.js +45 -0
  192. package/dist/lib/share/config.d.ts +19 -5
  193. package/dist/lib/share/config.js +44 -8
  194. package/dist/lib/share/provision.d.ts +58 -6
  195. package/dist/lib/share/provision.js +97 -22
  196. package/dist/lib/share/publish.d.ts +36 -13
  197. package/dist/lib/share/publish.js +55 -8
  198. package/dist/lib/share/worker-template.js +83 -17
  199. package/dist/lib/shims.d.ts +36 -0
  200. package/dist/lib/shims.js +120 -16
  201. package/dist/lib/ssh-exec.d.ts +14 -0
  202. package/dist/lib/ssh-exec.js +57 -0
  203. package/dist/lib/staleness/detectors/hooks.js +25 -1
  204. package/dist/lib/staleness/detectors/permissions.js +66 -0
  205. package/dist/lib/staleness/detectors/workflows.js +20 -0
  206. package/dist/lib/staleness/writers/hooks.js +58 -4
  207. package/dist/lib/staleness/writers/permissions.js +2 -2
  208. package/dist/lib/staleness/writers/skills.js +1 -1
  209. package/dist/lib/staleness/writers/sources.d.ts +10 -1
  210. package/dist/lib/staleness/writers/sources.js +68 -1
  211. package/dist/lib/star-nudge.d.ts +45 -0
  212. package/dist/lib/star-nudge.js +91 -0
  213. package/dist/lib/startup/command-registry.d.ts +7 -1
  214. package/dist/lib/startup/command-registry.js +19 -3
  215. package/dist/lib/state.d.ts +2 -0
  216. package/dist/lib/state.js +3 -0
  217. package/dist/lib/subagents-registry.d.ts +2 -0
  218. package/dist/lib/subagents-registry.js +3 -0
  219. package/dist/lib/teams/parsers.js +214 -6
  220. package/dist/lib/teams/supervisor.d.ts +7 -0
  221. package/dist/lib/teams/supervisor.js +2 -1
  222. package/dist/lib/template.d.ts +1 -1
  223. package/dist/lib/template.js +1 -1
  224. package/dist/lib/triggers/webhook.js +36 -3
  225. package/dist/lib/types.d.ts +73 -0
  226. package/dist/lib/uninstall.d.ts +84 -0
  227. package/dist/lib/uninstall.js +347 -0
  228. package/dist/lib/usage.d.ts +13 -1
  229. package/dist/lib/usage.js +32 -14
  230. package/dist/lib/version.d.ts +40 -0
  231. package/dist/lib/version.js +94 -16
  232. package/dist/lib/versions.d.ts +27 -0
  233. package/dist/lib/versions.js +248 -79
  234. package/dist/lib/workflows.d.ts +2 -0
  235. package/dist/lib/workflows.js +88 -0
  236. package/package.json +2 -1
  237. package/dist/commands/daemon.d.ts +0 -10
  238. package/dist/commands/daemon.js +0 -121
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Channel-provider registry for `agents send` / `agents notify`.
3
+ *
4
+ * One primitive, many channels. A provider knows how to deliver a message over
5
+ * exactly one channel — the agent mailbox, a rush-daemon gateway (telegram /
6
+ * imessage / slack / discord), openclaw, or a future "app". Providers register
7
+ * themselves at module load; the command layer only ever calls
8
+ * `resolveChannelProvider(name).send(...)` — no channel is special-cased above
9
+ * this seam, so new channels (apps) slot in without touching the command.
10
+ */
11
+ export interface SendOptions {
12
+ /** Channel-specific recipient id: a chat id, a mailbox/agent id, a Slack C0…, etc. */
13
+ target: string;
14
+ /** Channel thread id / timestamp (slack/telegram threads). */
15
+ thread?: string;
16
+ /** Absolute paths to file attachments. */
17
+ attachments?: string[];
18
+ /** Sender label (used by the mailbox provider). */
19
+ from?: string;
20
+ /** Resolve + build the delivery but do not actually send. */
21
+ dryRun?: boolean;
22
+ }
23
+ export interface SendResult {
24
+ ok: boolean;
25
+ /** Channel/provider name as surfaced back to the caller. */
26
+ channel: string;
27
+ /** Resolved recipient id, echoed for --json parity with `rush send`. */
28
+ id: string;
29
+ error?: string;
30
+ /** Echoed attachment paths (rush shape parity). */
31
+ attachments?: string[];
32
+ /** Mailbox provider returns the enqueued message id. */
33
+ msgId?: string;
34
+ }
35
+ export interface ChannelProvider {
36
+ /** Stable name used in `--channel` and as a `notify.transports` value. */
37
+ name: string;
38
+ send(text: string, opts: SendOptions): Promise<SendResult>;
39
+ }
40
+ export declare function registerChannelProvider(p: ChannelProvider): void;
41
+ export declare function resolveChannelProvider(name: string): ChannelProvider | undefined;
42
+ export declare function listChannelProviders(): string[];
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Channel-provider registry for `agents send` / `agents notify`.
3
+ *
4
+ * One primitive, many channels. A provider knows how to deliver a message over
5
+ * exactly one channel — the agent mailbox, a rush-daemon gateway (telegram /
6
+ * imessage / slack / discord), openclaw, or a future "app". Providers register
7
+ * themselves at module load; the command layer only ever calls
8
+ * `resolveChannelProvider(name).send(...)` — no channel is special-cased above
9
+ * this seam, so new channels (apps) slot in without touching the command.
10
+ */
11
+ const REGISTRY = new Map();
12
+ export function registerChannelProvider(p) {
13
+ REGISTRY.set(p.name, p);
14
+ }
15
+ export function resolveChannelProvider(name) {
16
+ return REGISTRY.get(name);
17
+ }
18
+ export function listChannelProviders() {
19
+ return [...REGISTRY.keys()].sort();
20
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Transport resolution: map a user-facing channel name to the provider that
3
+ * actually delivers it, using `notify.transports` from agents.yaml.
4
+ *
5
+ * Default is name-identity (`--channel slack` -> `slack` provider) — NOT a
6
+ * fallback to a different transport. Only telegram is dual-homed (rush vs
7
+ * openclaw-telegram); config picks. An unmapped-to-unregistered name dies loud.
8
+ */
9
+ import type { Meta } from '../types.js';
10
+ import { type ChannelProvider } from './registry.js';
11
+ export declare function resolveTransport(channel: string, meta: Meta): ChannelProvider;
@@ -0,0 +1,12 @@
1
+ import { die } from '../format.js';
2
+ import { resolveChannelProvider, listChannelProviders } from './registry.js';
3
+ export function resolveTransport(channel, meta) {
4
+ const providerName = meta.notify?.transports?.[channel] ?? channel;
5
+ const provider = resolveChannelProvider(providerName);
6
+ if (!provider) {
7
+ die(`No channel provider '${providerName}'` +
8
+ (providerName === channel ? '' : ` (mapped from channel '${channel}' via notify.transports)`) +
9
+ `. Registered: ${listChannelProviders().join(', ')}.`);
10
+ }
11
+ return provider;
12
+ }
@@ -110,6 +110,24 @@ export interface InstallResult {
110
110
  /** Set when the install runner threw or exited non-zero. */
111
111
  error?: string;
112
112
  }
113
+ /**
114
+ * Resolve the directory a `binary` install method downloads/extracts into.
115
+ *
116
+ * `/usr/local/bin` used to be hardcoded here, which fails on Apple Silicon
117
+ * Macs where that directory is root-owned and not user-writable (Homebrew
118
+ * moved on to /opt/homebrew but left /usr/local/bin behind). Resolution
119
+ * order:
120
+ * 1. `AGENTS_CLI_BIN_DIR` — explicit override, used as-is when set.
121
+ * 2. `~/.local/bin` — the XDG user-bin dir already used for shims (see
122
+ * posixpath.ts / shims.ts); created with mkdir -p if missing.
123
+ * 3. `/usr/local/bin` — last resort, for hosts where it's still writable.
124
+ * When it isn't either, throw an actionable error naming the env var and
125
+ * ~/.local/bin instead of letting curl/tar fail with a bare EACCES.
126
+ *
127
+ * Called once per install/dry-run so the same directory is used everywhere
128
+ * that method is described or executed.
129
+ */
130
+ export declare function resolveBinDir(): string;
113
131
  /**
114
132
  * Display-only rendering of how a method would be run, for `--dry-run` and
115
133
  * status output. Not used by installCli — execution goes through runInstallMethod
@@ -22,6 +22,7 @@ import { spawnSync } from 'child_process';
22
22
  import * as yaml from 'yaml';
23
23
  import { listResources, resolveResource } from './resources.js';
24
24
  import { composeWin32CommandLine } from './platform/index.js';
25
+ import { localBinDir } from './platform/posixpath.js';
25
26
  // ─── Validation primitives ───────────────────────────────────────────────────
26
27
  /** Token allowed inside `check:` strings — letters, digits, underscore, dot, slash, dash. */
27
28
  const SAFE_CHECK_TOKEN = /^[a-zA-Z0-9_./-]+$/;
@@ -340,6 +341,52 @@ export function describeMethod(method) {
340
341
  const spec = method.binary[key];
341
342
  return spec ? `download ${spec.url}` : 'binary download';
342
343
  }
344
+ /** Env var that overrides where `binary` installs write their downloaded file(s). */
345
+ const BIN_DIR_ENV = 'AGENTS_CLI_BIN_DIR';
346
+ /** Historical default install dir — kept only as a last-resort fallback. */
347
+ const LEGACY_BIN_DIR = '/usr/local/bin';
348
+ function isWritableDir(dir, opts) {
349
+ try {
350
+ if (opts.create)
351
+ fs.mkdirSync(dir, { recursive: true });
352
+ fs.accessSync(dir, fs.constants.W_OK);
353
+ return true;
354
+ }
355
+ catch {
356
+ return false;
357
+ }
358
+ }
359
+ /**
360
+ * Resolve the directory a `binary` install method downloads/extracts into.
361
+ *
362
+ * `/usr/local/bin` used to be hardcoded here, which fails on Apple Silicon
363
+ * Macs where that directory is root-owned and not user-writable (Homebrew
364
+ * moved on to /opt/homebrew but left /usr/local/bin behind). Resolution
365
+ * order:
366
+ * 1. `AGENTS_CLI_BIN_DIR` — explicit override, used as-is when set.
367
+ * 2. `~/.local/bin` — the XDG user-bin dir already used for shims (see
368
+ * posixpath.ts / shims.ts); created with mkdir -p if missing.
369
+ * 3. `/usr/local/bin` — last resort, for hosts where it's still writable.
370
+ * When it isn't either, throw an actionable error naming the env var and
371
+ * ~/.local/bin instead of letting curl/tar fail with a bare EACCES.
372
+ *
373
+ * Called once per install/dry-run so the same directory is used everywhere
374
+ * that method is described or executed.
375
+ */
376
+ export function resolveBinDir() {
377
+ const override = process.env[BIN_DIR_ENV];
378
+ if (override && override.trim())
379
+ return override.trim();
380
+ const local = localBinDir();
381
+ if (isWritableDir(local, { create: true }))
382
+ return local;
383
+ if (isWritableDir(LEGACY_BIN_DIR, { create: false }))
384
+ return LEGACY_BIN_DIR;
385
+ throw new Error(`${LEGACY_BIN_DIR} is not writable (EACCES) and ~/.local/bin could not be created or ` +
386
+ `written to either. Set ${BIN_DIR_ENV} to a writable directory, e.g.:\n` +
387
+ ` export ${BIN_DIR_ENV}="$HOME/.local/bin"\n` +
388
+ `then re-run the install.`);
389
+ }
343
390
  /**
344
391
  * Display-only rendering of how a method would be run, for `--dry-run` and
345
392
  * status output. Not used by installCli — execution goes through runInstallMethod
@@ -359,9 +406,10 @@ export function buildInstallCommand(method) {
359
406
  const spec = method.binary[key];
360
407
  if (!spec)
361
408
  return 'binary download';
409
+ const binDir = resolveBinDir();
362
410
  return spec.extract
363
- ? `curl -fsSL ${spec.url} -o /tmp/agents-cli-bin.tgz && tar -xzf /tmp/agents-cli-bin.tgz -C /usr/local/bin ${spec.extract}`
364
- : `curl -fsSL ${spec.url} -o /usr/local/bin/agents-cli-downloaded`;
411
+ ? `curl -fsSL ${spec.url} -o /tmp/agents-cli-bin.tgz && tar -xzf /tmp/agents-cli-bin.tgz -C ${binDir} ${spec.extract}`
412
+ : `curl -fsSL ${spec.url} -o ${path.join(binDir, 'agents-cli-downloaded')}`;
365
413
  }
366
414
  /**
367
415
  * Execute an install method via spawnSync with argv arrays. Each branch
@@ -425,6 +473,7 @@ function runInstallMethod(method) {
425
473
  if (!spec)
426
474
  throw new Error(`no binary declared for ${key}`);
427
475
  assertHttpsUrl(spec.url);
476
+ const binDir = resolveBinDir();
428
477
  if (spec.extract) {
429
478
  assertSafePathSegment(spec.extract);
430
479
  const tmp = path.join(os.tmpdir(), `agents-cli-bin-${process.pid}-${Date.now()}.tgz`);
@@ -433,7 +482,7 @@ function runInstallMethod(method) {
433
482
  if (dl.status !== 0) {
434
483
  throw new Error(`binary download failed (status ${dl.status ?? 'unknown'})`);
435
484
  }
436
- const x = spawnSync('tar', ['-xzf', tmp, '-C', '/usr/local/bin', spec.extract], {
485
+ const x = spawnSync('tar', ['-xzf', tmp, '-C', binDir, spec.extract], {
437
486
  stdio: 'inherit',
438
487
  });
439
488
  if (x.status !== 0) {
@@ -448,7 +497,7 @@ function runInstallMethod(method) {
448
497
  }
449
498
  }
450
499
  else {
451
- const r = spawnSync('curl', ['-fsSL', spec.url, '-o', '/usr/local/bin/agents-cli-downloaded'], { stdio: 'inherit' });
500
+ const r = spawnSync('curl', ['-fsSL', spec.url, '-o', path.join(binDir, 'agents-cli-downloaded')], { stdio: 'inherit' });
452
501
  if (r.status !== 0) {
453
502
  throw new Error(`binary download failed (status ${r.status ?? 'unknown'})`);
454
503
  }
@@ -30,12 +30,15 @@ function codexAvailable() {
30
30
  return findCodexBinary() !== null;
31
31
  }
32
32
  /** Spawn the codex CLI with the given arguments and capture its output. */
33
- function runCodex(args) {
33
+ function runCodex(args, env) {
34
34
  const bin = findCodexBinary();
35
35
  if (!bin)
36
36
  return Promise.resolve({ stdout: '', stderr: 'codex not found', code: 127 });
37
37
  return new Promise((resolve) => {
38
- const proc = spawn(bin, args, { stdio: ['pipe', 'pipe', 'pipe'] });
38
+ const proc = spawn(bin, args, {
39
+ stdio: ['pipe', 'pipe', 'pipe'],
40
+ env: env ? { ...process.env, ...env } : process.env,
41
+ });
39
42
  let stdout = '';
40
43
  let stderr = '';
41
44
  proc.stdout.on('data', (d) => { stdout += d.toString(); });
@@ -120,7 +123,7 @@ export class CodexCloudProvider {
120
123
  if (options.branch)
121
124
  args.push('--branch', options.branch);
122
125
  args.push(options.prompt);
123
- const { stdout, stderr, code } = await runCodex(args);
126
+ const { stdout, stderr, code } = await runCodex(args, options.env);
124
127
  if (code !== 0) {
125
128
  throw new Error(`codex cloud exec failed: ${stderr || stdout}`);
126
129
  }
@@ -53,6 +53,7 @@ export declare function buildSshArgs(computer: string, remoteBin: string, remote
53
53
  droidBin: string;
54
54
  user: string;
55
55
  port?: string;
56
+ env?: Record<string, string>;
56
57
  }): string[];
57
58
  /** Map a droid stream-json `result.subtype` / `is_error` to a CloudTaskStatus. */
58
59
  export declare function mapResultStatus(line: {
@@ -22,6 +22,7 @@
22
22
  import { spawn, execFileSync } from 'child_process';
23
23
  import * as fs from 'fs';
24
24
  import * as path from 'path';
25
+ import { controlOpts, sshConnectOpts } from '../ssh-exec.js';
25
26
  import { MissingTargetError } from './types.js';
26
27
  import { getShimsDir } from '../state.js';
27
28
  const SHIMS_DIR = getShimsDir();
@@ -105,11 +106,16 @@ export function buildExecArgs(prompt, opts) {
105
106
  * shell-quoted into a single remote command string.
106
107
  */
107
108
  export function buildSshArgs(computer, remoteBin, remoteArgv, opts) {
108
- const remoteCmd = [remoteBin, ...remoteArgv].map(shellQuote).join(' ');
109
+ const envPrefix = Object.entries(opts.env ?? {}).map(([key, value]) => {
110
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
111
+ throw new Error(`Invalid runtime env key for Factory dispatch: ${key}`);
112
+ }
113
+ return `${key}=${shellQuote(value)}`;
114
+ });
115
+ const remoteCmd = [...envPrefix, remoteBin, ...remoteArgv].map((part, idx) => idx < envPrefix.length ? part : shellQuote(part)).join(' ');
109
116
  const proxy = `ProxyCommand=${opts.droidBin} computer ssh ${computer} --proxy --port %p`;
110
117
  return [
111
- '-o', proxy,
112
- '-o', 'StrictHostKeyChecking=accept-new',
118
+ ...sshConnectOpts(controlOpts(), ['-o', proxy]),
113
119
  '-p', opts.port ?? '22',
114
120
  `${opts.user}@${computer}`,
115
121
  remoteCmd,
@@ -247,7 +253,7 @@ export class FactoryCloudProvider {
247
253
  const autonomy = resolveAutonomy(options.providerOptions?.autonomy ?? options.providerOptions?.mode, this.defaultAutonomy);
248
254
  const user = options.providerOptions?.user ?? 'droid';
249
255
  const execArgs = buildExecArgs(options.prompt, { autonomy });
250
- const sshArgs = buildSshArgs(computer, 'droid', execArgs, { droidBin, user });
256
+ const sshArgs = buildSshArgs(computer, 'droid', execArgs, { droidBin, user, env: options.env });
251
257
  const { events, status, summary, sessionId } = await this.runRemote(sshArgs);
252
258
  const now = new Date().toISOString();
253
259
  const id = sessionId ?? `droid-${Date.now()}`;
@@ -64,7 +64,7 @@ export declare function accountTokensFingerprint(tokens: AccountTokenEntry[]): s
64
64
  * Build the POST body for /api/v1/cloud-runs. Exported so tests can verify
65
65
  * the back-compat shape (singular fields + repos[]) without needing real
66
66
  * GitHub installations or a live Rush session. `findInstallation` is the
67
- * only other I/O and it's tested by the halo/proxy integration suite.
67
+ * only other I/O and it's tested by the cloud proxy integration suite.
68
68
  */
69
69
  export declare function buildDispatchBody(input: {
70
70
  agent?: string;
@@ -89,6 +89,8 @@ export declare function buildDispatchBody(input: {
89
89
  * MAX_IMAGES_PER_DISPATCH — extras are dropped, never sent. Omitted when empty.
90
90
  */
91
91
  images?: ImageAttachment[] | null;
92
+ /** Runtime env vars mounted into the cloud agent process. */
93
+ env?: Record<string, string> | null;
92
94
  }): Record<string, unknown>;
93
95
  export declare class RushCloudProvider implements CloudProvider {
94
96
  id: "rush";
@@ -255,7 +255,7 @@ export function accountTokensFingerprint(tokens) {
255
255
  * Build the POST body for /api/v1/cloud-runs. Exported so tests can verify
256
256
  * the back-compat shape (singular fields + repos[]) without needing real
257
257
  * GitHub installations or a live Rush session. `findInstallation` is the
258
- * only other I/O and it's tested by the halo/proxy integration suite.
258
+ * only other I/O and it's tested by the cloud proxy integration suite.
259
259
  */
260
260
  export function buildDispatchBody(input) {
261
261
  if (input.resolvedRepos.length === 0) {
@@ -286,6 +286,9 @@ export function buildDispatchBody(input) {
286
286
  if (input.images && input.images.length > 0) {
287
287
  body.images = input.images.slice(0, MAX_IMAGES_PER_DISPATCH);
288
288
  }
289
+ if (input.env && Object.keys(input.env).length > 0) {
290
+ body.env = input.env;
291
+ }
289
292
  return body;
290
293
  }
291
294
  export class RushCloudProvider {
@@ -358,6 +361,7 @@ export class RushCloudProvider {
358
361
  strategy,
359
362
  skills: options.skills,
360
363
  images: options.images,
364
+ env: options.env,
361
365
  });
362
366
  let res = await api('POST', '/api/v1/cloud-runs', token, body);
363
367
  // Server detects drift (new account or rotated token) by comparing the
@@ -402,6 +406,7 @@ export class RushCloudProvider {
402
406
  accountTokens,
403
407
  skills: options.skills,
404
408
  images: options.images,
409
+ env: options.env,
405
410
  });
406
411
  res = await api('POST', '/api/v1/cloud-runs', token, retryBody);
407
412
  // Persist consent on first successful upload so we don't re-prompt
@@ -181,6 +181,8 @@ export interface DispatchOptions {
181
181
  images?: ImageAttachment[];
182
182
  /** Provider-specific options (e.g., codex env ID, factory computer name). */
183
183
  providerOptions?: Record<string, unknown>;
184
+ /** Runtime env vars to inject into the remote agent process when the provider supports it. */
185
+ env?: Record<string, string>;
184
186
  }
185
187
  /**
186
188
  * Collapse `repo` + `repos` into a single deduped list. Exported so callers,
@@ -0,0 +1,35 @@
1
+ /** macOS Unix-domain socket path cap: `sizeof(sockaddr_un.sun_path)` in <sys/un.h>. */
2
+ export declare const SUN_LEN = 104;
3
+ /** Suffix codex appends to CODEX_HOME to reach its app-server control socket. */
4
+ export declare const CODEX_CONTROL_SOCKET_SUFFIX = "/app-server-control/app-server-control.sock";
5
+ /**
6
+ * True when this CODEX_HOME's derived control-socket path would overflow
7
+ * SUN_LEN on macOS.
8
+ */
9
+ export declare function codexHomeOverflowsSunLen(home: string): boolean;
10
+ /**
11
+ * The short, per-version codex home used when the versioned home overflows.
12
+ * `~/.agents/.codex-homes/<version>/.codex` keeps the socket path well under
13
+ * SUN_LEN while staying stable across reboots (unlike $TMPDIR) and per-version
14
+ * isolated.
15
+ */
16
+ export declare function shortCodexHome(agentsUserDir: string, version: string): string;
17
+ /**
18
+ * Resolve a macOS SUN_LEN-safe CODEX_HOME for the given versioned home,
19
+ * migrating the home to a short real path (once, idempotently) when needed.
20
+ *
21
+ * On non-darwin platforms, or when the versioned home already fits, the
22
+ * versioned home is returned unchanged. If migration fails for any reason the
23
+ * versioned home is returned (no worse than the pre-fix behavior).
24
+ */
25
+ export declare function resolveCodexHome(versionedHome: string, agentsUserDir: string, version: string, platform?: NodeJS.Platform): string;
26
+ /**
27
+ * Emit the bash block that a generated codex shim uses to export a
28
+ * SUN_LEN-safe CODEX_HOME. Mirrors {@link resolveCodexHome}.
29
+ *
30
+ * @param homeExpr shell expression for the versioned codex home
31
+ * (e.g. `$VERSION_DIR/home/.codex`)
32
+ * @param shortBaseExpr shell expression for the per-version short base dir
33
+ * (e.g. `$AGENTS_USER_DIR/.codex-homes/$VERSION`)
34
+ */
35
+ export declare function codexHomeShimBash(homeExpr: string, shortBaseExpr: string): string;
@@ -0,0 +1,136 @@
1
+ /**
2
+ * macOS SUN_LEN-safe CODEX_HOME resolution.
3
+ *
4
+ * Codex reads its config (approval_policy, sandbox_mode, MCP servers, rules)
5
+ * from CODEX_HOME and, for its app-server daemon, binds a Unix-domain control
6
+ * socket at `$CODEX_HOME/app-server-control/app-server-control.sock`. macOS
7
+ * caps Unix socket paths at 104 bytes (SUN_LEN — `sizeof(sockaddr_un.sun_path)`
8
+ * in `<sys/un.h>`). agents-cli points CODEX_HOME at the deep versioned home
9
+ * (`~/.agents/.history/versions/codex/<version>/home/.codex`), which for a
10
+ * typical user is long enough that the derived socket path exceeds 104 bytes.
11
+ * `codex app-server daemon start` then fails with `path must be shorter than
12
+ * SUN_LEN`, and every codex spawn on macOS dies (this took down all OpenClaw
13
+ * agents on mac-mini — RUSH-1866).
14
+ *
15
+ * Codex exposes no socket-path override and resolves symlinks before binding,
16
+ * so a short symlink pointing at the deep home does NOT help (the socket lives
17
+ * at the resolved target). The only fix is to make the *real* CODEX_HOME short.
18
+ * On macOS, when the versioned home's socket path would overflow, we relocate
19
+ * the home once to a short real directory under `~/.agents/.codex-homes/` and
20
+ * leave a symlink behind at the versioned path, then point CODEX_HOME at the
21
+ * short real path. Config, auth, and state migrate intact; there is no socket
22
+ * or sqlite fragmentation because a single physical home simply moves.
23
+ *
24
+ * This module is the single source of truth for that logic. `exec.ts` calls
25
+ * the TS resolver for `agents run`/`agents exec`; `shims.ts` emits the bash
26
+ * equivalent (`codexHomeShimBash`) into the generated codex shims. Keep the two
27
+ * in lockstep.
28
+ */
29
+ import * as fs from 'fs';
30
+ import * as path from 'path';
31
+ /** macOS Unix-domain socket path cap: `sizeof(sockaddr_un.sun_path)` in <sys/un.h>. */
32
+ export const SUN_LEN = 104;
33
+ /** Suffix codex appends to CODEX_HOME to reach its app-server control socket. */
34
+ export const CODEX_CONTROL_SOCKET_SUFFIX = '/app-server-control/app-server-control.sock';
35
+ /**
36
+ * True when this CODEX_HOME's derived control-socket path would overflow
37
+ * SUN_LEN on macOS.
38
+ */
39
+ export function codexHomeOverflowsSunLen(home) {
40
+ return home.length + CODEX_CONTROL_SOCKET_SUFFIX.length > SUN_LEN;
41
+ }
42
+ /**
43
+ * The short, per-version codex home used when the versioned home overflows.
44
+ * `~/.agents/.codex-homes/<version>/.codex` keeps the socket path well under
45
+ * SUN_LEN while staying stable across reboots (unlike $TMPDIR) and per-version
46
+ * isolated.
47
+ */
48
+ export function shortCodexHome(agentsUserDir, version) {
49
+ return path.join(agentsUserDir, '.codex-homes', version, '.codex');
50
+ }
51
+ /**
52
+ * Resolve a macOS SUN_LEN-safe CODEX_HOME for the given versioned home,
53
+ * migrating the home to a short real path (once, idempotently) when needed.
54
+ *
55
+ * On non-darwin platforms, or when the versioned home already fits, the
56
+ * versioned home is returned unchanged. If migration fails for any reason the
57
+ * versioned home is returned (no worse than the pre-fix behavior).
58
+ */
59
+ export function resolveCodexHome(versionedHome, agentsUserDir, version, platform = process.platform) {
60
+ if (platform !== 'darwin')
61
+ return versionedHome;
62
+ if (!codexHomeOverflowsSunLen(versionedHome))
63
+ return versionedHome;
64
+ const short = shortCodexHome(agentsUserDir, version);
65
+ try {
66
+ if (!fs.existsSync(short)) {
67
+ fs.mkdirSync(path.dirname(short), { recursive: true });
68
+ const st = fs.lstatSync(versionedHome, { throwIfNoEntry: false });
69
+ if (st && st.isDirectory() && !st.isSymbolicLink()) {
70
+ // Migrate the existing deep home so config/auth/state stay intact,
71
+ // then leave a symlink so anything referencing the versioned path
72
+ // still resolves.
73
+ fs.renameSync(versionedHome, short);
74
+ fs.symlinkSync(short, versionedHome);
75
+ }
76
+ else if (!st) {
77
+ // Fresh install: create the short home and link the versioned path to it.
78
+ fs.mkdirSync(short, { recursive: true });
79
+ fs.symlinkSync(short, versionedHome);
80
+ }
81
+ // If versionedHome is already a symlink (migrated by a prior run or the
82
+ // shim), leave it; `short` will be populated below.
83
+ }
84
+ }
85
+ catch {
86
+ // Migration lost a race or hit a permission error. Fall back to the
87
+ // versioned home rather than crash the invocation.
88
+ if (!fs.existsSync(short))
89
+ return versionedHome;
90
+ }
91
+ return fs.existsSync(short) ? short : versionedHome;
92
+ }
93
+ /**
94
+ * Emit the bash block that a generated codex shim uses to export a
95
+ * SUN_LEN-safe CODEX_HOME. Mirrors {@link resolveCodexHome}.
96
+ *
97
+ * @param homeExpr shell expression for the versioned codex home
98
+ * (e.g. `$VERSION_DIR/home/.codex`)
99
+ * @param shortBaseExpr shell expression for the per-version short base dir
100
+ * (e.g. `$AGENTS_USER_DIR/.codex-homes/$VERSION`)
101
+ */
102
+ export function codexHomeShimBash(homeExpr, shortBaseExpr) {
103
+ return `
104
+ # Codex reads its config (approval_policy, sandbox_mode, MCP servers, rules)
105
+ # from CODEX_HOME and binds a Unix control socket at
106
+ # "\$CODEX_HOME/app-server-control/app-server-control.sock" for its app-server
107
+ # daemon. macOS caps Unix socket paths at 104 bytes (SUN_LEN); the deep
108
+ # versioned home overflows, so "codex app-server daemon start" fails with
109
+ # "path must be shorter than SUN_LEN" and every codex spawn dies (RUSH-1866).
110
+ # Codex has no socket-path override and resolves symlinks before binding, so a
111
+ # short symlink to the deep home does NOT help. On macOS, when the derived
112
+ # socket path would overflow, relocate the home once to a short real dir and
113
+ # leave a symlink behind, then point CODEX_HOME at the short real path. A
114
+ # caller-provided CODEX_HOME always wins.
115
+ if [ -z "\${CODEX_HOME:-}" ]; then
116
+ CODEX_HOME="${homeExpr}"
117
+ # 43 = length of "/app-server-control/app-server-control.sock"; 104 = SUN_LEN.
118
+ if [ "\$(uname -s)" = "Darwin" ] && [ "\$(( \${#CODEX_HOME} + 43 ))" -gt 104 ]; then
119
+ _codex_short="${shortBaseExpr}/.codex"
120
+ if [ ! -e "\$_codex_short" ]; then
121
+ mkdir -p "${shortBaseExpr}"
122
+ if [ -d "\$CODEX_HOME" ] && [ ! -L "\$CODEX_HOME" ]; then
123
+ if mv "\$CODEX_HOME" "\$_codex_short" 2>/dev/null; then
124
+ ln -snf "\$_codex_short" "\$CODEX_HOME"
125
+ fi
126
+ elif [ ! -e "\$CODEX_HOME" ]; then
127
+ mkdir -p "\$_codex_short"
128
+ ln -snf "\$_codex_short" "\$CODEX_HOME"
129
+ fi
130
+ fi
131
+ [ -d "\$_codex_short" ] && CODEX_HOME="\$_codex_short"
132
+ fi
133
+ fi
134
+ export CODEX_HOME="\$CODEX_HOME"
135
+ `;
136
+ }
@@ -26,6 +26,10 @@ export interface CrabboxBox {
26
26
  state: string;
27
27
  /** Public IPv4, when the provider exposes one. */
28
28
  ip?: string;
29
+ /** Tailnet IPv4 (100.x), when the box was leased with `--network tailscale`. */
30
+ tailscaleIPv4?: string;
31
+ /** Tailnet MagicDNS FQDN, when the box joined the tailnet. */
32
+ tailscaleFQDN?: string;
29
33
  profile?: string;
30
34
  class?: string;
31
35
  /** True when running + bootstrap-complete. */
@@ -49,6 +53,13 @@ export interface CrabboxOptions {
49
53
  * `crabbox login` credentials.
50
54
  */
51
55
  secretsBundle?: string;
56
+ /**
57
+ * Hard cap (ms) on a single crabbox invocation that hits the provider API
58
+ * (`list`). Prevents a slow/unreachable provider from hanging an ambient
59
+ * command like `agents devices`. Defaults to 8s; callers on a fast/interactive
60
+ * path (devices list, `agents ssh` fall-through) pass a shorter bound.
61
+ */
62
+ timeoutMs?: number;
52
63
  }
53
64
  /** Locate the crabbox binary, or throw an actionable error. */
54
65
  export declare function findCrabbox(): string;
@@ -61,6 +72,17 @@ export declare function findCrabbox(): string;
61
72
  export declare const LEASE_PROVIDER_TOKEN_KEYS: string[];
62
73
  /** The first bundle that declares a provider token key, or undefined. Pure over `bundles`. */
63
74
  export declare function pickLeaseBundleFromList(bundles: SecretsBundle[]): string | undefined;
75
+ /**
76
+ * Env keys a bundle may use for a Tailscale auth key. crabbox reads
77
+ * `CRABBOX_TAILSCALE_AUTH_KEY` to join a leased box to the tailnet; we accept the
78
+ * common alternate names too and rename to that canonical key on injection.
79
+ */
80
+ export declare const TAILSCALE_AUTH_KEY_NAMES: string[];
81
+ /** The first bundle + key that declares a Tailscale auth key, or undefined. Pure over `bundles`. */
82
+ export declare function pickTailscaleBundleFromList(bundles: SecretsBundle[]): {
83
+ name: string;
84
+ key: string;
85
+ } | undefined;
64
86
  /** A resolved lease bundle: its name, plus (auto-detect only) the exact keys to inject. */
65
87
  export interface ResolvedLeaseBundle {
66
88
  name: string;
@@ -97,6 +119,13 @@ export interface WarmupOptions extends CrabboxOptions {
97
119
  code?: boolean;
98
120
  /** Cloud backend override (crabbox provider id, e.g. hetzner/aws/do). */
99
121
  provider?: string;
122
+ /**
123
+ * Network mode for the leased box. `'public'` (default) leases with a public
124
+ * IP; `'tailscale'` joins the box to the tailnet (`--network tailscale`,
125
+ * tagged `tag:crabbox`) so it is reachable only over Tailscale. The caller
126
+ * (command layer) decides WHEN to enable this — F5 here is plumbing only.
127
+ */
128
+ netMode?: 'public' | 'tailscale';
100
129
  }
101
130
  /**
102
131
  * Lease a box and block until it is ready. Returns the leased box.
@@ -137,6 +166,21 @@ export declare function crabboxRun(slug: string, remoteCmd: string, opts?: Crabb
137
166
  * Streams combined output; resolves with the remote exit code (null on dispatch failure).
138
167
  */
139
168
  export declare function crabboxRunScript(slug: string, script: string, opts?: CrabboxRunOptions): Promise<number | null>;
169
+ /**
170
+ * Parse crabbox's shell-quoted `ssh` command (`'ssh' '-i' '<key>' … 'crabbox@ip'`)
171
+ * into an argv array. Exported for testing.
172
+ */
173
+ export declare function parseCrabboxSshArgv(stdout: string): string[] | null;
174
+ /**
175
+ * The concrete `ssh` argv crabbox uses to reach box `slug` — including the
176
+ * per-lease identity key and known_hosts crabbox provisions under its own config
177
+ * dir. A raw `ssh crabbox@ip` fails `publickey`; this is the only auth that works.
178
+ * `crabbox ssh --id` PRINTS the command (it doesn't connect — that's `crabbox
179
+ * connect`), and `--reclaim` makes it resolve regardless of which repo holds the
180
+ * lease claim (matching `crabboxRunScript`). Returns null when crabbox can't
181
+ * resolve the box (caller treats setup-copy / `agents ssh` as best-effort).
182
+ */
183
+ export declare function crabboxSshArgv(slug: string, opts?: CrabboxOptions): string[] | null;
140
184
  /** Release the lease / delete the box. Best-effort; never throws. */
141
185
  export declare function crabboxStop(slug: string, opts?: CrabboxOptions): boolean;
142
186
  /** Never reap a box touched within this many seconds, regardless of idle-timeout. */