@phnx-labs/agents-cli 1.20.74 → 1.20.76
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 +893 -0
- package/README.md +36 -10
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.js +21 -3
- package/dist/commands/attach.d.ts +10 -0
- package/dist/commands/attach.js +41 -0
- package/dist/commands/computer.js +3 -3
- package/dist/commands/detach-core.d.ts +46 -0
- package/dist/commands/detach-core.js +61 -0
- package/dist/commands/detach.d.ts +5 -0
- package/dist/commands/detach.js +171 -0
- package/dist/commands/doctor.js +18 -0
- package/dist/commands/exec.js +31 -3
- package/dist/commands/feed.d.ts +7 -1
- package/dist/commands/feed.js +47 -4
- package/dist/commands/go.d.ts +9 -2
- package/dist/commands/go.js +16 -6
- package/dist/commands/harness.d.ts +14 -0
- package/dist/commands/harness.js +145 -0
- package/dist/commands/import.js +38 -8
- package/dist/commands/inspect.js +5 -2
- package/dist/commands/profiles.d.ts +20 -0
- package/dist/commands/profiles.js +68 -21
- package/dist/commands/repo.js +14 -0
- package/dist/commands/routines.js +15 -2
- package/dist/commands/secrets.js +44 -21
- package/dist/commands/sessions-browser.js +1 -1
- package/dist/commands/sessions-export.js +19 -5
- package/dist/commands/sessions-migrate.d.ts +29 -0
- package/dist/commands/sessions-migrate.js +596 -0
- package/dist/commands/sessions-picker.js +16 -1
- package/dist/commands/sessions.d.ts +36 -13
- package/dist/commands/sessions.js +210 -40
- package/dist/commands/setup-computer.js +2 -2
- package/dist/commands/ssh.js +71 -35
- package/dist/commands/status.js +10 -2
- package/dist/commands/versions.js +15 -6
- package/dist/commands/view.d.ts +5 -0
- package/dist/commands/view.js +24 -4
- package/dist/commands/watchdog.d.ts +7 -2
- package/dist/commands/watchdog.js +73 -57
- package/dist/index.js +58 -7
- package/dist/lib/activity.d.ts +14 -4
- package/dist/lib/activity.js +17 -5
- package/dist/lib/actor.d.ts +50 -0
- package/dist/lib/actor.js +166 -0
- package/dist/lib/agent-spec/provider.js +2 -1
- package/dist/lib/agent-spec/resolve.js +19 -5
- package/dist/lib/agent-spec/types.d.ts +9 -1
- package/dist/lib/agents.d.ts +21 -0
- package/dist/lib/agents.js +70 -6
- package/dist/lib/cloud/codex.d.ts +2 -0
- package/dist/lib/cloud/codex.js +14 -3
- package/dist/lib/cloud/session-index.d.ts +32 -0
- package/dist/lib/cloud/session-index.js +58 -0
- package/dist/lib/cloud/store.d.ts +7 -0
- package/dist/lib/cloud/store.js +25 -0
- package/dist/lib/config-transfer.js +4 -0
- package/dist/lib/daemon.js +18 -2
- package/dist/lib/devices/connect.d.ts +15 -1
- package/dist/lib/devices/connect.js +15 -1
- package/dist/lib/devices/fleet.d.ts +21 -0
- package/dist/lib/devices/fleet.js +26 -0
- package/dist/lib/devices/health-report.d.ts +38 -0
- package/dist/lib/devices/health-report.js +214 -0
- package/dist/lib/devices/health.js +4 -1
- package/dist/lib/devices/reachability.d.ts +31 -0
- package/dist/lib/devices/reachability.js +40 -0
- package/dist/lib/devices/registry.d.ts +33 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/devices/resolve-target.d.ts +15 -27
- package/dist/lib/devices/resolve-target.js +63 -102
- package/dist/lib/devices/sync.d.ts +18 -0
- package/dist/lib/devices/sync.js +23 -1
- package/dist/lib/devices/tailscale.d.ts +3 -0
- package/dist/lib/devices/tailscale.js +1 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +66 -0
- package/dist/lib/exec.js +195 -6
- package/dist/lib/feed-post.d.ts +63 -0
- package/dist/lib/feed-post.js +204 -0
- package/dist/lib/fleet/apply.d.ts +32 -2
- package/dist/lib/fleet/apply.js +97 -10
- package/dist/lib/fleet/types.d.ts +11 -0
- package/dist/lib/fs-walk.d.ts +13 -0
- package/dist/lib/fs-walk.js +16 -7
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +156 -0
- package/dist/lib/hosts/dispatch.d.ts +7 -0
- package/dist/lib/hosts/dispatch.js +2 -0
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/reconnect.d.ts +78 -0
- package/dist/lib/hosts/reconnect.js +127 -0
- package/dist/lib/hosts/registry.d.ts +75 -14
- package/dist/lib/hosts/registry.js +205 -30
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/hosts/run-target.d.ts +13 -5
- package/dist/lib/hosts/run-target.js +26 -10
- package/dist/lib/hosts/session-index.d.ts +18 -1
- package/dist/lib/hosts/session-index.js +37 -2
- package/dist/lib/hosts/session-marker.d.ts +33 -0
- package/dist/lib/hosts/session-marker.js +51 -0
- package/dist/lib/hosts/tasks.d.ts +8 -4
- package/dist/lib/import.d.ts +2 -0
- package/dist/lib/import.js +35 -2
- package/dist/lib/menubar/install-menubar.d.ts +20 -0
- package/dist/lib/menubar/install-menubar.js +45 -1
- package/dist/lib/migrate.d.ts +16 -0
- package/dist/lib/migrate.js +36 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +54 -1
- package/dist/lib/overdue.d.ts +7 -2
- package/dist/lib/overdue.js +18 -11
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/process.d.ts +17 -0
- package/dist/lib/platform/process.js +70 -0
- package/dist/lib/profiles-presets.js +9 -7
- package/dist/lib/profiles.d.ts +31 -0
- package/dist/lib/profiles.js +70 -0
- package/dist/lib/pty-server.d.ts +2 -10
- package/dist/lib/pty-server.js +4 -38
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +48 -8
- package/dist/lib/rotate.d.ts +18 -0
- package/dist/lib/rotate.js +28 -0
- package/dist/lib/routines.d.ts +26 -0
- package/dist/lib/routines.js +19 -2
- package/dist/lib/runner.js +104 -7
- package/dist/lib/secrets/agent.d.ts +66 -1
- package/dist/lib/secrets/agent.js +154 -90
- package/dist/lib/secrets/bundles.d.ts +34 -0
- package/dist/lib/secrets/bundles.js +179 -19
- package/dist/lib/secrets/index.d.ts +15 -1
- package/dist/lib/secrets/index.js +21 -3
- package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
- package/dist/lib/secrets/rc-hygiene.js +154 -0
- package/dist/lib/secrets/remote.d.ts +7 -4
- package/dist/lib/secrets/remote.js +7 -4
- package/dist/lib/secrets/sync-commands.d.ts +21 -0
- package/dist/lib/secrets/sync-commands.js +21 -0
- package/dist/lib/self-update.d.ts +23 -2
- package/dist/lib/self-update.js +86 -5
- package/dist/lib/session/active.d.ts +114 -7
- package/dist/lib/session/active.js +323 -52
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +48 -0
- package/dist/lib/session/db.js +238 -14
- package/dist/lib/session/detached.d.ts +30 -0
- package/dist/lib/session/detached.js +92 -0
- package/dist/lib/session/discover.d.ts +465 -2
- package/dist/lib/session/discover.js +1648 -468
- package/dist/lib/session/fork.js +2 -1
- package/dist/lib/session/hook-sessions.d.ts +43 -0
- package/dist/lib/session/hook-sessions.js +135 -0
- package/dist/lib/session/linear.d.ts +6 -0
- package/dist/lib/session/linear.js +55 -0
- package/dist/lib/session/migrate-targets.d.ts +65 -0
- package/dist/lib/session/migrate-targets.js +94 -0
- package/dist/lib/session/migrations.d.ts +37 -0
- package/dist/lib/session/migrations.js +60 -0
- package/dist/lib/session/parse.d.ts +18 -0
- package/dist/lib/session/parse.js +130 -30
- package/dist/lib/session/pid-registry.d.ts +34 -2
- package/dist/lib/session/pid-registry.js +49 -2
- package/dist/lib/session/render.d.ts +7 -2
- package/dist/lib/session/render.js +22 -10
- package/dist/lib/session/short-id.d.ts +17 -0
- package/dist/lib/session/short-id.js +20 -0
- package/dist/lib/session/state.js +6 -2
- package/dist/lib/shims.d.ts +1 -1
- package/dist/lib/shims.js +20 -6
- package/dist/lib/sqlite.d.ts +3 -2
- package/dist/lib/sqlite.js +27 -4
- package/dist/lib/staleness/detectors/commands.js +1 -1
- package/dist/lib/staleness/detectors/workflows.js +13 -2
- package/dist/lib/staleness/writers/commands.js +7 -7
- package/dist/lib/staleness/writers/workflows.d.ts +4 -2
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +3 -0
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +1 -32
- package/dist/lib/teams/agents.d.ts +19 -10
- package/dist/lib/teams/agents.js +40 -39
- package/dist/lib/types.d.ts +37 -2
- package/dist/lib/usage.d.ts +33 -1
- package/dist/lib/usage.js +172 -12
- package/dist/lib/versions.d.ts +9 -2
- package/dist/lib/versions.js +37 -8
- package/dist/lib/watchdog/log.d.ts +43 -0
- package/dist/lib/watchdog/log.js +69 -0
- package/dist/lib/watchdog/routine.d.ts +44 -0
- package/dist/lib/watchdog/routine.js +69 -0
- package/dist/lib/watchdog/runner.d.ts +51 -7
- package/dist/lib/watchdog/runner.js +239 -64
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +31 -16
- package/dist/lib/workflows.d.ts +16 -0
- package/dist/lib/workflows.js +110 -1
- package/package.json +1 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { splitUserHost } from '../hosts/registry.js';
|
|
2
|
+
import { type DeviceProfile } from './registry.js';
|
|
3
|
+
export { splitUserHost };
|
|
2
4
|
/** A dialable peer: the ssh target, the machine id used to tag its rows, a
|
|
3
5
|
* display name, and the OS family that picks the remote shell dialect. */
|
|
4
6
|
export interface ResolvedSshTarget {
|
|
@@ -7,35 +9,21 @@ export interface ResolvedSshTarget {
|
|
|
7
9
|
name: string;
|
|
8
10
|
os?: string;
|
|
9
11
|
}
|
|
10
|
-
/** Split a `user@host` / `host` token into its login user (if any) and host part. */
|
|
11
|
-
export declare function splitUserHost(token: string): {
|
|
12
|
-
user?: string;
|
|
13
|
-
host: string;
|
|
14
|
-
};
|
|
15
|
-
/**
|
|
16
|
-
* Resolve one `--host`/`--device` token to a concrete ssh target through the
|
|
17
|
-
* registry. Registry hit → the device's real address + platform (so the machine
|
|
18
|
-
* id, route, and OS all match the auto-discovery sweep), with any `user@`
|
|
19
|
-
* overriding the login account. Miss → a literal `user@host` fallback, its OS
|
|
20
|
-
* taken from the host overlay if enrolled. Returns undefined only when the token
|
|
21
|
-
* fails the shared ssh-target injection guard.
|
|
22
|
-
*/
|
|
23
|
-
export declare function resolveSshTarget(token: string, reg: DeviceRegistry): ResolvedSshTarget | undefined;
|
|
24
12
|
/**
|
|
25
13
|
* Resolve a target token to a full {@link DeviceProfile} for `agents ssh`. Same
|
|
26
|
-
* grammar as
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* returns undefined so the caller reports "Unknown
|
|
32
|
-
*
|
|
14
|
+
* grammar as the fan-out, but returns the whole profile (auth, shell, tailscale
|
|
15
|
+
* metadata) `buildSshInvocation` needs. A registered `name` / `user@device`
|
|
16
|
+
* yields that profile (login user overridden by any `user@`); an ad-hoc
|
|
17
|
+
* `user@host` / IP / FQDN literal yields a synthesized key-auth profile. A bare
|
|
18
|
+
* unregistered alias (no `@`/dot) — or an ssh_config-only alias, which `agents
|
|
19
|
+
* ssh` has never dialed — returns undefined so the caller reports "Unknown
|
|
20
|
+
* device" rather than dialing a literal.
|
|
33
21
|
*/
|
|
34
|
-
export declare function resolveDeviceTarget(token: string
|
|
22
|
+
export declare function resolveDeviceTarget(token: string): Promise<DeviceProfile | undefined>;
|
|
35
23
|
/**
|
|
36
|
-
* Resolve an explicit `--host`/`--device` list to dialable targets
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
24
|
+
* Resolve an explicit `--host`/`--device` list to dialable targets. A token that
|
|
25
|
+
* fails the injection guard or names nothing reachable is skipped with a stderr
|
|
26
|
+
* note (never fatal — one bad token must not blank the fan-out). Shared by every
|
|
27
|
+
* cross-machine fan-out so they can never diverge onto two routes.
|
|
40
28
|
*/
|
|
41
29
|
export declare function resolveExplicitTargets(hosts: string[]): Promise<ResolvedSshTarget[]>;
|
|
@@ -1,86 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Fan-out + `agents ssh` adapters over the single host/device resolver.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
4
|
+
* Every `--host` / `--device` token becomes a concrete target through one core,
|
|
5
|
+
* {@link matchHost} in ../hosts/registry.ts — the merged directory that reads the
|
|
6
|
+
* devices registry, the agents.yaml overlay, AND ssh_config (RUSH-1967). This
|
|
7
|
+
* module is only the two thin adapters the fan-out and interactive-ssh paths
|
|
8
|
+
* need on top of that core:
|
|
9
|
+
* - {@link resolveExplicitTargets} — a token list → dialable `{target, machine,
|
|
10
|
+
* name, os}` rows, so `sessions --host` / session bundles / remote agents-json
|
|
11
|
+
* dial the exact same address (and machine id) `run --host` does.
|
|
12
|
+
* - {@link resolveDeviceTarget} — a token → the full {@link DeviceProfile}
|
|
13
|
+
* `agents ssh` needs (auth, shell, tailscale metadata), with the same
|
|
14
|
+
* grammar; a bare unregistered alias returns undefined ("Unknown device").
|
|
15
15
|
*
|
|
16
|
-
* The grammar is uniform across the fleet: `
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* short-circuits to a bare `ssh muqsit@mac-mini` (LAN DNS). A `user@host` that
|
|
20
|
-
* matches no registered device falls back to a literal target so ad-hoc boxes
|
|
21
|
-
* still work.
|
|
16
|
+
* The grammar is uniform across the fleet: `name`, `user@name` (same device,
|
|
17
|
+
* login user overridden), a tailnet FQDN, an ssh_config alias, and an ad-hoc
|
|
18
|
+
* `user@host` all resolve identically no matter which subcommand called.
|
|
22
19
|
*/
|
|
23
20
|
import chalk from 'chalk';
|
|
24
|
-
import {
|
|
21
|
+
import { sshTargetFor } from '../hosts/types.js';
|
|
22
|
+
import { matchHost, splitUserHost } from '../hosts/registry.js';
|
|
25
23
|
import { normalizeHost } from '../machine-id.js';
|
|
26
24
|
import { resolveRemoteOsSync } from '../hosts/remote-os.js';
|
|
27
|
-
|
|
28
|
-
import { loadDevices } from './registry.js';
|
|
29
|
-
/** Split a `user@host` / `host` token into its login user (if any) and host part. */
|
|
30
|
-
export function splitUserHost(token) {
|
|
31
|
-
const at = token.indexOf('@');
|
|
32
|
-
return at === -1 ? { host: token } : { user: token.slice(0, at), host: token.slice(at + 1) };
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Match a host part (the piece after any `user@`) to a registered device: exact
|
|
36
|
-
* registry key first, then a normalized-host match so `yosemite-s0` and
|
|
37
|
-
* `yosemite-s0.<tailnet>.ts.net` land on the same profile. The single source of
|
|
38
|
-
* truth both `resolveSshTarget` (fan-out) and `resolveDeviceTarget` (`agents
|
|
39
|
-
* ssh`) share, so a `user@device` can never resolve two different routes.
|
|
40
|
-
*/
|
|
41
|
-
function matchDevice(host, reg) {
|
|
42
|
-
return reg[host] ?? Object.values(reg).find((d) => normalizeHost(d.name) === normalizeHost(host));
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Resolve one `--host`/`--device` token to a concrete ssh target through the
|
|
46
|
-
* registry. Registry hit → the device's real address + platform (so the machine
|
|
47
|
-
* id, route, and OS all match the auto-discovery sweep), with any `user@`
|
|
48
|
-
* overriding the login account. Miss → a literal `user@host` fallback, its OS
|
|
49
|
-
* taken from the host overlay if enrolled. Returns undefined only when the token
|
|
50
|
-
* fails the shared ssh-target injection guard.
|
|
51
|
-
*/
|
|
52
|
-
export function resolveSshTarget(token, reg) {
|
|
53
|
-
try {
|
|
54
|
-
assertValidSshTarget(token);
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
return undefined;
|
|
58
|
-
}
|
|
59
|
-
const { user, host } = splitUserHost(token);
|
|
60
|
-
// A device and a `user@device` are the same box; resolve the host part through
|
|
61
|
-
// the registry so both dial the Tailscale route, and let an explicit `user@`
|
|
62
|
-
// override only the login account.
|
|
63
|
-
const device = matchDevice(host, reg);
|
|
64
|
-
if (device) {
|
|
65
|
-
try {
|
|
66
|
-
const effective = user ? { ...device, user } : device;
|
|
67
|
-
return { target: sshTargetFor(effective), machine: normalizeHost(device.name), name: device.name, os: device.platform };
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
// Registered but has no address to dial — fall through to the literal token.
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return { target: token, machine: normalizeHost(host), name: token, os: resolveRemoteOsSync(token) };
|
|
74
|
-
}
|
|
25
|
+
export { splitUserHost };
|
|
75
26
|
/** Timestamps for a synthesized ad-hoc profile — never persisted, so a constant
|
|
76
27
|
* keeps the value deterministic (and side-effect free) without reading the clock. */
|
|
77
28
|
const SYNTH_TS = '1970-01-01T00:00:00.000Z';
|
|
78
|
-
/** True when a token is clearly a network target (a `user@`, or a dotted/IPv6
|
|
79
|
-
* host / IP) rather than a bare alias. A bare unknown word is a typo, so `agents
|
|
80
|
-
* ssh foo` still says "Unknown device" instead of dialing a literal `foo`. */
|
|
81
|
-
function looksLikeHostLiteral(token) {
|
|
82
|
-
return token.includes('@') || token.includes('.') || token.includes(':');
|
|
83
|
-
}
|
|
84
29
|
/** Synthesize a throwaway device profile for an ad-hoc `user@host` / `host`
|
|
85
30
|
* literal so `agents ssh` can dial a box that was never registered. */
|
|
86
31
|
function adHocDevice(token, host, user) {
|
|
@@ -97,49 +42,65 @@ function adHocDevice(token, host, user) {
|
|
|
97
42
|
};
|
|
98
43
|
}
|
|
99
44
|
/**
|
|
100
|
-
* Resolve
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* yields a synthesized key-auth profile. A bare unregistered alias (no `@`/dot)
|
|
106
|
-
* returns undefined so the caller reports "Unknown device" rather than dialing a
|
|
107
|
-
* literal — the strict behaviour the interactive wrapper has always had.
|
|
45
|
+
* Resolve one token to a dialable {@link ResolvedSshTarget}, or undefined when it
|
|
46
|
+
* fails the injection guard or names nothing reachable. A registered device (or
|
|
47
|
+
* `user@device`) dials its live Tailscale route; an ssh_config-only alias dials
|
|
48
|
+
* by its bare name (ssh applies the stanza) — now visible to the fan-out too; an
|
|
49
|
+
* ad-hoc `user@host` dials the literal.
|
|
108
50
|
*/
|
|
109
|
-
|
|
51
|
+
async function toResolvedTarget(token) {
|
|
52
|
+
const host = await matchHost(token);
|
|
53
|
+
if (!host)
|
|
54
|
+
return undefined;
|
|
55
|
+
let target;
|
|
110
56
|
try {
|
|
111
|
-
|
|
57
|
+
target = sshTargetFor(host);
|
|
112
58
|
}
|
|
113
59
|
catch {
|
|
60
|
+
return undefined; // matched a device/host with no address to dial
|
|
61
|
+
}
|
|
62
|
+
const hostPart = host.device ? host.device.name : splitUserHost(host.name).host;
|
|
63
|
+
const name = host.device ? host.device.name : host.name;
|
|
64
|
+
return { target, machine: normalizeHost(hostPart), name, os: host.os ?? resolveRemoteOsSync(name) };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Resolve a target token to a full {@link DeviceProfile} for `agents ssh`. Same
|
|
68
|
+
* grammar as the fan-out, but returns the whole profile (auth, shell, tailscale
|
|
69
|
+
* metadata) `buildSshInvocation` needs. A registered `name` / `user@device`
|
|
70
|
+
* yields that profile (login user overridden by any `user@`); an ad-hoc
|
|
71
|
+
* `user@host` / IP / FQDN literal yields a synthesized key-auth profile. A bare
|
|
72
|
+
* unregistered alias (no `@`/dot) — or an ssh_config-only alias, which `agents
|
|
73
|
+
* ssh` has never dialed — returns undefined so the caller reports "Unknown
|
|
74
|
+
* device" rather than dialing a literal.
|
|
75
|
+
*/
|
|
76
|
+
export async function resolveDeviceTarget(token) {
|
|
77
|
+
const host = await matchHost(token, { allowBareLiteral: true });
|
|
78
|
+
if (!host)
|
|
114
79
|
return undefined;
|
|
80
|
+
if (host.device) {
|
|
81
|
+
const { user } = splitUserHost(token);
|
|
82
|
+
return user ? { ...host.device, user } : host.device;
|
|
115
83
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
84
|
+
if (host.adhoc) {
|
|
85
|
+
const { user, host: hostPart } = splitUserHost(token);
|
|
86
|
+
return adHocDevice(token, hostPart, user);
|
|
87
|
+
}
|
|
88
|
+
// An overlay / ssh_config-only match is not a device — `agents ssh` stays
|
|
89
|
+
// devices-and-literals only, so report it as unknown.
|
|
122
90
|
return undefined;
|
|
123
91
|
}
|
|
124
92
|
/**
|
|
125
|
-
* Resolve an explicit `--host`/`--device` list to dialable targets
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
93
|
+
* Resolve an explicit `--host`/`--device` list to dialable targets. A token that
|
|
94
|
+
* fails the injection guard or names nothing reachable is skipped with a stderr
|
|
95
|
+
* note (never fatal — one bad token must not blank the fan-out). Shared by every
|
|
96
|
+
* cross-machine fan-out so they can never diverge onto two routes.
|
|
129
97
|
*/
|
|
130
98
|
export async function resolveExplicitTargets(hosts) {
|
|
131
|
-
let reg;
|
|
132
|
-
try {
|
|
133
|
-
reg = await loadDevices();
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
reg = {};
|
|
137
|
-
}
|
|
138
99
|
const out = [];
|
|
139
100
|
for (const h of hosts) {
|
|
140
|
-
const resolved =
|
|
101
|
+
const resolved = await toResolvedTarget(h);
|
|
141
102
|
if (!resolved) {
|
|
142
|
-
process.stderr.write(chalk.gray(` ${h}: not a
|
|
103
|
+
process.stderr.write(chalk.gray(` ${h}: not a resolvable ssh target — skipped\n`));
|
|
143
104
|
continue;
|
|
144
105
|
}
|
|
145
106
|
out.push(resolved);
|
|
@@ -45,6 +45,24 @@ export interface DeviceSyncResult {
|
|
|
45
45
|
/** Populated when ok is false: why discovery was skipped. */
|
|
46
46
|
reason?: string;
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* The nodes automatic discovery is allowed to see: sharee nodes (shared INTO
|
|
50
|
+
* the tailnet by another user, e.g. a funnel ingress relay) are not the
|
|
51
|
+
* operator's machines, so they must never be bootstrap-registered nor surfaced
|
|
52
|
+
* as pending. Explicit paths (`devices register <name>`, `devices add`, the
|
|
53
|
+
* `fleet:` manifest bootstrap) are deliberate user actions and stay unfiltered.
|
|
54
|
+
* Pure so the policy is unit-testable without a tailnet.
|
|
55
|
+
*/
|
|
56
|
+
export declare function discoverableNodes(nodes: TailscaleNode[]): TailscaleNode[];
|
|
57
|
+
/**
|
|
58
|
+
* Default checked-state for a node in the interactive sync picker. Pressing
|
|
59
|
+
* Enter registers every checked node, so "checked" must mean "auto-sync would
|
|
60
|
+
* register this": not dismissed, and not a sharee node — unless the user
|
|
61
|
+
* already deliberately registered that sharee node, in which case Enter must
|
|
62
|
+
* keep the fleet as-is. Pure so the default matrix is unit-testable without a
|
|
63
|
+
* prompt.
|
|
64
|
+
*/
|
|
65
|
+
export declare function defaultPickerChecked(node: TailscaleNode, registered: Set<string>, ignored: Set<string>): boolean;
|
|
48
66
|
/**
|
|
49
67
|
* Node names present on the tailnet but neither already in the registry nor on
|
|
50
68
|
* the ignore-list — i.e. genuinely new devices worth surfacing. Pure so the
|
package/dist/lib/devices/sync.js
CHANGED
|
@@ -59,6 +59,28 @@ export function withDefaultUser(input, prevUser, localUser) {
|
|
|
59
59
|
return input;
|
|
60
60
|
return { ...input, user: localUser };
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* The nodes automatic discovery is allowed to see: sharee nodes (shared INTO
|
|
64
|
+
* the tailnet by another user, e.g. a funnel ingress relay) are not the
|
|
65
|
+
* operator's machines, so they must never be bootstrap-registered nor surfaced
|
|
66
|
+
* as pending. Explicit paths (`devices register <name>`, `devices add`, the
|
|
67
|
+
* `fleet:` manifest bootstrap) are deliberate user actions and stay unfiltered.
|
|
68
|
+
* Pure so the policy is unit-testable without a tailnet.
|
|
69
|
+
*/
|
|
70
|
+
export function discoverableNodes(nodes) {
|
|
71
|
+
return nodes.filter((n) => !n.sharee);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Default checked-state for a node in the interactive sync picker. Pressing
|
|
75
|
+
* Enter registers every checked node, so "checked" must mean "auto-sync would
|
|
76
|
+
* register this": not dismissed, and not a sharee node — unless the user
|
|
77
|
+
* already deliberately registered that sharee node, in which case Enter must
|
|
78
|
+
* keep the fleet as-is. Pure so the default matrix is unit-testable without a
|
|
79
|
+
* prompt.
|
|
80
|
+
*/
|
|
81
|
+
export function defaultPickerChecked(node, registered, ignored) {
|
|
82
|
+
return !ignored.has(node.name) && (!node.sharee || registered.has(node.name));
|
|
83
|
+
}
|
|
62
84
|
/**
|
|
63
85
|
* Node names present on the tailnet but neither already in the registry nor on
|
|
64
86
|
* the ignore-list — i.e. genuinely new devices worth surfacing. Pure so the
|
|
@@ -101,7 +123,7 @@ export async function runDeviceSync(opts = {}) {
|
|
|
101
123
|
// the same host at once) would otherwise abort the whole `agents sync`. The
|
|
102
124
|
// whole body is inside the guard so the "never a sync failure" promise holds.
|
|
103
125
|
try {
|
|
104
|
-
const nodes = parseTailscaleStatus(tailscaleStatusJson());
|
|
126
|
+
const nodes = discoverableNodes(parseTailscaleStatus(tailscaleStatusJson()));
|
|
105
127
|
const [registeredBefore, ignored] = await Promise.all([loadDevices(), loadIgnored()]);
|
|
106
128
|
const registered = new Set(Object.keys(registeredBefore));
|
|
107
129
|
const pendingNames = computePendingDevices(nodes, registered, ignored);
|
|
@@ -9,6 +9,9 @@ export interface TailscaleNode {
|
|
|
9
9
|
direct: boolean;
|
|
10
10
|
relay?: string;
|
|
11
11
|
lastSeen?: string;
|
|
12
|
+
/** True for a node another user shared INTO this tailnet — not the operator's
|
|
13
|
+
* own machine, so discovery must never auto-register or suggest it. */
|
|
14
|
+
sharee: boolean;
|
|
12
15
|
}
|
|
13
16
|
/**
|
|
14
17
|
* Slugify a raw Tailscale HostName into a valid logical device name (ssh alias
|
package/dist/lib/events.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - Performance tracking: withTiming() wrapper for any async function
|
|
13
13
|
*/
|
|
14
14
|
export type EventLevel = 'audit' | 'warn' | 'info' | 'debug';
|
|
15
|
-
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'file.edited' | 'error' | 'warn' | 'info' | 'debug';
|
|
15
|
+
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'status.posted' | 'file.edited' | 'error' | 'warn' | 'info' | 'debug';
|
|
16
16
|
export declare function levelFor(event: EventType): EventLevel;
|
|
17
17
|
export interface EventMeta {
|
|
18
18
|
ts: string;
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -144,6 +144,15 @@ export interface ExecOptions {
|
|
|
144
144
|
* (output is mirrored to the parent's stdout exactly like stdio:'inherit').
|
|
145
145
|
*/
|
|
146
146
|
captureStdoutTail?: boolean;
|
|
147
|
+
/**
|
|
148
|
+
* Print the run's resolved session id to stdout as a one-line sentinel once the
|
|
149
|
+
* child exits (see hosts/session-marker.ts). Set by the `--host` dispatch so the
|
|
150
|
+
* LAUNCHER can relate the remote-created session back to itself — Claude's id is
|
|
151
|
+
* forced up front, but every other agent coins its own id on the remote box, and
|
|
152
|
+
* this marker is how that id rides the followed log home. Headless-only and inert
|
|
153
|
+
* for interactive runs (no combined log to parse).
|
|
154
|
+
*/
|
|
155
|
+
emitSessionId?: boolean;
|
|
147
156
|
/**
|
|
148
157
|
* Escape hatch for the interactive tmux spawn-wrap (see shouldWrapInTmux):
|
|
149
158
|
* when true, spawn the agent directly instead of inside a shared-socket tmux
|
|
@@ -339,6 +348,63 @@ export declare const BUDGET_KILL_EXIT_CODE = 7;
|
|
|
339
348
|
export declare const RATE_LIMIT_PATTERNS: RegExp[];
|
|
340
349
|
/** Return true if the text contains any known rate-limit or overload indicator. */
|
|
341
350
|
export declare function detectRateLimit(text: string): boolean;
|
|
351
|
+
/**
|
|
352
|
+
* Patterns that indicate an authentication failure — the agent is logged out,
|
|
353
|
+
* its token was revoked, or the session expired. These are the user-visible
|
|
354
|
+
* strings a logged-out agent surfaces (observed across the routine-run corpus).
|
|
355
|
+
* Unlike a rate limit, an auth failure is NOT self-healing by failover — every
|
|
356
|
+
* chain entry on the same account fails identically — so it is classified
|
|
357
|
+
* separately and never triggers a fallback attempt.
|
|
358
|
+
*
|
|
359
|
+
* The bare `401` is deliberately paired with an auth keyword: a plain "401" can
|
|
360
|
+
* appear in legitimate output (an HTTP-status table, a log line), so it only
|
|
361
|
+
* counts when it co-occurs with OAuth/authentication/credentials/Unauthorized.
|
|
362
|
+
*/
|
|
363
|
+
export declare const AUTH_FAILURE_PATTERNS: RegExp[];
|
|
364
|
+
/**
|
|
365
|
+
* Return true if the text contains any known authentication-failure indicator.
|
|
366
|
+
* Agent-agnostic: matches the user-visible error string wherever it surfaces
|
|
367
|
+
* (stdout tail, a captured error message, a plain-text agent's output).
|
|
368
|
+
*/
|
|
369
|
+
export declare function detectAuthFailure(text: string): boolean;
|
|
370
|
+
/**
|
|
371
|
+
* Return true if a stream-json log carries the structural markers of an auth
|
|
372
|
+
* failure. This is the authoritative signal for Claude: a logged-out run emits
|
|
373
|
+
* {"type":"system","subtype":"api_retry","error":"authentication_failed",…}
|
|
374
|
+
* {"type":"assistant",…,"error":"authentication_failed"}
|
|
375
|
+
* {"type":"result","is_error":true,"result":"Failed to authenticate…"}
|
|
376
|
+
* Note `terminal_reason` is "completed" on such a run, so exit-code / terminal-
|
|
377
|
+
* reason logic can never catch it — the `error:"authentication_failed"` marker
|
|
378
|
+
* and the `result`+`is_error` text are the reliable signals.
|
|
379
|
+
*
|
|
380
|
+
* Gated on the Claude stream-json shape; other agents don't emit these fields,
|
|
381
|
+
* so callers pass their agent and this returns false for non-claude.
|
|
382
|
+
*/
|
|
383
|
+
export declare function detectAuthFailureEvent(logText: string, agent: AgentId): boolean;
|
|
384
|
+
/**
|
|
385
|
+
* Return the first human-readable auth-failure phrase found in the text, for use
|
|
386
|
+
* as a stable, short run `errorMessage` reason. Falls back to null when the only
|
|
387
|
+
* signal was the structural `error:"authentication_failed"` marker with no
|
|
388
|
+
* user-visible string.
|
|
389
|
+
*/
|
|
390
|
+
export declare function authFailureReason(text: string): string | null;
|
|
391
|
+
/**
|
|
392
|
+
* Decide whether a run's stream-json log is an authentication failure. The
|
|
393
|
+
* single source of truth for both the foreground and detached run paths.
|
|
394
|
+
*
|
|
395
|
+
* The structural marker (`detectAuthFailureEvent`, gated on `is_error:true`) is
|
|
396
|
+
* authoritative and safe on ANY exit — it catches a logged-out Claude that exits
|
|
397
|
+
* 0 (its `result` event carries `is_error:true` while `terminal_reason` is
|
|
398
|
+
* "completed"). The raw user-visible string (`detectAuthFailure`) is only
|
|
399
|
+
* consulted when the process actually FAILED, as a fallback for a run that died
|
|
400
|
+
* mid-stream before emitting a `result` event. That gate is what prevents a
|
|
401
|
+
* genuinely-completed run whose output merely *mentions* an auth phrase (e.g. a
|
|
402
|
+
* routine documenting a login flow) from being misclassified and having its
|
|
403
|
+
* legitimate report suppressed.
|
|
404
|
+
*/
|
|
405
|
+
export declare function isAuthFailureFromLog(logText: string, agent: AgentId, opts: {
|
|
406
|
+
processFailed: boolean;
|
|
407
|
+
}): boolean;
|
|
342
408
|
/** An agent (with optional pinned version) in a fallback chain. */
|
|
343
409
|
export interface FallbackEntry {
|
|
344
410
|
agent: AgentId;
|