@phnx-labs/agents-cli 1.20.64 → 1.20.66

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 (188) hide show
  1. package/CHANGELOG.md +51 -3
  2. package/README.md +156 -3
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/apply.d.ts +12 -0
  5. package/dist/commands/apply.js +274 -0
  6. package/dist/commands/browser.js +2 -2
  7. package/dist/commands/cloud.js +32 -2
  8. package/dist/commands/doctor.js +4 -1
  9. package/dist/commands/exec.d.ts +48 -0
  10. package/dist/commands/exec.js +159 -49
  11. package/dist/commands/feed.js +25 -11
  12. package/dist/commands/hosts.js +44 -6
  13. package/dist/commands/mcp.js +55 -5
  14. package/dist/commands/monitors.d.ts +12 -0
  15. package/dist/commands/monitors.js +748 -0
  16. package/dist/commands/output.js +2 -2
  17. package/dist/commands/plugins.js +28 -7
  18. package/dist/commands/routines.js +23 -2
  19. package/dist/commands/secrets.d.ts +16 -0
  20. package/dist/commands/secrets.js +215 -64
  21. package/dist/commands/serve.js +31 -0
  22. package/dist/commands/sessions-browser.d.ts +82 -0
  23. package/dist/commands/sessions-browser.js +320 -0
  24. package/dist/commands/sessions-export.js +8 -3
  25. package/dist/commands/sessions.d.ts +17 -0
  26. package/dist/commands/sessions.js +157 -10
  27. package/dist/commands/share.d.ts +2 -0
  28. package/dist/commands/share.js +150 -0
  29. package/dist/commands/ssh.js +54 -3
  30. package/dist/commands/versions.js +7 -3
  31. package/dist/commands/view.d.ts +26 -0
  32. package/dist/commands/view.js +32 -9
  33. package/dist/commands/webhook.js +10 -2
  34. package/dist/index.js +35 -14
  35. package/dist/lib/agents.d.ts +46 -0
  36. package/dist/lib/agents.js +121 -3
  37. package/dist/lib/auto-dispatch-provider.js +7 -2
  38. package/dist/lib/auto-dispatch.d.ts +3 -0
  39. package/dist/lib/auto-dispatch.js +3 -0
  40. package/dist/lib/browser/chrome.js +2 -2
  41. package/dist/lib/cloud/antigravity.js +2 -2
  42. package/dist/lib/cloud/host.d.ts +59 -0
  43. package/dist/lib/cloud/host.js +224 -0
  44. package/dist/lib/cloud/registry.js +4 -0
  45. package/dist/lib/cloud/types.d.ts +6 -4
  46. package/dist/lib/computer-rpc.js +3 -1
  47. package/dist/lib/crabbox/cli.js +5 -1
  48. package/dist/lib/crabbox/runtimes.js +11 -2
  49. package/dist/lib/daemon.d.ts +20 -4
  50. package/dist/lib/daemon.js +62 -19
  51. package/dist/lib/devices/connect.d.ts +18 -1
  52. package/dist/lib/devices/connect.js +10 -2
  53. package/dist/lib/devices/fleet.d.ts +3 -2
  54. package/dist/lib/devices/fleet.js +9 -0
  55. package/dist/lib/devices/known-hosts.d.ts +62 -0
  56. package/dist/lib/devices/known-hosts.js +137 -0
  57. package/dist/lib/devices/registry.d.ts +15 -0
  58. package/dist/lib/devices/registry.js +9 -0
  59. package/dist/lib/exec.d.ts +19 -2
  60. package/dist/lib/exec.js +41 -13
  61. package/dist/lib/fleet/apply.d.ts +63 -0
  62. package/dist/lib/fleet/apply.js +214 -0
  63. package/dist/lib/fleet/auth-sync.d.ts +67 -0
  64. package/dist/lib/fleet/auth-sync.js +142 -0
  65. package/dist/lib/fleet/manifest.d.ts +29 -0
  66. package/dist/lib/fleet/manifest.js +127 -0
  67. package/dist/lib/fleet/types.d.ts +129 -0
  68. package/dist/lib/fleet/types.js +13 -0
  69. package/dist/lib/git.d.ts +27 -0
  70. package/dist/lib/git.js +34 -2
  71. package/dist/lib/hosts/dispatch.d.ts +29 -8
  72. package/dist/lib/hosts/dispatch.js +66 -20
  73. package/dist/lib/hosts/passthrough.js +2 -0
  74. package/dist/lib/hosts/providers/devices.d.ts +27 -0
  75. package/dist/lib/hosts/providers/devices.js +98 -0
  76. package/dist/lib/hosts/registry.d.ts +10 -16
  77. package/dist/lib/hosts/registry.js +17 -50
  78. package/dist/lib/hosts/remote-cmd.d.ts +23 -0
  79. package/dist/lib/hosts/remote-cmd.js +79 -4
  80. package/dist/lib/hosts/run-target.d.ts +84 -0
  81. package/dist/lib/hosts/run-target.js +99 -0
  82. package/dist/lib/hosts/types.d.ts +23 -5
  83. package/dist/lib/hosts/types.js +22 -4
  84. package/dist/lib/linear-autoclose.d.ts +30 -0
  85. package/dist/lib/linear-autoclose.js +22 -0
  86. package/dist/lib/mcp.d.ts +27 -1
  87. package/dist/lib/mcp.js +126 -12
  88. package/dist/lib/monitors/config.d.ts +161 -0
  89. package/dist/lib/monitors/config.js +372 -0
  90. package/dist/lib/monitors/dispatch.d.ts +28 -0
  91. package/dist/lib/monitors/dispatch.js +91 -0
  92. package/dist/lib/monitors/engine.d.ts +61 -0
  93. package/dist/lib/monitors/engine.js +205 -0
  94. package/dist/lib/monitors/sources/command.d.ts +11 -0
  95. package/dist/lib/monitors/sources/command.js +31 -0
  96. package/dist/lib/monitors/sources/device.d.ts +13 -0
  97. package/dist/lib/monitors/sources/device.js +45 -0
  98. package/dist/lib/monitors/sources/file.d.ts +14 -0
  99. package/dist/lib/monitors/sources/file.js +57 -0
  100. package/dist/lib/monitors/sources/http.d.ts +10 -0
  101. package/dist/lib/monitors/sources/http.js +34 -0
  102. package/dist/lib/monitors/sources/index.d.ts +14 -0
  103. package/dist/lib/monitors/sources/index.js +31 -0
  104. package/dist/lib/monitors/sources/poll.d.ts +9 -0
  105. package/dist/lib/monitors/sources/poll.js +9 -0
  106. package/dist/lib/monitors/sources/types.d.ts +18 -0
  107. package/dist/lib/monitors/sources/types.js +9 -0
  108. package/dist/lib/monitors/sources/webhook.d.ts +23 -0
  109. package/dist/lib/monitors/sources/webhook.js +47 -0
  110. package/dist/lib/monitors/sources/ws.d.ts +14 -0
  111. package/dist/lib/monitors/sources/ws.js +45 -0
  112. package/dist/lib/monitors/state.d.ts +69 -0
  113. package/dist/lib/monitors/state.js +144 -0
  114. package/dist/lib/picker.d.ts +53 -0
  115. package/dist/lib/picker.js +214 -1
  116. package/dist/lib/platform/exec.d.ts +16 -0
  117. package/dist/lib/platform/exec.js +17 -0
  118. package/dist/lib/plugins.d.ts +31 -1
  119. package/dist/lib/plugins.js +175 -15
  120. package/dist/lib/redact.d.ts +14 -1
  121. package/dist/lib/redact.js +47 -1
  122. package/dist/lib/remote-agents-json.js +7 -1
  123. package/dist/lib/rotate.d.ts +6 -3
  124. package/dist/lib/rotate.js +0 -1
  125. package/dist/lib/routines.d.ts +16 -0
  126. package/dist/lib/routines.js +19 -0
  127. package/dist/lib/runner.d.ts +1 -0
  128. package/dist/lib/runner.js +102 -9
  129. package/dist/lib/secrets/agent.d.ts +83 -10
  130. package/dist/lib/secrets/agent.js +237 -70
  131. package/dist/lib/secrets/bundles.d.ts +26 -0
  132. package/dist/lib/secrets/bundles.js +59 -8
  133. package/dist/lib/secrets/filestore.d.ts +9 -0
  134. package/dist/lib/secrets/filestore.js +21 -8
  135. package/dist/lib/secrets/index.d.ts +7 -0
  136. package/dist/lib/secrets/index.js +26 -6
  137. package/dist/lib/secrets/mcp.js +4 -2
  138. package/dist/lib/secrets/remote.d.ts +17 -0
  139. package/dist/lib/secrets/remote.js +40 -0
  140. package/dist/lib/self-update.d.ts +20 -0
  141. package/dist/lib/self-update.js +54 -1
  142. package/dist/lib/serve/control.d.ts +95 -0
  143. package/dist/lib/serve/control.js +260 -0
  144. package/dist/lib/serve/server.d.ts +35 -1
  145. package/dist/lib/serve/server.js +106 -76
  146. package/dist/lib/serve/stream.d.ts +43 -0
  147. package/dist/lib/serve/stream.js +116 -0
  148. package/dist/lib/serve/token.d.ts +35 -0
  149. package/dist/lib/serve/token.js +85 -0
  150. package/dist/lib/session/bundle.d.ts +14 -0
  151. package/dist/lib/session/bundle.js +12 -1
  152. package/dist/lib/session/remote-list.js +5 -1
  153. package/dist/lib/session/state.d.ts +7 -25
  154. package/dist/lib/session/state.js +16 -6
  155. package/dist/lib/session/sync/config.js +8 -2
  156. package/dist/lib/session/types.d.ts +30 -0
  157. package/dist/lib/share/capture.d.ts +29 -0
  158. package/dist/lib/share/capture.js +140 -0
  159. package/dist/lib/share/config.d.ts +35 -0
  160. package/dist/lib/share/config.js +100 -0
  161. package/dist/lib/share/og.d.ts +25 -0
  162. package/dist/lib/share/og.js +84 -0
  163. package/dist/lib/share/provision.d.ts +10 -0
  164. package/dist/lib/share/provision.js +91 -0
  165. package/dist/lib/share/publish.d.ts +57 -0
  166. package/dist/lib/share/publish.js +145 -0
  167. package/dist/lib/share/worker-template.d.ts +2 -0
  168. package/dist/lib/share/worker-template.js +82 -0
  169. package/dist/lib/shims.d.ts +13 -0
  170. package/dist/lib/shims.js +42 -2
  171. package/dist/lib/ssh-exec.d.ts +24 -0
  172. package/dist/lib/ssh-exec.js +15 -3
  173. package/dist/lib/ssh-tunnel.d.ts +19 -1
  174. package/dist/lib/ssh-tunnel.js +86 -7
  175. package/dist/lib/startup/command-registry.d.ts +3 -0
  176. package/dist/lib/startup/command-registry.js +6 -0
  177. package/dist/lib/state.d.ts +5 -0
  178. package/dist/lib/state.js +12 -0
  179. package/dist/lib/tmux/session.d.ts +7 -0
  180. package/dist/lib/tmux/session.js +3 -1
  181. package/dist/lib/triggers/webhook.d.ts +18 -0
  182. package/dist/lib/triggers/webhook.js +105 -0
  183. package/dist/lib/types.d.ts +36 -1
  184. package/dist/lib/usage.js +7 -5
  185. package/dist/lib/versions.js +14 -11
  186. package/dist/lib/workflows.d.ts +20 -0
  187. package/dist/lib/workflows.js +24 -0
  188. package/package.json +2 -1
@@ -2,17 +2,23 @@
2
2
  * Host provider registry.
3
3
  *
4
4
  * Mirrors the cloud provider registry: a Map of provider id → implementation,
5
- * instantiated once. v1 registers only `local`; adding `rush`/`tailscale`/
6
- * `crabbox` later is a one-line `providers.set(...)` with no caller changes.
5
+ * instantiated once. Registers `local` then `devices` (order is precedence:
6
+ * an enrolled host shadows a same-name device); adding `rush`/`crabbox` later
7
+ * is a one-line `providers.set(...)` with no caller changes.
7
8
  */
9
+ import { DeviceOffloadUnsupportedError } from './types.js';
8
10
  import { LocalHostProvider } from './providers/local.js';
9
- import { getDevice } from '../devices/registry.js';
11
+ import { DevicesHostProvider } from './providers/devices.js';
10
12
  import { assertValidSshTarget } from '../ssh-exec.js';
13
+ // Re-export so existing importers (tests, commands) keep their path; the class
14
+ // itself lives in types.ts so providers can throw it without a circular import.
15
+ export { DeviceOffloadUnsupportedError };
11
16
  const providers = new Map();
12
17
  function initProviders() {
13
18
  if (providers.size > 0)
14
19
  return;
15
20
  providers.set('local', new LocalHostProvider());
21
+ providers.set('devices', new DevicesHostProvider());
16
22
  }
17
23
  export function getProvider(id) {
18
24
  initProviders();
@@ -40,51 +46,13 @@ export async function listAllHosts() {
40
46
  }
41
47
  return out;
42
48
  }
43
- /**
44
- * Thrown when a device resolves but can't be used as an offload target because
45
- * it authenticates with a password. The offload path runs over `sshExec`, whose
46
- * `SSH_OPTS` force `BatchMode=yes` (no password prompts), so only key / ssh-config
47
- * auth can carry a `--host` run. Named so the top-level catch prints the message
48
- * cleanly instead of a stack trace.
49
- */
50
- export class DeviceOffloadUnsupportedError extends Error {
51
- constructor(name) {
52
- super(`Device "${name}" uses password auth, which --host offload can't use yet ` +
53
- `(runs go over ssh with BatchMode=yes). Switch it to key auth with ` +
54
- `\`agents devices set ${name} --auth key\`, or enroll it as a host with ` +
55
- `\`agents hosts add ${name}\`.`);
56
- this.name = 'DeviceOffloadUnsupportedError';
57
- }
58
- }
59
- /**
60
- * Bridge a registered device into a `Host` so `--host <device>` just works.
61
- * A device's dnsName (preferred) or ip becomes the ssh address; `source: 'inline'`
62
- * makes `sshTargetFor` emit `user@address`.
63
- */
64
- function deviceToHost(device) {
65
- if (device.auth.method === 'password') {
66
- throw new DeviceOffloadUnsupportedError(device.name);
67
- }
68
- const address = device.address.dnsName ?? device.address.ip;
69
- if (!address) {
70
- throw new Error(`Device "${device.name}" has no address (Tailscale DNS name or IP) to reach it by.`);
71
- }
72
- return {
73
- name: device.name,
74
- provider: 'local',
75
- source: 'inline',
76
- address,
77
- user: device.user,
78
- ...(device.platform !== 'unknown' ? { os: device.platform } : {}),
79
- enrolled: true,
80
- };
81
- }
82
49
  /**
83
50
  * Resolve a host name to a single host, or null if unknown. Resolution order:
84
- * 1. host providers — the `agents hosts` registry (agents.yaml overlay + ssh-config)
85
- * 2. the devices registry (`agents devices`) a machine registered once with
86
- * `agents devices sync` is reachable by `--host`/`--device` with no second enroll
87
- * 3. an ad-hoc `user@host` (must contain `@`, validated) — nothing to register
51
+ * 1. host providers, in registration order — the `agents hosts` registry
52
+ * (`local`: agents.yaml overlay + ssh-config), then the devices registry
53
+ * (`devices`: a machine registered once with `agents devices sync` is
54
+ * reachable by `--host`/`--device` with no second enroll)
55
+ * 2. an ad-hoc `user@host` (must contain `@`, validated) — nothing to register
88
56
  *
89
57
  * A bare unknown name returns null so capability-tag routing (`resolveHostByCap`)
90
58
  * stays reachable: `--host gpu` must fall through to a cap lookup, not be misread
@@ -96,9 +64,6 @@ export async function resolveHost(name) {
96
64
  if (host)
97
65
  return host;
98
66
  }
99
- const device = await getDevice(name);
100
- if (device)
101
- return deviceToHost(device);
102
67
  if (name.includes('@')) {
103
68
  assertValidSshTarget(name);
104
69
  const at = name.indexOf('@');
@@ -111,7 +76,9 @@ export async function resolveHost(name) {
111
76
  * matching host, or throws on 0 or >1 matches unless `any` is set (then first).
112
77
  */
113
78
  export async function resolveHostByCap(cap, any = false) {
114
- const matches = (await listAllHosts()).filter((h) => h.caps?.includes(cap));
79
+ // Non-dispatchable hosts (password-auth devices) are listed for honesty but
80
+ // must never be picked as a run target.
81
+ const matches = (await listAllHosts()).filter((h) => h.caps?.includes(cap) && h.dispatchable !== false);
115
82
  if (matches.length === 0)
116
83
  throw new Error(`No host tagged "${cap}". Tag one with: agents hosts add <name> --cap ${cap}`);
117
84
  if (matches.length > 1 && !any) {
@@ -33,6 +33,29 @@ export declare function stripRoutingFlags(args: string[], specs: StripSpec[]): s
33
33
  * binary (which would re-trigger routing).
34
34
  */
35
35
  export declare const HOST_ROUTING_SPECS: StripSpec[];
36
+ /** How one `agents run` option behaves when the run is offloaded with `--host`. */
37
+ export type RunOptionForwarding =
38
+ /** Appended to the remote `agents run` argv — same behavior local or remote. */
39
+ 'forward'
40
+ /** Refused with an actionable error BEFORE dispatch — never silently dropped. */
41
+ | 'reject'
42
+ /** Consumed by the dispatching side (routing, follow rendering, cwd portability). */
43
+ | 'local-only';
44
+ /**
45
+ * The forwarding contract for `agents run … --host`: every option of the `run`
46
+ * command is classified here, keyed by its commander attribute name. A
47
+ * commander-introspection test (run-forwarding.test.ts) fails when a run
48
+ * option is missing from this table, so a new option can never silently drop
49
+ * at the SSH boundary again — the exact bug this table exists to prevent
50
+ * (--secrets/--effort/--env/--timeout historically vanished on --host runs
51
+ * with no error).
52
+ *
53
+ * Rejections are value-aware at the call site (exec.ts): `--secrets` only
54
+ * rejects when a bundle was actually passed, `--resume` only when bare.
55
+ */
56
+ export declare const RUN_OPTION_FORWARDING: Record<string, RunOptionForwarding>;
57
+ /** Actionable messages for value-aware rejections, keyed by attribute name. */
58
+ export declare const RUN_OPTION_REJECT_MESSAGES: Record<string, string>;
36
59
  /**
37
60
  * Build the single command string for `ssh <target> <cmd>`. The forwarded args
38
61
  * are quoted for the inner login shell, then the whole `agents …` invocation is
@@ -50,6 +50,77 @@ export const HOST_ROUTING_SPECS = [
50
50
  { long: 'device', takesValue: true },
51
51
  { long: 'remote-cwd', takesValue: true },
52
52
  ];
53
+ /**
54
+ * The forwarding contract for `agents run … --host`: every option of the `run`
55
+ * command is classified here, keyed by its commander attribute name. A
56
+ * commander-introspection test (run-forwarding.test.ts) fails when a run
57
+ * option is missing from this table, so a new option can never silently drop
58
+ * at the SSH boundary again — the exact bug this table exists to prevent
59
+ * (--secrets/--effort/--env/--timeout historically vanished on --host runs
60
+ * with no error).
61
+ *
62
+ * Rejections are value-aware at the call site (exec.ts): `--secrets` only
63
+ * rejects when a bundle was actually passed, `--resume` only when bare.
64
+ */
65
+ export const RUN_OPTION_FORWARDING = {
66
+ // forwarded — the remote run behaves exactly like a local one
67
+ mode: 'forward',
68
+ effort: 'forward',
69
+ model: 'forward',
70
+ env: 'forward',
71
+ addDir: 'forward',
72
+ name: 'forward',
73
+ resume: 'forward', // concrete id only — bare `--resume` rejects (picker can't cross SSH)
74
+ sessionId: 'forward',
75
+ timeout: 'forward',
76
+ fallback: 'forward',
77
+ balanced: 'forward',
78
+ strategy: 'forward',
79
+ loop: 'forward',
80
+ maxIterations: 'forward',
81
+ budget: 'forward',
82
+ until: 'forward',
83
+ interval: 'forward',
84
+ json: 'forward', // remote emits ndjson into its log; the local follow streams it verbatim
85
+ verbose: 'forward',
86
+ yes: 'forward', // a detached remote run can't answer the budget-confirm prompt
87
+ acp: 'forward', // the remote CLI routes through ACP on ITS side of the wire
88
+ autoSecrets: 'forward', // workflow frontmatter secrets resolve on the REMOTE keychain
89
+ // rejected — cannot cross the SSH boundary; fail loud, never degrade
90
+ secrets: 'reject',
91
+ secretsKeys: 'reject',
92
+ allowExpired: 'reject',
93
+ resumeCheckpoint: 'reject',
94
+ // local-only — routing, dispatch-path choice, and follow rendering
95
+ quiet: 'local-only', // the remote argv always carries --quiet
96
+ headless: 'local-only',
97
+ interactive: 'local-only', // the interactive path forwards --interactive itself
98
+ cwd: 'local-only', // made portable into remoteCwd
99
+ project: 'local-only',
100
+ remoteCwd: 'local-only',
101
+ raw: 'local-only', // interactive builder forwards --raw itself
102
+ tmux: 'local-only',
103
+ disableTmux: 'local-only',
104
+ host: 'local-only',
105
+ device: 'local-only',
106
+ on: 'local-only',
107
+ computer: 'local-only',
108
+ any: 'local-only',
109
+ follow: 'local-only',
110
+ lease: 'local-only',
111
+ keepBox: 'local-only',
112
+ copyCreds: 'local-only', // copies creds TO the host before dispatch — local concern only
113
+ };
114
+ /** Actionable messages for value-aware rejections, keyed by attribute name. */
115
+ export const RUN_OPTION_REJECT_MESSAGES = {
116
+ secrets: '--secrets cannot cross the SSH boundary — Keychain values are never sent to a host implicitly. ' +
117
+ 'Provision the bundle on the host first (agents secrets export --host <name>), then run without --secrets; ' +
118
+ 'workflow frontmatter secrets resolve from the HOST\'s own keychain.',
119
+ secretsKeys: '--secrets-keys applies to --secrets bundles, which cannot cross the SSH boundary (see --secrets).',
120
+ allowExpired: '--allow-expired applies to --secrets bundles, which cannot cross the SSH boundary (see --secrets).',
121
+ resumeCheckpoint: '--resume-checkpoint reads a local checkpoint.json — it cannot resume a run on another machine. Run it locally, or start a fresh --loop run on the host.',
122
+ resumeBare: '--resume with no id opens the interactive picker, which cannot run across a detached host dispatch. Pass a concrete session id: agents run <agent> --resume <id> --host <name>.',
123
+ };
53
124
  /**
54
125
  * Build the single command string for `ssh <target> <cmd>`. The forwarded args
55
126
  * are quoted for the inner login shell, then the whole `agents …` invocation is
@@ -156,12 +227,16 @@ export function buildWindowsAgentsCommand(cmd) {
156
227
  */
157
228
  export function buildWindowsStdinImportCommand(bundle, opts = {}) {
158
229
  const force = opts.force ? ' --force' : '';
230
+ // Create AND write the temp file INSIDE the try so its finally always cleans
231
+ // up: if GetTempFileName succeeds but WriteAllText (or the import) then throws,
232
+ // the secret-bearing temp file would otherwise be left behind (RUSH-1764). $tmp
233
+ // starts null so a GetTempFileName that itself throws leaves nothing to remove.
159
234
  const script = [
160
235
  '$in = [Console]::In.ReadToEnd()',
161
- '$tmp = [System.IO.Path]::GetTempFileName()',
162
- '[System.IO.File]::WriteAllText($tmp, $in)',
163
- `try { & agents secrets import ${powershellQuote(bundle)} --from $tmp${force}; $code = $LASTEXITCODE } ` +
164
- `finally { Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue }`,
236
+ '$tmp = $null',
237
+ `try { $tmp = [System.IO.Path]::GetTempFileName(); [System.IO.File]::WriteAllText($tmp, $in); ` +
238
+ `& agents secrets import ${powershellQuote(bundle)} --from $tmp${force}; $code = $LASTEXITCODE } ` +
239
+ `finally { if ($tmp) { Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue } }`,
165
240
  'if ($null -eq $code) { $code = 1 }',
166
241
  'exit $code',
167
242
  ].join('; ');
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Shared host-run dispatch — the one path every surface uses to run an agent on
3
+ * another machine: `agents run --host` (commands/exec.ts), the `host` cloud
4
+ * provider (`agents cloud run --provider host`), and host-placed routines.
5
+ *
6
+ * Wraps the two steps every caller needs and previously lived inline in
7
+ * exec.ts's `--host` branch:
8
+ * 1. resolution — name → capability tag → error, with the same fall-through
9
+ * semantics as `agents run --host` (only "Multiple hosts tagged…" is a
10
+ * resolution verdict; "no host tagged" degrades to unknown-host), and
11
+ * 2. headless dispatch — session-id mint (Claude only), detached SSH launch,
12
+ * and LOCAL session-index registration so the run shows in `agents sessions`.
13
+ *
14
+ * Interactive dispatch stays in exec.ts: it is inherently tied to the caller's
15
+ * TTY and has no other consumers.
16
+ */
17
+ import type { Host } from './types.js';
18
+ import type { DispatchResult } from './dispatch.js';
19
+ import type { HostCredentials } from './credentials.js';
20
+ /**
21
+ * Resolution failed with a user-actionable message the caller should print
22
+ * verbatim. Distinct from `DeviceOffloadUnsupportedError` (which propagates to
23
+ * the top-level catch) so callers can tell "bad name" from "bad auth method".
24
+ */
25
+ export declare class HostResolutionError extends Error {
26
+ constructor(message: string);
27
+ }
28
+ /**
29
+ * Resolve a `--host` value the way `agents run` does: exact name (providers →
30
+ * devices → `user@host`), then capability tag. Throws `HostResolutionError`
31
+ * for an ambiguous tag or an unknown name; lets `DeviceOffloadUnsupportedError`
32
+ * (password-auth device) propagate untouched for the top-level catch.
33
+ */
34
+ export declare function resolveHostRunTarget(name: string, opts?: {
35
+ any?: boolean;
36
+ }): Promise<Host>;
37
+ export interface HostPromptRun {
38
+ agent: string;
39
+ prompt: string;
40
+ /** Explicit agent version pin, forwarded as `agent@version`. */
41
+ version?: string;
42
+ mode?: string;
43
+ model?: string;
44
+ /** Durable `--name <slug>` handle recorded on the task. */
45
+ name?: string;
46
+ /** Resume an existing session on the host by concrete id. */
47
+ resume?: string;
48
+ /** Working directory on the host, already made remote-portable by the caller. */
49
+ remoteCwd?: string;
50
+ /** Stream progress and block until completion (default true). */
51
+ follow?: boolean;
52
+ timeoutMs?: number;
53
+ /** Local directory to record in the session index (defaults to process.cwd()). */
54
+ cwd?: string;
55
+ /** Forwarded run options — see RUN_OPTION_FORWARDING in remote-cmd.ts. */
56
+ effort?: string;
57
+ env?: string[];
58
+ addDir?: string[];
59
+ timeout?: string;
60
+ strategy?: string;
61
+ balanced?: boolean;
62
+ fallback?: string;
63
+ loop?: boolean;
64
+ maxIterations?: string;
65
+ budget?: string;
66
+ until?: string;
67
+ interval?: string;
68
+ json?: boolean;
69
+ verbose?: boolean;
70
+ yes?: boolean;
71
+ acp?: boolean;
72
+ autoSecrets?: boolean;
73
+ passthroughArgs?: string[];
74
+ /** Copy runtime credentials to the host before the run and shred them after. */
75
+ copyCreds?: HostCredentials;
76
+ }
77
+ /**
78
+ * Dispatch a headless prompt run onto a resolved host: mint the forced session
79
+ * id (Claude is the only agent that accepts `--session-id`; on resume the
80
+ * remote session keeps its id), launch detached over SSH, and register the run
81
+ * in the LOCAL session index. Returns the task record and the remote exit code
82
+ * (`-1` = follow window closed while the run continues).
83
+ */
84
+ export declare function dispatchPromptToHost(host: Host, opts: HostPromptRun): Promise<DispatchResult>;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Shared host-run dispatch — the one path every surface uses to run an agent on
3
+ * another machine: `agents run --host` (commands/exec.ts), the `host` cloud
4
+ * provider (`agents cloud run --provider host`), and host-placed routines.
5
+ *
6
+ * Wraps the two steps every caller needs and previously lived inline in
7
+ * exec.ts's `--host` branch:
8
+ * 1. resolution — name → capability tag → error, with the same fall-through
9
+ * semantics as `agents run --host` (only "Multiple hosts tagged…" is a
10
+ * resolution verdict; "no host tagged" degrades to unknown-host), and
11
+ * 2. headless dispatch — session-id mint (Claude only), detached SSH launch,
12
+ * and LOCAL session-index registration so the run shows in `agents sessions`.
13
+ *
14
+ * Interactive dispatch stays in exec.ts: it is inherently tied to the caller's
15
+ * TTY and has no other consumers.
16
+ */
17
+ import { randomUUID } from 'crypto';
18
+ import { resolveHost, resolveHostByCap } from './registry.js';
19
+ import { dispatchToHost } from './dispatch.js';
20
+ import { registerHostSession } from './session-index.js';
21
+ /**
22
+ * Resolution failed with a user-actionable message the caller should print
23
+ * verbatim. Distinct from `DeviceOffloadUnsupportedError` (which propagates to
24
+ * the top-level catch) so callers can tell "bad name" from "bad auth method".
25
+ */
26
+ export class HostResolutionError extends Error {
27
+ constructor(message) {
28
+ super(message);
29
+ this.name = 'HostResolutionError';
30
+ }
31
+ }
32
+ /**
33
+ * Resolve a `--host` value the way `agents run` does: exact name (providers →
34
+ * devices → `user@host`), then capability tag. Throws `HostResolutionError`
35
+ * for an ambiguous tag or an unknown name; lets `DeviceOffloadUnsupportedError`
36
+ * (password-auth device) propagate untouched for the top-level catch.
37
+ */
38
+ export async function resolveHostRunTarget(name, opts = {}) {
39
+ let host = await resolveHost(name);
40
+ if (!host) {
41
+ try {
42
+ host = await resolveHostByCap(name, opts.any);
43
+ }
44
+ catch (e) {
45
+ const msg = e.message ?? '';
46
+ // Ambiguity is a verdict, not a miss — surface it. "No host tagged"
47
+ // falls through to the generic unknown-host error below.
48
+ if (msg.startsWith('Multiple hosts'))
49
+ throw new HostResolutionError(msg);
50
+ }
51
+ }
52
+ if (!host)
53
+ throw new HostResolutionError(`Unknown host "${name}". List hosts: agents hosts list`);
54
+ return host;
55
+ }
56
+ /**
57
+ * Dispatch a headless prompt run onto a resolved host: mint the forced session
58
+ * id (Claude is the only agent that accepts `--session-id`; on resume the
59
+ * remote session keeps its id), launch detached over SSH, and register the run
60
+ * in the LOCAL session index. Returns the task record and the remote exit code
61
+ * (`-1` = follow window closed while the run continues).
62
+ */
63
+ export async function dispatchPromptToHost(host, opts) {
64
+ const sessionId = opts.agent === 'claude' && !opts.resume ? randomUUID() : undefined;
65
+ const result = await dispatchToHost(host, {
66
+ agent: opts.agent,
67
+ prompt: opts.prompt,
68
+ version: opts.version,
69
+ mode: opts.mode,
70
+ model: opts.model,
71
+ remoteCwd: opts.remoteCwd,
72
+ sessionId,
73
+ name: opts.name,
74
+ resume: opts.resume,
75
+ follow: opts.follow !== false,
76
+ timeoutMs: opts.timeoutMs,
77
+ effort: opts.effort,
78
+ env: opts.env,
79
+ addDir: opts.addDir,
80
+ timeout: opts.timeout,
81
+ strategy: opts.strategy,
82
+ balanced: opts.balanced,
83
+ fallback: opts.fallback,
84
+ loop: opts.loop,
85
+ maxIterations: opts.maxIterations,
86
+ budget: opts.budget,
87
+ until: opts.until,
88
+ interval: opts.interval,
89
+ json: opts.json,
90
+ verbose: opts.verbose,
91
+ yes: opts.yes,
92
+ acp: opts.acp,
93
+ autoSecrets: opts.autoSecrets,
94
+ passthroughArgs: opts.passthroughArgs,
95
+ copyCreds: opts.copyCreds,
96
+ });
97
+ registerHostSession(result.task, { cwd: opts.cwd ?? process.cwd(), prompt: opts.prompt });
98
+ return result;
99
+ }
@@ -2,13 +2,14 @@
2
2
  * Agent-host provider contract.
3
3
  *
4
4
  * A `HostProvider` answers "what are my hosts, and how do I reach them?" — the
5
- * pluggable directory/metadata/reachability layer. v1 ships only the `local`
6
- * provider (ssh-config ∪ inline registry); `rush`/`tailscale`/`crabbox` are
7
- * additive fast-follows behind this same contract. Capability-gated so partial
8
- * providers are first-class (mirrors the cloud provider registry).
5
+ * pluggable directory/metadata/reachability layer. Shipped providers: `local`
6
+ * (ssh-config ∪ inline registry) and `devices` (the Tailscale fleet from
7
+ * `agents devices`); `rush`/`crabbox` remain additive fast-follows behind this
8
+ * same contract. Capability-gated so partial providers are first-class
9
+ * (mirrors the cloud provider registry).
9
10
  */
10
11
  import type { HostEntry } from '../types.js';
11
- export type HostProviderId = 'local';
12
+ export type HostProviderId = 'local' | 'devices';
12
13
  export type HostStatus = 'online' | 'offline' | 'unknown';
13
14
  /** A host as seen at runtime: its persisted entry plus name/provider/status. */
14
15
  export interface Host extends HostEntry {
@@ -17,6 +18,23 @@ export interface Host extends HostEntry {
17
18
  /** True when the host has an explicit overlay/inline entry in the registry. */
18
19
  enrolled?: boolean;
19
20
  status?: HostStatus;
21
+ /**
22
+ * False when the host is listed for honesty but can't carry a `--host` run
23
+ * (today: password-auth devices — offload rides BatchMode=yes ssh). Absent
24
+ * means dispatchable. Cap routing and target pickers filter on this.
25
+ */
26
+ dispatchable?: boolean;
27
+ }
28
+ /**
29
+ * Thrown when a device resolves but can't be used as an offload target because
30
+ * it authenticates with a password. The offload path runs over `sshExec`, whose
31
+ * `SSH_OPTS` force `BatchMode=yes` (no password prompts), so only key / ssh-config
32
+ * auth can carry a `--host` run. Named so the top-level catch prints the message
33
+ * cleanly instead of a stack trace. (Lives here, not registry.ts, so providers
34
+ * can throw it without a circular import; registry.ts re-exports it.)
35
+ */
36
+ export declare class DeviceOffloadUnsupportedError extends Error {
37
+ constructor(name: string);
20
38
  }
21
39
  export interface HostProviderCapabilities {
22
40
  /** Can list/track hosts. */
@@ -2,11 +2,29 @@
2
2
  * Agent-host provider contract.
3
3
  *
4
4
  * A `HostProvider` answers "what are my hosts, and how do I reach them?" — the
5
- * pluggable directory/metadata/reachability layer. v1 ships only the `local`
6
- * provider (ssh-config ∪ inline registry); `rush`/`tailscale`/`crabbox` are
7
- * additive fast-follows behind this same contract. Capability-gated so partial
8
- * providers are first-class (mirrors the cloud provider registry).
5
+ * pluggable directory/metadata/reachability layer. Shipped providers: `local`
6
+ * (ssh-config ∪ inline registry) and `devices` (the Tailscale fleet from
7
+ * `agents devices`); `rush`/`crabbox` remain additive fast-follows behind this
8
+ * same contract. Capability-gated so partial providers are first-class
9
+ * (mirrors the cloud provider registry).
9
10
  */
11
+ /**
12
+ * Thrown when a device resolves but can't be used as an offload target because
13
+ * it authenticates with a password. The offload path runs over `sshExec`, whose
14
+ * `SSH_OPTS` force `BatchMode=yes` (no password prompts), so only key / ssh-config
15
+ * auth can carry a `--host` run. Named so the top-level catch prints the message
16
+ * cleanly instead of a stack trace. (Lives here, not registry.ts, so providers
17
+ * can throw it without a circular import; registry.ts re-exports it.)
18
+ */
19
+ export class DeviceOffloadUnsupportedError extends Error {
20
+ constructor(name) {
21
+ super(`Device "${name}" uses password auth, which --host offload can't use yet ` +
22
+ `(runs go over ssh with BatchMode=yes). Switch it to key auth with ` +
23
+ `\`agents devices set ${name} --auth key\`, or enroll it as a host with ` +
24
+ `\`agents hosts add ${name}\`.`);
25
+ this.name = 'DeviceOffloadUnsupportedError';
26
+ }
27
+ }
10
28
  /**
11
29
  * The ssh target string for a host: the bare name for ssh-config hosts (ssh
12
30
  * resolves HostName/User/Port/Identity), else `user@address` (or `address`).
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Linear auto-close: close Linear issues whose linked GitHub PRs have merged.
3
+ *
4
+ * The pure decision function `shouldCloseIssue` is the only logic here —
5
+ * it is unit-tested and shared with the command routine's equivalent
6
+ * shell check. The rest (Linear GraphQL, gh invocations) lives in the
7
+ * routine YAML's `command:` shell script so the routine stays self-contained
8
+ * and independent of the CLI build.
9
+ *
10
+ * @see apps/cli/routines/linear-autoclose.yml
11
+ */
12
+ /**
13
+ * Subset of `gh pr view --json state,mergedAt` output that drives the
14
+ * close decision. State is one of: OPEN | CLOSED | MERGED (gh GraphQL).
15
+ */
16
+ export interface PrInfo {
17
+ /** gh GraphQL PR state: 'OPEN' | 'CLOSED' | 'MERGED' */
18
+ state: string;
19
+ /** ISO-8601 merge timestamp, or null when the PR was not merged. */
20
+ mergedAt: string | null;
21
+ }
22
+ /**
23
+ * Pure decision: returns true when the PR has been merged and its linked
24
+ * Linear issue should therefore be closed.
25
+ *
26
+ * A PR is considered merged only when both conditions hold:
27
+ * 1. `state === 'MERGED'` — gh marks CLOSED (rejected) separately from MERGED
28
+ * 2. `mergedAt !== null` — defensive guard; a merged PR always carries a timestamp
29
+ */
30
+ export declare function shouldCloseIssue(pr: PrInfo): boolean;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Linear auto-close: close Linear issues whose linked GitHub PRs have merged.
3
+ *
4
+ * The pure decision function `shouldCloseIssue` is the only logic here —
5
+ * it is unit-tested and shared with the command routine's equivalent
6
+ * shell check. The rest (Linear GraphQL, gh invocations) lives in the
7
+ * routine YAML's `command:` shell script so the routine stays self-contained
8
+ * and independent of the CLI build.
9
+ *
10
+ * @see apps/cli/routines/linear-autoclose.yml
11
+ */
12
+ /**
13
+ * Pure decision: returns true when the PR has been merged and its linked
14
+ * Linear issue should therefore be closed.
15
+ *
16
+ * A PR is considered merged only when both conditions hold:
17
+ * 1. `state === 'MERGED'` — gh marks CLOSED (rejected) separately from MERGED
18
+ * 2. `mergedAt !== null` — defensive guard; a merged PR always carries a timestamp
19
+ */
20
+ export function shouldCloseIssue(pr) {
21
+ return pr.state === 'MERGED' && pr.mergedAt !== null;
22
+ }
package/dist/lib/mcp.d.ts CHANGED
@@ -44,10 +44,35 @@ export declare function parseMcpServerConfig(filePath: string): McpYamlConfig |
44
44
  * command-line options or that contain characters unsafe for argv/identifier use.
45
45
  */
46
46
  export declare function validateMcpServerName(name: string): void;
47
+ /** Path to the user-owned project-trust store (never inside a repo). */
48
+ export declare function getMcpTrustStorePath(): string;
49
+ /**
50
+ * Whether the project that owns `projectAgentsDir` has been explicitly trusted
51
+ * for MCP auto-apply. Untrusted by default (fail closed).
52
+ */
53
+ export declare function isProjectMcpTrusted(projectAgentsDir: string): boolean;
54
+ /**
55
+ * Record explicit trust for the project containing `cwd` so its project-scoped
56
+ * MCP servers may be registered/spawned. Returns the trusted project root, or
57
+ * null when `cwd` is not inside a project (no `.agents/` to trust).
58
+ */
59
+ export declare function trustProjectMcp(cwd?: string): string | null;
60
+ /** Revoke MCP trust for the project containing `cwd`. Returns true if it was trusted. */
61
+ export declare function untrustProjectMcp(cwd?: string): boolean;
47
62
  /**
48
63
  * List all MCP server configs from ~/.agents/mcp/.
64
+ *
65
+ * When `enforceProjectTrust` is set, project-scoped configs are included only
66
+ * for a project the user has explicitly trusted (see `isProjectMcpTrusted`) —
67
+ * this is the choke point that keeps an untrusted cloned repo's MCP servers out
68
+ * of the register/spawn path. It ALSO fixes name-collision shadowing: an
69
+ * untrusted project entry is dropped before dedup, so it can never mask a
70
+ * same-named user entry. Display callers omit the flag to surface project
71
+ * entries (command+args and all) regardless of trust.
49
72
  */
50
- export declare function listMcpServerConfigs(cwd?: string): InstalledMcpServer[];
73
+ export declare function listMcpServerConfigs(cwd?: string, options?: {
74
+ enforceProjectTrust?: boolean;
75
+ }): InstalledMcpServer[];
51
76
  /**
52
77
  * Scan a repository for MCP server YAML configs.
53
78
  * Looks under <repoPath>/mcp/*.yaml — same on-disk layout as ~/.agents/mcp/.
@@ -70,6 +95,7 @@ export declare function installMcpConfigCentrally(sourcePath: string): {
70
95
  */
71
96
  export declare function getMcpServersByName(names?: string[], options?: {
72
97
  cwd?: string;
98
+ enforceProjectTrust?: boolean;
73
99
  }): InstalledMcpServer[];
74
100
  /**
75
101
  * Assemble the JSON payload Claude's `--mcp-config` flag expects from a set of