@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.
- package/CHANGELOG.md +51 -3
- package/README.md +156 -3
- 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.d.ts +48 -0
- package/dist/commands/exec.js +159 -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 +748 -0
- package/dist/commands/output.js +2 -2
- package/dist/commands/plugins.js +28 -7
- 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-browser.d.ts +82 -0
- package/dist/commands/sessions-browser.js +320 -0
- package/dist/commands/sessions-export.js +8 -3
- package/dist/commands/sessions.d.ts +17 -0
- package/dist/commands/sessions.js +157 -10
- package/dist/commands/share.d.ts +2 -0
- package/dist/commands/share.js +150 -0
- package/dist/commands/ssh.js +54 -3
- 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 +35 -14
- package/dist/lib/agents.d.ts +46 -0
- package/dist/lib/agents.js +121 -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/connect.d.ts +18 -1
- package/dist/lib/devices/connect.js +10 -2
- package/dist/lib/devices/fleet.d.ts +3 -2
- package/dist/lib/devices/fleet.js +9 -0
- package/dist/lib/devices/known-hosts.d.ts +62 -0
- package/dist/lib/devices/known-hosts.js +137 -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 +66 -20
- 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 +79 -4
- 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 +205 -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 +45 -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/picker.d.ts +53 -0
- package/dist/lib/picker.js +214 -1
- package/dist/lib/platform/exec.d.ts +16 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/plugins.d.ts +31 -1
- package/dist/lib/plugins.js +175 -15
- 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 +83 -10
- package/dist/lib/secrets/agent.js +237 -70
- package/dist/lib/secrets/bundles.d.ts +26 -0
- package/dist/lib/secrets/bundles.js +59 -8
- package/dist/lib/secrets/filestore.d.ts +9 -0
- package/dist/lib/secrets/filestore.js +21 -8
- package/dist/lib/secrets/index.d.ts +7 -0
- package/dist/lib/secrets/index.js +26 -6
- 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/share/capture.d.ts +29 -0
- package/dist/lib/share/capture.js +140 -0
- package/dist/lib/share/config.d.ts +35 -0
- package/dist/lib/share/config.js +100 -0
- package/dist/lib/share/og.d.ts +25 -0
- package/dist/lib/share/og.js +84 -0
- package/dist/lib/share/provision.d.ts +10 -0
- package/dist/lib/share/provision.js +91 -0
- package/dist/lib/share/publish.d.ts +57 -0
- package/dist/lib/share/publish.js +145 -0
- package/dist/lib/share/worker-template.d.ts +2 -0
- package/dist/lib/share/worker-template.js +82 -0
- package/dist/lib/shims.d.ts +13 -0
- package/dist/lib/shims.js +42 -2
- package/dist/lib/ssh-exec.d.ts +24 -0
- package/dist/lib/ssh-exec.js +15 -3
- 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 +3 -0
- package/dist/lib/startup/command-registry.js +6 -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 +36 -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
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents apply` (alias `ag apply`) — reconcile the whole fleet to a declared
|
|
3
|
+
* profile in one command: install agents-cli + agents, sync config, and
|
|
4
|
+
* propagate login so a machine that's signed in once seeds every device. Kills
|
|
5
|
+
* the "6 hosts x ~8 harnesses = ~48 OAuth flows" slog.
|
|
6
|
+
*
|
|
7
|
+
* The manifest is the `fleet:` block of any `-f` file (default `agents.yaml`).
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as os from 'os';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
import { Option } from 'commander';
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import { setHelpSections } from '../lib/help.js';
|
|
16
|
+
import { machineId } from '../lib/session/sync/config.js';
|
|
17
|
+
import { loadDevices, isControlDevice } from '../lib/devices/registry.js';
|
|
18
|
+
import { readFleetFile, resolveDesired } from '../lib/fleet/manifest.js';
|
|
19
|
+
import { snapshotAuth, materializeAuth, parseAuthBundle, KEYCHAIN_BOUND_ON_MAC } from '../lib/fleet/auth-sync.js';
|
|
20
|
+
import { agentIdOf, diffFleet, probeDevice, runFleetApply, pool, sourceHome, } from '../lib/fleet/apply.js';
|
|
21
|
+
/** Version of the running agents-cli — the fleet target version. */
|
|
22
|
+
function localCliVersion() {
|
|
23
|
+
try {
|
|
24
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(here, '..', '..', 'package.json'), 'utf-8'));
|
|
26
|
+
return String(pkg.version ?? '');
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function readStdin() {
|
|
33
|
+
const chunks = [];
|
|
34
|
+
for await (const c of process.stdin)
|
|
35
|
+
chunks.push(c);
|
|
36
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
37
|
+
}
|
|
38
|
+
/** Hidden path: receive an auth bundle on stdin and materialize it locally. */
|
|
39
|
+
async function runRecvAuth() {
|
|
40
|
+
const raw = await readStdin();
|
|
41
|
+
const bundle = parseAuthBundle(raw);
|
|
42
|
+
const res = materializeAuth(bundle, { home: os.homedir() });
|
|
43
|
+
if (res.errors.length > 0) {
|
|
44
|
+
console.error(`recv-auth: ${res.errors.length} error(s): ${res.errors.join('; ')}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
console.log(`recv-auth: wrote login for ${res.written.join(', ') || '(nothing)'}`);
|
|
48
|
+
}
|
|
49
|
+
function confirm(question) {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
process.stdout.write(question);
|
|
52
|
+
const onData = (d) => {
|
|
53
|
+
process.stdin.pause();
|
|
54
|
+
process.stdin.off('data', onData);
|
|
55
|
+
resolve(/^y(es)?$/i.test(d.toString().trim()));
|
|
56
|
+
};
|
|
57
|
+
process.stdin.resume();
|
|
58
|
+
process.stdin.once('data', onData);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const ONLY_KINDS = {
|
|
62
|
+
agents: new Set(['install-cli', 'upgrade-cli', 'add-agent']),
|
|
63
|
+
config: new Set(['sync-config']),
|
|
64
|
+
login: new Set(['push-login', 'needs-login']),
|
|
65
|
+
};
|
|
66
|
+
/** Render the device x dimension matrix (cribbed from `doctor --devices`). */
|
|
67
|
+
function renderPlan(plan) {
|
|
68
|
+
const rows = plan.devices;
|
|
69
|
+
const nameWidth = Math.max('device'.length, ...rows.map((r) => r.device.length));
|
|
70
|
+
const cell = (row, kinds, okLabel) => {
|
|
71
|
+
if (!row.probe.reachable)
|
|
72
|
+
return chalk.gray('- offline');
|
|
73
|
+
const acts = row.actions.filter((a) => kinds.includes(a.kind));
|
|
74
|
+
if (acts.length === 0)
|
|
75
|
+
return chalk.green(`ok ${okLabel}`);
|
|
76
|
+
if (acts.some((a) => a.kind === 'needs-login')) {
|
|
77
|
+
const push = acts.filter((a) => a.kind === 'push-login').length;
|
|
78
|
+
const need = acts.filter((a) => a.kind === 'needs-login').length;
|
|
79
|
+
return chalk.yellow(`${push} push · ${need} manual`);
|
|
80
|
+
}
|
|
81
|
+
return chalk.cyan('↑ ' + acts.map((a) => a.agent ?? a.kind.replace('-cli', '')).join(','));
|
|
82
|
+
};
|
|
83
|
+
const header = ` ${'device'.padEnd(nameWidth)} ${'agents-cli'.padEnd(12)}${'agents'.padEnd(20)}${'config'.padEnd(10)}login`;
|
|
84
|
+
console.log(chalk.gray(header));
|
|
85
|
+
for (const row of rows) {
|
|
86
|
+
const cli = row.probe.reachable
|
|
87
|
+
? (row.actions.find((a) => a.kind === 'install-cli') ? chalk.cyan('install')
|
|
88
|
+
: row.actions.find((a) => a.kind === 'upgrade-cli') ? chalk.cyan('upgrade')
|
|
89
|
+
: chalk.green(`ok ${row.probe.cliVersion ?? ''}`))
|
|
90
|
+
: chalk.gray('- offline');
|
|
91
|
+
const agentsCell = row.probe.reachable
|
|
92
|
+
? (() => {
|
|
93
|
+
const add = row.actions.filter((a) => a.kind === 'add-agent');
|
|
94
|
+
return add.length === 0 ? chalk.green(`ok ${row.desired.agents.length}/${row.desired.agents.length}`) : chalk.cyan('+ ' + add.map((a) => a.agent).join(','));
|
|
95
|
+
})()
|
|
96
|
+
: chalk.gray('-');
|
|
97
|
+
const configCell = row.probe.reachable
|
|
98
|
+
? (row.actions.some((a) => a.kind === 'sync-config') ? chalk.cyan('↑ sync') : chalk.green('ok'))
|
|
99
|
+
: chalk.gray('-');
|
|
100
|
+
const loginCell = cell(row, ['push-login', 'needs-login'], `${row.desired.agents.length}/${row.desired.agents.length}`);
|
|
101
|
+
console.log(` ${row.device.padEnd(nameWidth)} ${stripPad(cli, 12)}${stripPad(agentsCell, 20)}${stripPad(configCell, 10)}${loginCell}`);
|
|
102
|
+
}
|
|
103
|
+
console.log();
|
|
104
|
+
console.log(chalk.gray(` ${plan.actions.length} action(s) across ${rows.filter((r) => r.probe.reachable).length} reachable device(s)`));
|
|
105
|
+
// Distinguish *why* a login can't be propagated: a macOS keychain-bound token
|
|
106
|
+
// vs. the source simply not being signed in to that agent (no portable file).
|
|
107
|
+
const bound = [];
|
|
108
|
+
const noToken = [];
|
|
109
|
+
for (const r of rows) {
|
|
110
|
+
for (const a of r.loginBlocked) {
|
|
111
|
+
const tag = `${a}@${r.device}`;
|
|
112
|
+
if (r.probe.platform === 'macos' && KEYCHAIN_BOUND_ON_MAC.has(a))
|
|
113
|
+
bound.push(tag);
|
|
114
|
+
else
|
|
115
|
+
noToken.push(tag);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (bound.length > 0) {
|
|
119
|
+
console.log(chalk.yellow(` manual login needed (macOS keychain-bound): ${bound.join(', ')}`));
|
|
120
|
+
}
|
|
121
|
+
if (noToken.length > 0) {
|
|
122
|
+
console.log(chalk.yellow(` manual login needed (no portable token on source): ${noToken.join(', ')}`));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** padEnd on the visible width, ignoring chalk color codes. Exported for tests. */
|
|
126
|
+
export function stripPad(s, width) {
|
|
127
|
+
// Strip the whole SGR sequence (ESC `[` ... `m`). Matching only the `[...m`
|
|
128
|
+
// tail leaves each leading ESC byte counted as visible, so every colored
|
|
129
|
+
// cell over-pads and the plan table misaligns in a real TTY.
|
|
130
|
+
// eslint-disable-next-line no-control-regex
|
|
131
|
+
const visible = s.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
132
|
+
return s + ' '.repeat(Math.max(1, width - visible));
|
|
133
|
+
}
|
|
134
|
+
async function runApply(opts) {
|
|
135
|
+
const file = opts.file ?? 'agents.yaml';
|
|
136
|
+
const manifest = readFleetFile(path.resolve(file));
|
|
137
|
+
const source = machineId();
|
|
138
|
+
const registry = await loadDevices();
|
|
139
|
+
// Control devices (a cockpit) never run agents — exclude them from the
|
|
140
|
+
// reconcile set entirely so `agents apply` doesn't try to install/sync/login
|
|
141
|
+
// onto a paired phone.
|
|
142
|
+
const all = Object.values(registry).filter((d) => !isControlDevice(d));
|
|
143
|
+
const online = all.filter((d) => d.tailscale?.online === true).map((d) => d.name);
|
|
144
|
+
const registered = all.map((d) => d.name);
|
|
145
|
+
let desired = resolveDesired(manifest, { onlineDevices: online, registeredDevices: registered, source });
|
|
146
|
+
if (opts.device) {
|
|
147
|
+
desired = desired.filter((d) => d.device === opts.device);
|
|
148
|
+
if (desired.length === 0)
|
|
149
|
+
throw new Error(`Device '${opts.device}' is not a target in this manifest.`);
|
|
150
|
+
}
|
|
151
|
+
if (opts.login === false)
|
|
152
|
+
desired = desired.map((d) => ({ ...d, login: 'skip' }));
|
|
153
|
+
if (desired.length === 0) {
|
|
154
|
+
console.log(chalk.gray('No target devices — nothing to apply.'));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// Snapshot source auth once for every agent named anywhere in the profile.
|
|
158
|
+
const allAgents = [...new Set(desired.flatMap((d) => d.agents.map(agentIdOf)))];
|
|
159
|
+
const snap = snapshotAuth(allAgents, { home: sourceHome(), platform: process.platform });
|
|
160
|
+
const filesByAgent = new Map();
|
|
161
|
+
for (const f of snap.files) {
|
|
162
|
+
const arr = filesByAgent.get(f.agent) ?? [];
|
|
163
|
+
arr.push(f);
|
|
164
|
+
filesByAgent.set(f.agent, arr);
|
|
165
|
+
}
|
|
166
|
+
const sourceAuth = {
|
|
167
|
+
available: new Set(snap.files.map((f) => f.agent)),
|
|
168
|
+
bound: new Set(snap.bound),
|
|
169
|
+
filesByAgent,
|
|
170
|
+
};
|
|
171
|
+
// Probe every target device in parallel.
|
|
172
|
+
const nameToProfile = new Map(desired.map((d) => [d.device, registry[d.device]]));
|
|
173
|
+
console.log(chalk.gray(`Probing ${desired.length} device(s)…`));
|
|
174
|
+
const probeList = await pool(desired, 6, async (d) => probeDevice(nameToProfile.get(d.device)));
|
|
175
|
+
const probes = new Map(probeList.map((p) => [p.device, p]));
|
|
176
|
+
const targetCliVersion = localCliVersion();
|
|
177
|
+
let plan = diffFleet(desired, probes, { targetCliVersion, sourceAuth });
|
|
178
|
+
// --only filter.
|
|
179
|
+
if (opts.only) {
|
|
180
|
+
const keep = new Set();
|
|
181
|
+
for (const k of opts.only.split(',').map((s) => s.trim()))
|
|
182
|
+
for (const x of ONLY_KINDS[k] ?? [])
|
|
183
|
+
keep.add(x);
|
|
184
|
+
plan = {
|
|
185
|
+
devices: plan.devices.map((r) => ({ ...r, actions: r.actions.filter((a) => keep.has(a.kind)) })),
|
|
186
|
+
actions: plan.actions.filter((a) => keep.has(a.kind)),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
console.log();
|
|
190
|
+
console.log(chalk.bold(`Fleet profile · ${desired.length} device(s) · ${allAgents.length} agent(s) (${allAgents.join(', ')})`));
|
|
191
|
+
renderPlan(plan);
|
|
192
|
+
const isDry = opts.plan || opts.dryRun;
|
|
193
|
+
if (isDry)
|
|
194
|
+
return;
|
|
195
|
+
if (plan.actions.filter((a) => a.kind !== 'needs-login').length === 0) {
|
|
196
|
+
console.log(chalk.green('\nNothing to do — fleet already matches the profile.'));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (!opts.yes) {
|
|
200
|
+
const ok = await confirm(chalk.bold(`\nApply this plan? (${plan.actions.length} action(s)) [y/N] `));
|
|
201
|
+
if (!ok) {
|
|
202
|
+
console.log(chalk.gray('Aborted.'));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
console.log();
|
|
207
|
+
const results = await runFleetApply(plan.devices, nameToProfile, {
|
|
208
|
+
targetCliVersion,
|
|
209
|
+
source,
|
|
210
|
+
sourceAuth,
|
|
211
|
+
});
|
|
212
|
+
reportResults(results);
|
|
213
|
+
}
|
|
214
|
+
function reportResults(results) {
|
|
215
|
+
let failures = 0;
|
|
216
|
+
for (const r of results) {
|
|
217
|
+
if (r.note && r.steps.length === 0) {
|
|
218
|
+
console.log(` ${chalk.gray(r.device.padEnd(16))} ${chalk.yellow('skipped')} ${chalk.gray(r.note)}`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const badge = r.ok ? chalk.green('ok ') : chalk.red('fail');
|
|
222
|
+
if (!r.ok)
|
|
223
|
+
failures++;
|
|
224
|
+
const summary = r.steps.map((s) => `${s.ok ? '✓' : '✗'} ${s.kind}`).join(' ');
|
|
225
|
+
console.log(` ${chalk.bold(r.device.padEnd(16))} ${badge} ${chalk.gray(summary)}`);
|
|
226
|
+
for (const s of r.steps.filter((x) => x.kind === 'needs-login')) {
|
|
227
|
+
console.log(` ${chalk.yellow('→')} ${chalk.gray(s.detail)}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
console.log();
|
|
231
|
+
if (failures > 0) {
|
|
232
|
+
console.error(chalk.red(`${failures} device(s) had failures.`));
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
console.log(chalk.green('Fleet reconciled.'));
|
|
236
|
+
}
|
|
237
|
+
export function registerApplyCommand(program) {
|
|
238
|
+
const applyCmd = program
|
|
239
|
+
.command('apply')
|
|
240
|
+
.description('Reconcile the fleet to a declared profile: install agents, sync config, propagate login.')
|
|
241
|
+
.option('-f, --file <path>', 'Manifest file carrying a fleet: block (default: agents.yaml)')
|
|
242
|
+
.option('--plan', 'Show the reconcile plan and exit (no changes)')
|
|
243
|
+
.option('--dry-run', 'Alias for --plan')
|
|
244
|
+
.option('-y, --yes', 'Skip the confirmation prompt')
|
|
245
|
+
.option('--device <name>', 'Scope the apply to a single device')
|
|
246
|
+
.option('--only <dims>', 'Limit to dimensions: comma list of agents,config,login')
|
|
247
|
+
.option('--no-login', 'Do not propagate logins')
|
|
248
|
+
.addOption(new Option('--recv-auth', 'internal: receive an auth bundle on stdin').hideHelp())
|
|
249
|
+
.action(async (opts) => {
|
|
250
|
+
try {
|
|
251
|
+
if (opts.recvAuth) {
|
|
252
|
+
await runRecvAuth();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
await runApply(opts);
|
|
256
|
+
}
|
|
257
|
+
catch (e) {
|
|
258
|
+
console.error(chalk.red(e.message));
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
setHelpSections(applyCmd, {
|
|
263
|
+
examples: `
|
|
264
|
+
# Preview what would change across the fleet
|
|
265
|
+
agents apply --plan -f agents.yaml
|
|
266
|
+
|
|
267
|
+
# Bring every device to the profile (installs, syncs, propagates login)
|
|
268
|
+
ag apply -f agents.yaml
|
|
269
|
+
|
|
270
|
+
# One device only
|
|
271
|
+
agents apply --device yosemite-s1 -y
|
|
272
|
+
`,
|
|
273
|
+
});
|
|
274
|
+
}
|
package/dist/commands/browser.js
CHANGED
|
@@ -4,7 +4,7 @@ import { listProfiles, getProfile, createProfile, deleteProfile, ensureDefaultBr
|
|
|
4
4
|
import { updateMeta } from '../lib/state.js';
|
|
5
5
|
import { loginsForProfile, profilesLoggedInto, serviceForUrl, loginsWithAccountsForProfile, accountsForProfile, credKeysForService, AUTH_SIGNATURES, } from '../lib/browser/login-detection.js';
|
|
6
6
|
import { parseSecretRef } from '../lib/browser/secret-ref.js';
|
|
7
|
-
import { readAndResolveBundleEnv, bundleExists, readBundle, describeBundle } from '../lib/secrets/bundles.js';
|
|
7
|
+
import { readAndResolveBundleEnv, isHeadlessSecretsContext, bundleExists, readBundle, describeBundle } from '../lib/secrets/bundles.js';
|
|
8
8
|
import { findBrowserPath, getPortOccupant, isLauncherScript } from '../lib/browser/chrome.js';
|
|
9
9
|
import { listProfileCacheDirs, removeProfileCache, listAllProfileSnapshots, } from '../lib/browser/runtime-state.js';
|
|
10
10
|
import { DEFAULT_VIEWPORT } from '../lib/browser/devices.js';
|
|
@@ -1420,7 +1420,7 @@ function registerTaskCommands(browser) {
|
|
|
1420
1420
|
process.exit(1);
|
|
1421
1421
|
}
|
|
1422
1422
|
try {
|
|
1423
|
-
const { env } = readAndResolveBundleEnv(parsed.bundle, { caller: 'browser type', keys: [parsed.key] });
|
|
1423
|
+
const { env } = readAndResolveBundleEnv(parsed.bundle, { caller: 'browser type', keys: [parsed.key], agentOnly: isHeadlessSecretsContext() });
|
|
1424
1424
|
if (!(parsed.key in env)) {
|
|
1425
1425
|
console.error(`Key "${parsed.key}" not in bundle "${parsed.bundle}".`);
|
|
1426
1426
|
process.exit(1);
|
package/dist/commands/cloud.js
CHANGED
|
@@ -8,6 +8,7 @@ import { insertTask, updateTaskStatus, getTaskById, listTasks as listStoredTasks
|
|
|
8
8
|
import { renderStream } from '../lib/cloud/stream.js';
|
|
9
9
|
import { MissingTargetError, MAX_IMAGES_PER_DISPATCH } from '../lib/cloud/types.js';
|
|
10
10
|
import { normalizeTriggerEvent, validateTrigger, writeJob, jobExists, GITHUB_TRIGGER_EVENTS } from '../lib/routines.js';
|
|
11
|
+
import { machineId } from '../lib/machine-id.js';
|
|
11
12
|
import { emit } from '../lib/events.js';
|
|
12
13
|
/** Map a supported image file extension to its wire mimeType. Rejects anything else. */
|
|
13
14
|
function imageMimeFromPath(file) {
|
|
@@ -145,7 +146,7 @@ Examples:
|
|
|
145
146
|
cloud
|
|
146
147
|
.command('run [prompt]')
|
|
147
148
|
.description('Dispatch a task to a cloud agent.')
|
|
148
|
-
.option('--provider <id>', 'Cloud backend: rush, codex, factory, antigravity (overrides agent auto-routing)')
|
|
149
|
+
.option('--provider <id>', 'Cloud backend: rush, codex, factory, antigravity, host (overrides agent auto-routing)')
|
|
149
150
|
.option('--agent <name>', 'Agent to run: claude, codex, droid, antigravity (auto-routes to its native cloud)')
|
|
150
151
|
.option('--repo <owner/repo>', 'GitHub repository. Repeatable for multi-repo dispatch (Rush Cloud only).', (value, previous) => {
|
|
151
152
|
const acc = Array.isArray(previous) ? previous : [];
|
|
@@ -160,6 +161,9 @@ Examples:
|
|
|
160
161
|
.option('--model <model>', 'Model override')
|
|
161
162
|
.option('--env <id>', 'Codex Cloud environment ID')
|
|
162
163
|
.option('--computer <name>', 'Factory/Droid computer target')
|
|
164
|
+
.option('--host <name>', 'One of your machines as the target (a registered host, device, capability tag, or user@host). Implies --provider host.')
|
|
165
|
+
.option('--remote-cwd <dir>', 'Working directory on the host (--provider host only)')
|
|
166
|
+
.option('--any', 'With --host <cap> (a capability tag), pick any matching host instead of erroring when several match')
|
|
163
167
|
.option('--autonomy <level>', 'Factory/Droid autonomy: low, medium, high (default high)')
|
|
164
168
|
.option('--mode <mode>', 'Execution mode (e.g., plan, edit, full)')
|
|
165
169
|
.option('--image <path>', `Attach an image (.png/.jpg/.webp) for vision dispatch. Repeatable, up to ${MAX_IMAGES_PER_DISPATCH} (Rush Cloud only).`, (value, previous) => {
|
|
@@ -189,6 +193,10 @@ Examples:
|
|
|
189
193
|
# Codex Cloud
|
|
190
194
|
agents cloud run "add auth tests" --provider codex --env env_abc123
|
|
191
195
|
|
|
196
|
+
# One of your own machines (agents hosts / agents devices), over SSH
|
|
197
|
+
agents cloud run "run the nightly benchmark" --host gpu-box --agent claude
|
|
198
|
+
agents cloud run "rebuild the index" --host gpu --any --remote-cwd ~/proj
|
|
199
|
+
|
|
192
200
|
# Default provider (set in ~/.agents/agents.yaml)
|
|
193
201
|
agents cloud run "refactor auth module" --repo user/repo
|
|
194
202
|
`)
|
|
@@ -208,9 +216,16 @@ Examples:
|
|
|
208
216
|
process.stderr.write(chalk.dim(`Reading prompt from ${filePath} (${sizeKB} KB)\n`));
|
|
209
217
|
}
|
|
210
218
|
}
|
|
219
|
+
// --host names one of YOUR machines as the target — that only means
|
|
220
|
+
// something to the host provider, so it implies --provider host rather
|
|
221
|
+
// than silently riding along to a cloud backend that would ignore it.
|
|
222
|
+
if (options.host && options.provider && options.provider !== 'host') {
|
|
223
|
+
die(`--host targets your own machines (--provider host), not ${options.provider}. Drop --host, or use --provider host.`);
|
|
224
|
+
}
|
|
225
|
+
const explicitProvider = options.provider ?? (options.host ? 'host' : undefined);
|
|
211
226
|
// Agent-aware: with no --provider, the agent routes to its native cloud
|
|
212
227
|
// (claude→rush, codex→codex, droid→factory, antigravity→antigravity).
|
|
213
|
-
const provider = resolveProvider(
|
|
228
|
+
const provider = resolveProvider(explicitProvider, options.agent);
|
|
214
229
|
// --repo is repeatable: commander gives us an array via our collector.
|
|
215
230
|
// A single --repo value arrives as a one-element array; keep the legacy
|
|
216
231
|
// singular `repo` field in sync so providers that only know that field
|
|
@@ -234,6 +249,12 @@ Examples:
|
|
|
234
249
|
dispatchOptions.providerOptions.env = options.env;
|
|
235
250
|
if (options.computer)
|
|
236
251
|
dispatchOptions.providerOptions.computer = options.computer;
|
|
252
|
+
if (options.host)
|
|
253
|
+
dispatchOptions.providerOptions.host = options.host;
|
|
254
|
+
if (options.remoteCwd)
|
|
255
|
+
dispatchOptions.providerOptions.remoteCwd = options.remoteCwd;
|
|
256
|
+
if (options.any)
|
|
257
|
+
dispatchOptions.providerOptions.any = true;
|
|
237
258
|
if (options.autonomy)
|
|
238
259
|
dispatchOptions.providerOptions.autonomy = options.autonomy;
|
|
239
260
|
if (options.mode)
|
|
@@ -279,6 +300,15 @@ Examples:
|
|
|
279
300
|
};
|
|
280
301
|
if (repoValues[0])
|
|
281
302
|
routine.repo = repoValues[0];
|
|
303
|
+
// --host with --on: the webhook-fired run places on that machine (the
|
|
304
|
+
// routine carries the placement, and firing pins to THIS device so a
|
|
305
|
+
// fleet of receivers can't each dispatch a duplicate).
|
|
306
|
+
if (options.host) {
|
|
307
|
+
routine.host = options.host;
|
|
308
|
+
if (options.remoteCwd)
|
|
309
|
+
routine.remoteCwd = options.remoteCwd;
|
|
310
|
+
routine.devices = [machineId()];
|
|
311
|
+
}
|
|
282
312
|
writeJob(routine);
|
|
283
313
|
if (json) {
|
|
284
314
|
console.log(JSON.stringify({ ok: true, registered: routineName, trigger }, null, 2));
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { addHostOption } from '../lib/hosts/option.js';
|
|
2
2
|
import { buildRemoteAgentsInvocation } from '../lib/hosts/remote-cmd.js';
|
|
3
|
-
import { loadDevices } from '../lib/devices/registry.js';
|
|
3
|
+
import { loadDevices, isControlDevice } from '../lib/devices/registry.js';
|
|
4
4
|
import { resolveHost } from '../lib/hosts/registry.js';
|
|
5
5
|
import { sshExec } from '../lib/ssh-exec.js';
|
|
6
6
|
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
@@ -249,6 +249,9 @@ async function resolveFleetTargets(opts) {
|
|
|
249
249
|
const localName = machineId();
|
|
250
250
|
return Object.values(registry)
|
|
251
251
|
.filter((d) => d.name.toLowerCase() !== localName)
|
|
252
|
+
// Control devices (a cockpit) never run agents — skip them in the fleet
|
|
253
|
+
// fan-out (an explicit --device <name> still resolves above).
|
|
254
|
+
.filter((d) => !isControlDevice(d))
|
|
252
255
|
.map((d) => ({
|
|
253
256
|
name: d.name,
|
|
254
257
|
sshTarget: d.name,
|
package/dist/commands/exec.d.ts
CHANGED
|
@@ -7,6 +7,54 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { type Command } from 'commander';
|
|
9
9
|
import type { ExecEffort } from '../lib/exec.js';
|
|
10
|
+
import { type SshGResult } from '../lib/hosts/ssh-config.js';
|
|
11
|
+
/** The host descriptor fields the `--copy-creds` security gate reads. */
|
|
12
|
+
export interface CopyCredsGateHost {
|
|
13
|
+
name: string;
|
|
14
|
+
/** Concrete SSH target for inline/provider hosts; unset for ssh-config aliases. */
|
|
15
|
+
address?: string;
|
|
16
|
+
/** `devices` = registered Tailscale fleet host; `local` = ssh-config/inline. */
|
|
17
|
+
provider?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Outcome of the `--copy-creds` security gate (RUSH-1767). */
|
|
20
|
+
export interface CopyCredsGateDecision {
|
|
21
|
+
/** Ship credentials? True only when the host key is pinned in the managed store. */
|
|
22
|
+
allowed: boolean;
|
|
23
|
+
/** The address whose key is checked/pinned — an ssh-config alias resolved to its real HostName. */
|
|
24
|
+
pinTarget: string;
|
|
25
|
+
/** True when the non-device self-pin path pinned the target during this call. */
|
|
26
|
+
selfPinned: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** Injectable network seams so the gate decision is testable without ssh/keyscan. */
|
|
29
|
+
export interface CopyCredsGateDeps {
|
|
30
|
+
/** Managed known_hosts store file (defaults to the real cache path). */
|
|
31
|
+
file?: string;
|
|
32
|
+
/** Resolve an ssh-config alias to its effective HostName/Port (defaults to real `ssh -G`). */
|
|
33
|
+
resolve?: (name: string) => SshGResult | undefined;
|
|
34
|
+
/** Self-pin `target` via ssh-keyscan; returns whether it's pinned afterward (defaults to real `pinHostKey`). */
|
|
35
|
+
selfPin?: (target: string, port: number | undefined, file: string) => boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Decide whether `--copy-creds` may ship credentials (and the Claude OAuth
|
|
39
|
+
* token) to `host` (RUSH-1767). Credentials ship only to a host whose SSH host
|
|
40
|
+
* key is pinned in the managed known_hosts store, so the offload never rides an
|
|
41
|
+
* accept-new (TOFU) connection a machine-in-the-middle could intercept.
|
|
42
|
+
*
|
|
43
|
+
* Resolution:
|
|
44
|
+
* - The checked target is the host's concrete `address`, else its ssh-config
|
|
45
|
+
* HostName (`ssh -G`), else its name — so an alias is pinned/verified against
|
|
46
|
+
* the SAME real host the strict dispatch later connects to.
|
|
47
|
+
* - An unpinned NON-device (a bare `~/.ssh/config` `Host` alias or literal,
|
|
48
|
+
* which `agents ssh <name>` can't reach — "Unknown device") is self-pinned in
|
|
49
|
+
* place via ssh-keyscan. A registered device is left unpinned here — it earns
|
|
50
|
+
* its pin through the normal `agents ssh <name>` accept-new connect, so the
|
|
51
|
+
* caller steers there instead.
|
|
52
|
+
*
|
|
53
|
+
* The network seams (`resolve`, `selfPin`) default to the real ssh -G /
|
|
54
|
+
* ssh-keyscan implementations and are injectable so the ship/refuse decision is
|
|
55
|
+
* unit-testable against real known_hosts fixtures with no network.
|
|
56
|
+
*/
|
|
57
|
+
export declare function decideCopyCredsGate(host: CopyCredsGateHost, deps?: CopyCredsGateDeps): CopyCredsGateDecision;
|
|
10
58
|
/**
|
|
11
59
|
* Build the LoopConfig the driver consumes from CLI flags and/or a workflow's
|
|
12
60
|
* `loop:` frontmatter block (issue #332). Returns undefined when neither source
|