@phnx-labs/agents-cli 1.20.64 → 1.20.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -3
- package/README.md +37 -2
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.d.ts +12 -0
- package/dist/commands/apply.js +274 -0
- package/dist/commands/browser.js +2 -2
- package/dist/commands/cloud.js +32 -2
- package/dist/commands/doctor.js +4 -1
- package/dist/commands/exec.js +94 -49
- package/dist/commands/feed.js +25 -11
- package/dist/commands/hosts.js +44 -6
- package/dist/commands/mcp.js +55 -5
- package/dist/commands/monitors.d.ts +12 -0
- package/dist/commands/monitors.js +740 -0
- package/dist/commands/output.js +2 -2
- package/dist/commands/routines.js +23 -2
- package/dist/commands/secrets.d.ts +16 -0
- package/dist/commands/secrets.js +215 -64
- package/dist/commands/serve.js +31 -0
- package/dist/commands/sessions-export.js +8 -3
- package/dist/commands/sessions.d.ts +16 -0
- package/dist/commands/sessions.js +36 -6
- package/dist/commands/ssh.js +45 -2
- package/dist/commands/versions.js +7 -3
- package/dist/commands/view.d.ts +26 -0
- package/dist/commands/view.js +32 -9
- package/dist/commands/webhook.js +10 -2
- package/dist/index.js +34 -14
- package/dist/lib/agents.d.ts +18 -0
- package/dist/lib/agents.js +53 -3
- package/dist/lib/auto-dispatch-provider.js +7 -2
- package/dist/lib/auto-dispatch.d.ts +3 -0
- package/dist/lib/auto-dispatch.js +3 -0
- package/dist/lib/browser/chrome.js +2 -2
- package/dist/lib/cloud/antigravity.js +2 -2
- package/dist/lib/cloud/host.d.ts +59 -0
- package/dist/lib/cloud/host.js +224 -0
- package/dist/lib/cloud/registry.js +4 -0
- package/dist/lib/cloud/types.d.ts +6 -4
- package/dist/lib/computer-rpc.js +3 -1
- package/dist/lib/crabbox/cli.js +5 -1
- package/dist/lib/crabbox/runtimes.js +11 -2
- package/dist/lib/daemon.d.ts +20 -4
- package/dist/lib/daemon.js +62 -19
- package/dist/lib/devices/fleet.d.ts +3 -2
- package/dist/lib/devices/fleet.js +9 -0
- package/dist/lib/devices/registry.d.ts +15 -0
- package/dist/lib/devices/registry.js +9 -0
- package/dist/lib/exec.d.ts +19 -2
- package/dist/lib/exec.js +41 -13
- package/dist/lib/fleet/apply.d.ts +63 -0
- package/dist/lib/fleet/apply.js +214 -0
- package/dist/lib/fleet/auth-sync.d.ts +67 -0
- package/dist/lib/fleet/auth-sync.js +142 -0
- package/dist/lib/fleet/manifest.d.ts +29 -0
- package/dist/lib/fleet/manifest.js +127 -0
- package/dist/lib/fleet/types.d.ts +129 -0
- package/dist/lib/fleet/types.js +13 -0
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +34 -2
- package/dist/lib/hosts/dispatch.d.ts +29 -8
- package/dist/lib/hosts/dispatch.js +46 -18
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/providers/devices.d.ts +27 -0
- package/dist/lib/hosts/providers/devices.js +98 -0
- package/dist/lib/hosts/registry.d.ts +10 -16
- package/dist/lib/hosts/registry.js +17 -50
- package/dist/lib/hosts/remote-cmd.d.ts +23 -0
- package/dist/lib/hosts/remote-cmd.js +71 -0
- package/dist/lib/hosts/run-target.d.ts +84 -0
- package/dist/lib/hosts/run-target.js +99 -0
- package/dist/lib/hosts/types.d.ts +23 -5
- package/dist/lib/hosts/types.js +22 -4
- package/dist/lib/linear-autoclose.d.ts +30 -0
- package/dist/lib/linear-autoclose.js +22 -0
- package/dist/lib/mcp.d.ts +27 -1
- package/dist/lib/mcp.js +126 -12
- package/dist/lib/monitors/config.d.ts +161 -0
- package/dist/lib/monitors/config.js +372 -0
- package/dist/lib/monitors/dispatch.d.ts +28 -0
- package/dist/lib/monitors/dispatch.js +91 -0
- package/dist/lib/monitors/engine.d.ts +61 -0
- package/dist/lib/monitors/engine.js +201 -0
- package/dist/lib/monitors/sources/command.d.ts +11 -0
- package/dist/lib/monitors/sources/command.js +31 -0
- package/dist/lib/monitors/sources/device.d.ts +13 -0
- package/dist/lib/monitors/sources/device.js +35 -0
- package/dist/lib/monitors/sources/file.d.ts +14 -0
- package/dist/lib/monitors/sources/file.js +57 -0
- package/dist/lib/monitors/sources/http.d.ts +10 -0
- package/dist/lib/monitors/sources/http.js +34 -0
- package/dist/lib/monitors/sources/index.d.ts +14 -0
- package/dist/lib/monitors/sources/index.js +31 -0
- package/dist/lib/monitors/sources/poll.d.ts +9 -0
- package/dist/lib/monitors/sources/poll.js +9 -0
- package/dist/lib/monitors/sources/types.d.ts +18 -0
- package/dist/lib/monitors/sources/types.js +9 -0
- package/dist/lib/monitors/sources/webhook.d.ts +23 -0
- package/dist/lib/monitors/sources/webhook.js +47 -0
- package/dist/lib/monitors/sources/ws.d.ts +14 -0
- package/dist/lib/monitors/sources/ws.js +45 -0
- package/dist/lib/monitors/state.d.ts +69 -0
- package/dist/lib/monitors/state.js +144 -0
- package/dist/lib/platform/exec.d.ts +16 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/plugins.js +101 -2
- package/dist/lib/redact.d.ts +14 -1
- package/dist/lib/redact.js +47 -1
- package/dist/lib/remote-agents-json.js +7 -1
- package/dist/lib/rotate.d.ts +6 -3
- package/dist/lib/rotate.js +0 -1
- package/dist/lib/routines.d.ts +16 -0
- package/dist/lib/routines.js +19 -0
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +102 -9
- package/dist/lib/secrets/agent.d.ts +48 -10
- package/dist/lib/secrets/agent.js +123 -15
- package/dist/lib/secrets/bundles.d.ts +26 -0
- package/dist/lib/secrets/bundles.js +59 -8
- package/dist/lib/secrets/mcp.js +4 -2
- package/dist/lib/secrets/remote.d.ts +17 -0
- package/dist/lib/secrets/remote.js +40 -0
- package/dist/lib/self-update.d.ts +20 -0
- package/dist/lib/self-update.js +54 -1
- package/dist/lib/serve/control.d.ts +95 -0
- package/dist/lib/serve/control.js +260 -0
- package/dist/lib/serve/server.d.ts +35 -1
- package/dist/lib/serve/server.js +106 -76
- package/dist/lib/serve/stream.d.ts +43 -0
- package/dist/lib/serve/stream.js +116 -0
- package/dist/lib/serve/token.d.ts +35 -0
- package/dist/lib/serve/token.js +85 -0
- package/dist/lib/session/bundle.d.ts +14 -0
- package/dist/lib/session/bundle.js +12 -1
- package/dist/lib/session/remote-list.js +5 -1
- package/dist/lib/session/state.d.ts +7 -25
- package/dist/lib/session/state.js +16 -6
- package/dist/lib/session/sync/config.js +8 -2
- package/dist/lib/session/types.d.ts +30 -0
- package/dist/lib/ssh-tunnel.d.ts +19 -1
- package/dist/lib/ssh-tunnel.js +86 -7
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +4 -0
- package/dist/lib/state.d.ts +5 -0
- package/dist/lib/state.js +12 -0
- package/dist/lib/tmux/session.d.ts +7 -0
- package/dist/lib/tmux/session.js +3 -1
- package/dist/lib/triggers/webhook.d.ts +18 -0
- package/dist/lib/triggers/webhook.js +105 -0
- package/dist/lib/types.d.ts +26 -1
- package/dist/lib/usage.js +7 -5
- package/dist/lib/versions.js +14 -11
- package/dist/lib/workflows.d.ts +20 -0
- package/dist/lib/workflows.js +24 -0
- package/package.json +2 -1
|
@@ -235,38 +235,61 @@ async function launchDetached(host, target, opts) {
|
|
|
235
235
|
* session-id / resume flag wiring is unit-testable without an SSH round-trip.
|
|
236
236
|
* `--session-id` and `--resume` are mutually exclusive (the CLI rejects both);
|
|
237
237
|
* resume wins when — defensively — both are set.
|
|
238
|
+
*
|
|
239
|
+
* Every field here is classified 'forward' in RUN_OPTION_FORWARDING
|
|
240
|
+
* (remote-cmd.ts) — keep the two in lockstep; run-forwarding.test.ts asserts
|
|
241
|
+
* the table side.
|
|
238
242
|
*/
|
|
239
243
|
export function buildRunForwardedArgs(opts) {
|
|
240
244
|
const agentArg = opts.version ? `${opts.agent}@${opts.version}` : opts.agent;
|
|
241
245
|
const args = ['run', agentArg, opts.prompt, '--quiet'];
|
|
242
|
-
if (opts.strategy)
|
|
243
|
-
args.push('--strategy', opts.strategy);
|
|
244
246
|
if (opts.mode)
|
|
245
247
|
args.push('--mode', opts.mode);
|
|
246
248
|
if (opts.model)
|
|
247
249
|
args.push('--model', opts.model);
|
|
248
|
-
|
|
250
|
+
// 'auto' is the remote default — forwarding it would only add noise.
|
|
251
|
+
if (opts.effort && opts.effort !== 'auto')
|
|
249
252
|
args.push('--effort', opts.effort);
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
253
|
+
for (const kv of opts.env ?? [])
|
|
254
|
+
args.push('--env', kv);
|
|
255
|
+
for (const dir of opts.addDir ?? [])
|
|
256
|
+
args.push('--add-dir', dir);
|
|
257
|
+
if (opts.timeout)
|
|
258
|
+
args.push('--timeout', opts.timeout);
|
|
259
|
+
if (opts.strategy)
|
|
260
|
+
args.push('--strategy', opts.strategy);
|
|
261
|
+
if (opts.balanced)
|
|
262
|
+
args.push('--balanced');
|
|
263
|
+
if (opts.fallback)
|
|
264
|
+
args.push('--fallback', opts.fallback);
|
|
265
|
+
if (opts.loop)
|
|
266
|
+
args.push('--loop');
|
|
267
|
+
if (opts.maxIterations)
|
|
268
|
+
args.push('--max-iterations', opts.maxIterations);
|
|
269
|
+
if (opts.budget)
|
|
270
|
+
args.push('--budget', opts.budget);
|
|
271
|
+
if (opts.until)
|
|
272
|
+
args.push('--until', opts.until);
|
|
273
|
+
if (opts.interval)
|
|
274
|
+
args.push('--interval', opts.interval);
|
|
254
275
|
if (opts.json)
|
|
255
276
|
args.push('--json');
|
|
256
277
|
if (opts.verbose)
|
|
257
278
|
args.push('--verbose');
|
|
258
|
-
if (opts.timeout)
|
|
259
|
-
args.push('--timeout', opts.timeout);
|
|
260
279
|
if (opts.yes)
|
|
261
280
|
args.push('--yes');
|
|
262
281
|
if (opts.acp)
|
|
263
282
|
args.push('--acp');
|
|
283
|
+
if (opts.autoSecrets === false)
|
|
284
|
+
args.push('--no-auto-secrets');
|
|
264
285
|
if (opts.name)
|
|
265
286
|
args.push('--name', opts.name);
|
|
266
287
|
if (opts.resume)
|
|
267
288
|
args.push('--resume', opts.resume);
|
|
268
289
|
else if (opts.sessionId)
|
|
269
290
|
args.push('--session-id', opts.sessionId);
|
|
291
|
+
if (opts.passthroughArgs && opts.passthroughArgs.length > 0)
|
|
292
|
+
args.push('--', ...opts.passthroughArgs);
|
|
270
293
|
return args;
|
|
271
294
|
}
|
|
272
295
|
/**
|
|
@@ -283,24 +306,29 @@ export function buildInteractiveRunForwardedArgs(opts) {
|
|
|
283
306
|
args.push(opts.prompt);
|
|
284
307
|
if (opts.forceInteractive)
|
|
285
308
|
args.push('--interactive');
|
|
286
|
-
if (opts.strategy)
|
|
287
|
-
args.push('--strategy', opts.strategy);
|
|
288
309
|
if (opts.mode)
|
|
289
310
|
args.push('--mode', opts.mode);
|
|
290
311
|
if (opts.model)
|
|
291
312
|
args.push('--model', opts.model);
|
|
292
|
-
|
|
313
|
+
// 'auto' is the remote default — forwarding it would only add noise.
|
|
314
|
+
if (opts.effort && opts.effort !== 'auto')
|
|
293
315
|
args.push('--effort', opts.effort);
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
316
|
+
for (const kv of opts.env ?? [])
|
|
317
|
+
args.push('--env', kv);
|
|
318
|
+
for (const dir of opts.addDir ?? [])
|
|
319
|
+
args.push('--add-dir', dir);
|
|
320
|
+
if (opts.timeout)
|
|
321
|
+
args.push('--timeout', opts.timeout);
|
|
322
|
+
if (opts.strategy)
|
|
323
|
+
args.push('--strategy', opts.strategy);
|
|
324
|
+
if (opts.balanced)
|
|
325
|
+
args.push('--balanced');
|
|
326
|
+
if (opts.fallback)
|
|
327
|
+
args.push('--fallback', opts.fallback);
|
|
298
328
|
if (opts.json)
|
|
299
329
|
args.push('--json');
|
|
300
330
|
if (opts.verbose)
|
|
301
331
|
args.push('--verbose');
|
|
302
|
-
if (opts.timeout)
|
|
303
|
-
args.push('--timeout', opts.timeout);
|
|
304
332
|
if (opts.yes)
|
|
305
333
|
args.push('--yes');
|
|
306
334
|
if (opts.acp)
|
|
@@ -107,6 +107,8 @@ const OWN_HOST_COMMANDS = new Set([
|
|
|
107
107
|
'ssh',
|
|
108
108
|
'devices',
|
|
109
109
|
'fleet', // alias of devices
|
|
110
|
+
'apply', // `--device` scopes the fleet reconcile to one device (it targets devices itself)
|
|
111
|
+
'monitors', // `--device` names the OWNER machine (pin-to-one), not a routing target
|
|
110
112
|
]);
|
|
111
113
|
/** `--no-tty` is stripped like the routing flags but carries no value. */
|
|
112
114
|
const STRIP_SPECS = [...HOST_ROUTING_SPECS, { long: 'no-tty', takesValue: false }];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Devices host provider: the Tailscale fleet as dispatch targets.
|
|
3
|
+
*
|
|
4
|
+
* Bridges the devices registry (`agents devices`, ~/.agents/.history/devices/
|
|
5
|
+
* registry.json) into the host pool behind the same `HostProvider` seam as
|
|
6
|
+
* `local` — the "tailscale provider" fast-follow named in docs/hosts.md. With
|
|
7
|
+
* it, a machine registered once via `agents devices sync` shows up in
|
|
8
|
+
* `agents hosts list`, participates in capability routing, and is enumerable
|
|
9
|
+
* by target pickers — not just resolvable by exact name.
|
|
10
|
+
*
|
|
11
|
+
* Password-auth devices are listed (the pool stays honest about what exists)
|
|
12
|
+
* but marked `dispatchable: false`; resolving one for dispatch throws the same
|
|
13
|
+
* typed `DeviceOffloadUnsupportedError` as before — offload rides `sshExec`,
|
|
14
|
+
* whose SSH_OPTS force `BatchMode=yes`.
|
|
15
|
+
*
|
|
16
|
+
* Precedence is unchanged: this provider registers AFTER `local`, so an
|
|
17
|
+
* enrolled host shadows a same-name device in both list dedup and resolve
|
|
18
|
+
* order, exactly like the old tier-2 devices fall-through in resolveHost.
|
|
19
|
+
*/
|
|
20
|
+
import type { Host, HostProvider, HostProviderCapabilities, HostStatus } from '../types.js';
|
|
21
|
+
export declare class DevicesHostProvider implements HostProvider {
|
|
22
|
+
readonly id: "devices";
|
|
23
|
+
capabilities(): HostProviderCapabilities;
|
|
24
|
+
list(): Promise<Host[]>;
|
|
25
|
+
resolve(name: string): Promise<Host | null>;
|
|
26
|
+
presence(name: string): Promise<HostStatus>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Devices host provider: the Tailscale fleet as dispatch targets.
|
|
3
|
+
*
|
|
4
|
+
* Bridges the devices registry (`agents devices`, ~/.agents/.history/devices/
|
|
5
|
+
* registry.json) into the host pool behind the same `HostProvider` seam as
|
|
6
|
+
* `local` — the "tailscale provider" fast-follow named in docs/hosts.md. With
|
|
7
|
+
* it, a machine registered once via `agents devices sync` shows up in
|
|
8
|
+
* `agents hosts list`, participates in capability routing, and is enumerable
|
|
9
|
+
* by target pickers — not just resolvable by exact name.
|
|
10
|
+
*
|
|
11
|
+
* Password-auth devices are listed (the pool stays honest about what exists)
|
|
12
|
+
* but marked `dispatchable: false`; resolving one for dispatch throws the same
|
|
13
|
+
* typed `DeviceOffloadUnsupportedError` as before — offload rides `sshExec`,
|
|
14
|
+
* whose SSH_OPTS force `BatchMode=yes`.
|
|
15
|
+
*
|
|
16
|
+
* Precedence is unchanged: this provider registers AFTER `local`, so an
|
|
17
|
+
* enrolled host shadows a same-name device in both list dedup and resolve
|
|
18
|
+
* order, exactly like the old tier-2 devices fall-through in resolveHost.
|
|
19
|
+
*/
|
|
20
|
+
import { loadDevices, getDevice, isControlDevice } from '../../devices/registry.js';
|
|
21
|
+
import { DeviceOffloadUnsupportedError } from '../types.js';
|
|
22
|
+
/** Tailscale's own presence bit, when the sync captured one. */
|
|
23
|
+
function statusOf(device) {
|
|
24
|
+
if (!device.tailscale)
|
|
25
|
+
return 'unknown';
|
|
26
|
+
return device.tailscale.online ? 'online' : 'offline';
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Bridge a device profile into a `Host`. dnsName (stable across IP churn) is
|
|
30
|
+
* preferred over ip; `source: 'inline'` makes `sshTargetFor` emit `user@address`.
|
|
31
|
+
* Capability tags attach by enrolling the device (`agents hosts add <device>
|
|
32
|
+
* --cap …` sources the target from this profile) — the enrolled entry then
|
|
33
|
+
* shadows this row by provider precedence, carrying the caps.
|
|
34
|
+
*/
|
|
35
|
+
function deviceToPoolHost(device) {
|
|
36
|
+
// A control device (a cockpit, e.g. a paired iPhone) drives the fleet but
|
|
37
|
+
// never runs agents — it must never enter the host pool or be resolvable as a
|
|
38
|
+
// dispatch target, whatever platform it reports (an iPhone syncs as `unknown`,
|
|
39
|
+
// which remoteShellFor would otherwise default to POSIX and try to SSH).
|
|
40
|
+
if (isControlDevice(device))
|
|
41
|
+
return null;
|
|
42
|
+
const address = device.address.dnsName ?? device.address.ip;
|
|
43
|
+
if (!address)
|
|
44
|
+
return null; // unreachable profile — nothing to dispatch to
|
|
45
|
+
return {
|
|
46
|
+
name: device.name,
|
|
47
|
+
provider: 'devices',
|
|
48
|
+
source: 'inline',
|
|
49
|
+
address,
|
|
50
|
+
user: device.user,
|
|
51
|
+
...(device.platform !== 'unknown' ? { os: device.platform } : {}),
|
|
52
|
+
enrolled: true,
|
|
53
|
+
status: statusOf(device),
|
|
54
|
+
dispatchable: device.auth.method !== 'password',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export class DevicesHostProvider {
|
|
58
|
+
id = 'devices';
|
|
59
|
+
capabilities() {
|
|
60
|
+
// mutate stays false: `agents devices sync/add/set` own the registry.
|
|
61
|
+
return { directory: true, mutate: false, presence: true, relay: false, lease: false };
|
|
62
|
+
}
|
|
63
|
+
async list() {
|
|
64
|
+
const devices = await loadDevices();
|
|
65
|
+
const out = [];
|
|
66
|
+
for (const device of Object.values(devices)) {
|
|
67
|
+
const host = deviceToPoolHost(device);
|
|
68
|
+
if (host)
|
|
69
|
+
out.push(host);
|
|
70
|
+
}
|
|
71
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
72
|
+
}
|
|
73
|
+
async resolve(name) {
|
|
74
|
+
const device = await getDevice(name);
|
|
75
|
+
if (!device)
|
|
76
|
+
return null;
|
|
77
|
+
// Resolving is asking to dispatch. A control device can't run agents — fail
|
|
78
|
+
// loud with a clear message instead of attempting an SSH dispatch onto a
|
|
79
|
+
// phone (which remoteShellFor would treat as a POSIX host).
|
|
80
|
+
if (isControlDevice(device)) {
|
|
81
|
+
throw new Error(`Device "${device.name}" is a control device (a cockpit), not an executor — it can't run agents. Dispatch to a worker device instead.`);
|
|
82
|
+
}
|
|
83
|
+
// Keep the long-standing typed refusal for password auth (BatchMode=yes
|
|
84
|
+
// can't answer a prompt).
|
|
85
|
+
if (device.auth.method === 'password') {
|
|
86
|
+
throw new DeviceOffloadUnsupportedError(device.name);
|
|
87
|
+
}
|
|
88
|
+
const host = deviceToPoolHost(device);
|
|
89
|
+
if (!host) {
|
|
90
|
+
throw new Error(`Device "${device.name}" has no address (Tailscale DNS name or IP) to reach it by.`);
|
|
91
|
+
}
|
|
92
|
+
return host;
|
|
93
|
+
}
|
|
94
|
+
async presence(name) {
|
|
95
|
+
const device = await getDevice(name);
|
|
96
|
+
return device ? statusOf(device) : 'unknown';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -2,30 +2,24 @@
|
|
|
2
2
|
* Host provider registry.
|
|
3
3
|
*
|
|
4
4
|
* Mirrors the cloud provider registry: a Map of provider id → implementation,
|
|
5
|
-
* instantiated once.
|
|
6
|
-
*
|
|
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
|
*/
|
|
8
9
|
import type { Host, HostProvider, HostProviderId } from './types.js';
|
|
10
|
+
import { DeviceOffloadUnsupportedError } from './types.js';
|
|
11
|
+
export { DeviceOffloadUnsupportedError };
|
|
9
12
|
export declare function getProvider(id: HostProviderId): HostProvider;
|
|
10
13
|
export declare function getAllProviders(): HostProvider[];
|
|
11
14
|
/** Every host across all registered providers, deduped by name (first wins). */
|
|
12
15
|
export declare function listAllHosts(): Promise<Host[]>;
|
|
13
|
-
/**
|
|
14
|
-
* Thrown when a device resolves but can't be used as an offload target because
|
|
15
|
-
* it authenticates with a password. The offload path runs over `sshExec`, whose
|
|
16
|
-
* `SSH_OPTS` force `BatchMode=yes` (no password prompts), so only key / ssh-config
|
|
17
|
-
* auth can carry a `--host` run. Named so the top-level catch prints the message
|
|
18
|
-
* cleanly instead of a stack trace.
|
|
19
|
-
*/
|
|
20
|
-
export declare class DeviceOffloadUnsupportedError extends Error {
|
|
21
|
-
constructor(name: string);
|
|
22
|
-
}
|
|
23
16
|
/**
|
|
24
17
|
* Resolve a host name to a single host, or null if unknown. Resolution order:
|
|
25
|
-
* 1. host providers — the `agents hosts` registry
|
|
26
|
-
*
|
|
27
|
-
* `
|
|
28
|
-
*
|
|
18
|
+
* 1. host providers, in registration order — the `agents hosts` registry
|
|
19
|
+
* (`local`: agents.yaml overlay + ssh-config), then the devices registry
|
|
20
|
+
* (`devices`: a machine registered once with `agents devices sync` is
|
|
21
|
+
* reachable by `--host`/`--device` with no second enroll)
|
|
22
|
+
* 2. an ad-hoc `user@host` (must contain `@`, validated) — nothing to register
|
|
29
23
|
*
|
|
30
24
|
* A bare unknown name returns null so capability-tag routing (`resolveHostByCap`)
|
|
31
25
|
* stays reachable: `--host gpu` must fall through to a cap lookup, not be misread
|
|
@@ -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.
|
|
6
|
-
*
|
|
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 {
|
|
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
|
|
85
|
-
*
|
|
86
|
-
* `
|
|
87
|
-
*
|
|
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
|
-
|
|
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
|
|
@@ -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>;
|