@phnx-labs/agents-cli 1.20.63 → 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 +56 -3
- package/README.md +46 -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 +149 -77
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +52 -19
- package/dist/commands/hosts.js +44 -6
- package/dist/commands/lease.d.ts +23 -0
- package/dist/commands/lease.js +201 -0
- package/dist/commands/mailboxes.d.ts +20 -0
- package/dist/commands/mailboxes.js +390 -0
- 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 +43 -16
- 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.d.ts +2 -0
- package/dist/commands/sessions-export.js +284 -0
- package/dist/commands/sessions-import.d.ts +2 -0
- package/dist/commands/sessions-import.js +230 -0
- package/dist/commands/sessions.d.ts +16 -0
- package/dist/commands/sessions.js +40 -6
- package/dist/commands/ssh.js +143 -5
- package/dist/commands/usage.d.ts +2 -0
- package/dist/commands/usage.js +7 -2
- package/dist/commands/versions.js +7 -3
- package/dist/commands/view.d.ts +27 -1
- 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 +36 -0
- package/dist/lib/agents.js +80 -20
- 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/browser/drivers/ssh.js +19 -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/comms-render.d.ts +37 -0
- package/dist/lib/comms-render.js +89 -0
- package/dist/lib/computer-rpc.js +3 -1
- package/dist/lib/crabbox/cli.d.ts +72 -0
- package/dist/lib/crabbox/cli.js +162 -9
- package/dist/lib/crabbox/runtimes.d.ts +13 -0
- package/dist/lib/crabbox/runtimes.js +35 -2
- package/dist/lib/daemon.d.ts +20 -4
- package/dist/lib/daemon.js +68 -20
- package/dist/lib/devices/fleet.d.ts +3 -2
- package/dist/lib/devices/fleet.js +9 -0
- package/dist/lib/devices/health.d.ts +77 -0
- package/dist/lib/devices/health.js +186 -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/mailbox.d.ts +39 -0
- package/dist/lib/mailbox.js +112 -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/paths.d.ts +13 -0
- package/dist/lib/paths.js +26 -4
- 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 +37 -2
- package/dist/lib/routines.js +54 -12
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +348 -13
- package/dist/lib/sandbox.d.ts +9 -1
- package/dist/lib/sandbox.js +11 -2
- 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 +164 -0
- package/dist/lib/session/bundle.js +200 -0
- package/dist/lib/session/remote-bundle.d.ts +12 -0
- package/dist/lib/session/remote-bundle.js +61 -0
- 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/agents.d.ts +54 -6
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/sync/config.js +8 -2
- package/dist/lib/session/sync/manifest.d.ts +14 -3
- package/dist/lib/session/sync/manifest.js +4 -0
- package/dist/lib/session/sync/sync.d.ts +23 -2
- package/dist/lib/session/sync/sync.js +177 -74
- package/dist/lib/session/types.d.ts +30 -0
- package/dist/lib/ssh-tunnel.d.ts +19 -1
- package/dist/lib/ssh-tunnel.js +99 -8
- package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
- package/dist/lib/staleness/detectors/subagents.js +5 -192
- package/dist/lib/staleness/writers/subagents.d.ts +10 -0
- package/dist/lib/staleness/writers/subagents.js +11 -102
- package/dist/lib/startup/command-registry.d.ts +4 -0
- package/dist/lib/startup/command-registry.js +9 -0
- package/dist/lib/state.d.ts +5 -0
- package/dist/lib/state.js +12 -0
- package/dist/lib/subagents-registry.d.ts +85 -0
- package/dist/lib/subagents-registry.js +393 -0
- package/dist/lib/subagents.d.ts +8 -8
- package/dist/lib/subagents.js +32 -663
- package/dist/lib/sync-umbrella.d.ts +1 -0
- package/dist/lib/sync-umbrella.js +14 -3
- 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 +35 -1
- package/dist/lib/usage.d.ts +42 -3
- package/dist/lib/usage.js +163 -21
- 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,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse + validate the `fleet:` block of a profile manifest and resolve it into
|
|
3
|
+
* per-device desired state.
|
|
4
|
+
*
|
|
5
|
+
* `fleet:` is additive to the `Meta` schema (`types.ts`). Any `-f <file>` that
|
|
6
|
+
* carries a `fleet:` block is a valid manifest, so `ag apply -f agents.yaml`
|
|
7
|
+
* works against the project file. The functions here are pure (no SSH, no
|
|
8
|
+
* registry I/O) so they're fully unit-testable; device enumeration is injected.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as yaml from 'yaml';
|
|
12
|
+
const LOGIN_MODES = ['sync', 'skip'];
|
|
13
|
+
function isStringArray(v) {
|
|
14
|
+
return Array.isArray(v) && v.every((x) => typeof x === 'string');
|
|
15
|
+
}
|
|
16
|
+
function validateLogin(v, where) {
|
|
17
|
+
if (v === undefined)
|
|
18
|
+
return undefined;
|
|
19
|
+
if (typeof v !== 'string' || !LOGIN_MODES.includes(v)) {
|
|
20
|
+
throw new Error(`fleet: ${where}.login must be one of ${LOGIN_MODES.join(' | ')} (got ${JSON.stringify(v)}).`);
|
|
21
|
+
}
|
|
22
|
+
return v;
|
|
23
|
+
}
|
|
24
|
+
function validateDefaults(v, where) {
|
|
25
|
+
if (v === undefined)
|
|
26
|
+
return {};
|
|
27
|
+
if (typeof v !== 'object' || v === null || Array.isArray(v)) {
|
|
28
|
+
throw new Error(`fleet: ${where} must be a mapping.`);
|
|
29
|
+
}
|
|
30
|
+
const o = v;
|
|
31
|
+
if (o.agents !== undefined && !isStringArray(o.agents)) {
|
|
32
|
+
throw new Error(`fleet: ${where}.agents must be a list of agent specs (e.g. [claude@latest]).`);
|
|
33
|
+
}
|
|
34
|
+
if (o.sync !== undefined && !isStringArray(o.sync)) {
|
|
35
|
+
throw new Error(`fleet: ${where}.sync must be a list of scope names (e.g. [user]).`);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
agents: o.agents,
|
|
39
|
+
sync: o.sync,
|
|
40
|
+
login: validateLogin(o.login, where),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Validate a raw `fleet:` object (already YAML-parsed) into a typed manifest. */
|
|
44
|
+
export function parseFleetManifest(raw) {
|
|
45
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
46
|
+
throw new Error('fleet: block must be a mapping with a `devices:` key.');
|
|
47
|
+
}
|
|
48
|
+
const o = raw;
|
|
49
|
+
const defaults = validateDefaults(o.defaults, 'defaults');
|
|
50
|
+
if (o.devices === undefined) {
|
|
51
|
+
throw new Error('fleet: a `devices:` key is required (use `devices: all` or an explicit map).');
|
|
52
|
+
}
|
|
53
|
+
let devices;
|
|
54
|
+
if (o.devices === 'all') {
|
|
55
|
+
devices = 'all';
|
|
56
|
+
}
|
|
57
|
+
else if (typeof o.devices === 'object' && o.devices !== null && !Array.isArray(o.devices)) {
|
|
58
|
+
const map = {};
|
|
59
|
+
for (const [name, ov] of Object.entries(o.devices)) {
|
|
60
|
+
// An empty entry (`device: {}`) inherits defaults — represent as {}.
|
|
61
|
+
const merged = ov == null ? {} : validateDefaults(ov, `devices.${name}`);
|
|
62
|
+
map[name] = merged;
|
|
63
|
+
}
|
|
64
|
+
devices = map;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
throw new Error(`fleet: devices must be the string 'all' or a mapping of device -> overrides (got ${JSON.stringify(o.devices)}).`);
|
|
68
|
+
}
|
|
69
|
+
return { defaults, devices };
|
|
70
|
+
}
|
|
71
|
+
/** Read a YAML file and extract + validate its `fleet:` block. */
|
|
72
|
+
export function readFleetFile(filePath) {
|
|
73
|
+
if (!fs.existsSync(filePath)) {
|
|
74
|
+
throw new Error(`Manifest not found: ${filePath}`);
|
|
75
|
+
}
|
|
76
|
+
let doc;
|
|
77
|
+
try {
|
|
78
|
+
doc = yaml.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
throw new Error(`Failed to parse YAML in ${filePath}: ${e.message}`);
|
|
82
|
+
}
|
|
83
|
+
if (typeof doc !== 'object' || doc === null || Array.isArray(doc)) {
|
|
84
|
+
throw new Error(`${filePath} is not a mapping — no fleet: block to apply.`);
|
|
85
|
+
}
|
|
86
|
+
const fleet = doc.fleet;
|
|
87
|
+
if (fleet === undefined) {
|
|
88
|
+
throw new Error(`${filePath} has no fleet: block. Add one to declare the profile (see \`agents apply --help\`).`);
|
|
89
|
+
}
|
|
90
|
+
return parseFleetManifest(fleet);
|
|
91
|
+
}
|
|
92
|
+
/** Merge a per-device override over defaults into a concrete desired state. */
|
|
93
|
+
function mergeDesired(device, defaults, override) {
|
|
94
|
+
return {
|
|
95
|
+
device,
|
|
96
|
+
agents: override.agents ?? defaults.agents ?? [],
|
|
97
|
+
sync: override.sync ?? defaults.sync ?? [],
|
|
98
|
+
login: override.login ?? defaults.login ?? 'sync',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Expand a manifest into concrete per-device desired states. `devices: all`
|
|
103
|
+
* expands to every online registered device minus the source; an explicit map
|
|
104
|
+
* validates each name against the registry. Devices with no desired agents,
|
|
105
|
+
* sync scopes, and `login: skip` are still returned (probe/report only).
|
|
106
|
+
*/
|
|
107
|
+
export function resolveDesired(manifest, ctx) {
|
|
108
|
+
const defaults = manifest.defaults ?? {};
|
|
109
|
+
const out = [];
|
|
110
|
+
if (manifest.devices === 'all') {
|
|
111
|
+
for (const name of ctx.onlineDevices) {
|
|
112
|
+
if (name === ctx.source)
|
|
113
|
+
continue;
|
|
114
|
+
out.push(mergeDesired(name, defaults, {}));
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
for (const [name, override] of Object.entries(manifest.devices)) {
|
|
119
|
+
if (name === ctx.source)
|
|
120
|
+
continue;
|
|
121
|
+
if (!ctx.registeredDevices.includes(name)) {
|
|
122
|
+
throw new Error(`fleet: device '${name}' is not a registered device. Run \`agents devices add ${name}\` or fix the manifest.`);
|
|
123
|
+
}
|
|
124
|
+
out.push(mergeDesired(name, defaults, override));
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the fleet profile-sync feature (`agents apply` / `ag apply`).
|
|
3
|
+
*
|
|
4
|
+
* `agents.yaml` gains an additive `fleet:` block that declares a *profile*: which
|
|
5
|
+
* agents every device should have installed, which config scopes to reconcile,
|
|
6
|
+
* and whether logins/tokens propagate. `apply` reconciles the live fleet to it.
|
|
7
|
+
*
|
|
8
|
+
* These types are the contract shared by the manifest parser (`manifest.ts`),
|
|
9
|
+
* the reconcile engine (`apply.ts`), the auth propagation (`auth-sync.ts`), and
|
|
10
|
+
* the command (`commands/apply.ts`). Runtime probe/diff shapes live here too so
|
|
11
|
+
* the pure diff can be unit-tested without SSH.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* How login/token state propagates to a device.
|
|
15
|
+
* - `sync` (default): push portable credentials where possible, surface the rest
|
|
16
|
+
* as a manual login.
|
|
17
|
+
* - `skip`: probe/report only; take no login action.
|
|
18
|
+
* (A per-agent interactive `prompt` mode is intentionally not offered yet — it
|
|
19
|
+
* was removed rather than accepted as a silent no-op that behaves like `skip`.)
|
|
20
|
+
*/
|
|
21
|
+
export type FleetLoginMode = 'sync' | 'skip';
|
|
22
|
+
/** Defaults applied to every targeted device unless a per-device entry overrides. */
|
|
23
|
+
export interface FleetDefaults {
|
|
24
|
+
/** Agent specs to ensure installed, e.g. `['claude@latest', 'codex@latest']`. */
|
|
25
|
+
agents?: string[];
|
|
26
|
+
/** Config sync scopes to reconcile on each device, e.g. `['user']`. */
|
|
27
|
+
sync?: string[];
|
|
28
|
+
/** Login propagation strategy. Default `'sync'`. */
|
|
29
|
+
login?: FleetLoginMode;
|
|
30
|
+
}
|
|
31
|
+
/** Per-device override; any omitted field inherits from `defaults`. */
|
|
32
|
+
export interface FleetDeviceOverride {
|
|
33
|
+
agents?: string[];
|
|
34
|
+
sync?: string[];
|
|
35
|
+
login?: FleetLoginMode;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The `fleet:` block as it appears in `agents.yaml` (or any `-f` file). `devices`
|
|
39
|
+
* is either the literal string `'all'` (every online registered device minus the
|
|
40
|
+
* source machine) or an explicit map of device-name -> override.
|
|
41
|
+
*/
|
|
42
|
+
export interface FleetManifest {
|
|
43
|
+
defaults?: FleetDefaults;
|
|
44
|
+
devices: 'all' | Record<string, FleetDeviceOverride>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A device's desired state after merging defaults with its override and
|
|
48
|
+
* expanding `devices: all`. This is what the reconcile engine drives toward.
|
|
49
|
+
*/
|
|
50
|
+
export interface DeviceDesired {
|
|
51
|
+
/** Registered device name (from `agents devices`). */
|
|
52
|
+
device: string;
|
|
53
|
+
/** Resolved agent specs to ensure installed. */
|
|
54
|
+
agents: string[];
|
|
55
|
+
/** Config sync scopes. */
|
|
56
|
+
sync: string[];
|
|
57
|
+
/** Login propagation strategy for this device. */
|
|
58
|
+
login: FleetLoginMode;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* What a probe found on one device. Populated from `readyProbe` plus an
|
|
62
|
+
* installed-agents listing; `reachable: false` short-circuits everything else.
|
|
63
|
+
*/
|
|
64
|
+
export interface DeviceProbe {
|
|
65
|
+
device: string;
|
|
66
|
+
reachable: boolean;
|
|
67
|
+
/** Platform of the device (`linux` | `macos` | `windows`), for login classification. */
|
|
68
|
+
platform?: string;
|
|
69
|
+
/** agents-cli version present on the device (undefined if not installed). */
|
|
70
|
+
cliVersion?: string;
|
|
71
|
+
/** Agent ids currently installed on the device. */
|
|
72
|
+
installedAgents: string[];
|
|
73
|
+
/** Reason string when `reachable` is false or the probe partially failed. */
|
|
74
|
+
note?: string;
|
|
75
|
+
}
|
|
76
|
+
/** One planned action against a device, in a single reconcile dimension. */
|
|
77
|
+
export type FleetActionKind = 'install-cli' | 'upgrade-cli' | 'add-agent' | 'sync-config' | 'push-login' | 'needs-login';
|
|
78
|
+
export interface FleetAction {
|
|
79
|
+
device: string;
|
|
80
|
+
kind: FleetActionKind;
|
|
81
|
+
/** Agent id for agent/login actions; undefined for cli/config actions. */
|
|
82
|
+
agent?: string;
|
|
83
|
+
/** Human, one-line description of the action. */
|
|
84
|
+
detail: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The full reconcile plan: per-device desired vs probed, plus the flat list of
|
|
88
|
+
* actions. Pure output of `diffFleet(desired, probes)` — drives both `--plan`
|
|
89
|
+
* rendering and the confirm prompt.
|
|
90
|
+
*/
|
|
91
|
+
export interface FleetPlan {
|
|
92
|
+
devices: DeviceDiff[];
|
|
93
|
+
actions: FleetAction[];
|
|
94
|
+
}
|
|
95
|
+
/** Per-device diff row rendered in the plan matrix. */
|
|
96
|
+
export interface DeviceDiff {
|
|
97
|
+
device: string;
|
|
98
|
+
desired: DeviceDesired;
|
|
99
|
+
probe: DeviceProbe;
|
|
100
|
+
actions: FleetAction[];
|
|
101
|
+
/** Agents that must be logged in on the device but can't be propagated
|
|
102
|
+
* (source token is device-bound, e.g. macOS keychain). Surfaced, not faked. */
|
|
103
|
+
loginBlocked: string[];
|
|
104
|
+
}
|
|
105
|
+
/** A portable auth file captured from a source agent home, ready to propagate. */
|
|
106
|
+
export interface AuthFilePayload {
|
|
107
|
+
/** Agent id this file belongs to. */
|
|
108
|
+
agent: string;
|
|
109
|
+
/** Path relative to the agent's config dir (or $HOME), reconstructed on target. */
|
|
110
|
+
rel: string;
|
|
111
|
+
/** File contents, base64. */
|
|
112
|
+
contentB64: string;
|
|
113
|
+
/** POSIX mode to restore (e.g. 0o600 for credentials). */
|
|
114
|
+
mode: number;
|
|
115
|
+
}
|
|
116
|
+
/** The plaintext we encrypt before shipping auth over the wire. */
|
|
117
|
+
export interface AuthBundle {
|
|
118
|
+
/** Schema version for forward-compat. */
|
|
119
|
+
v: 1;
|
|
120
|
+
/** Source machine name the snapshot was taken on. */
|
|
121
|
+
source: string;
|
|
122
|
+
files: AuthFilePayload[];
|
|
123
|
+
}
|
|
124
|
+
/** Result of classifying one source agent's auth for propagation. */
|
|
125
|
+
export interface AuthSnapshotResult {
|
|
126
|
+
files: AuthFilePayload[];
|
|
127
|
+
/** Agent ids whose auth is device-bound (keychain) and cannot be captured. */
|
|
128
|
+
bound: string[];
|
|
129
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the fleet profile-sync feature (`agents apply` / `ag apply`).
|
|
3
|
+
*
|
|
4
|
+
* `agents.yaml` gains an additive `fleet:` block that declares a *profile*: which
|
|
5
|
+
* agents every device should have installed, which config scopes to reconcile,
|
|
6
|
+
* and whether logins/tokens propagate. `apply` reconciles the live fleet to it.
|
|
7
|
+
*
|
|
8
|
+
* These types are the contract shared by the manifest parser (`manifest.ts`),
|
|
9
|
+
* the reconcile engine (`apply.ts`), the auth propagation (`auth-sync.ts`), and
|
|
10
|
+
* the command (`commands/apply.ts`). Runtime probe/diff shapes live here too so
|
|
11
|
+
* the pure diff can be unit-tested without SSH.
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
package/dist/lib/git.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git operations for the agents-cli system repo and package repositories.
|
|
3
|
+
*
|
|
4
|
+
* Handles cloning, pulling, syncing, and inspecting git repos used by
|
|
5
|
+
* the agents version management and plugin/package system. Includes
|
|
6
|
+
* source parsing for GitHub shorthand, SSH, HTTPS, and local paths.
|
|
7
|
+
*/
|
|
8
|
+
import { SimpleGit } from 'simple-git';
|
|
1
9
|
/**
|
|
2
10
|
* Validate that a clone/pull source uses a safe git transport before it is
|
|
3
11
|
* handed to `git`.
|
|
@@ -16,6 +24,25 @@
|
|
|
16
24
|
* @throws Error if the source uses a disallowed transport.
|
|
17
25
|
*/
|
|
18
26
|
export declare function assertSafeGitTransport(source: string): void;
|
|
27
|
+
/**
|
|
28
|
+
* Validate a branch name before it is passed to `git push` / `git pull`.
|
|
29
|
+
*
|
|
30
|
+
* A name beginning with `-` is parsed by git as a command-line option
|
|
31
|
+
* (e.g. `--mirror`, `--receive-pack=…`), not a ref. Pure string check —
|
|
32
|
+
* identical on every OS, no spawn.
|
|
33
|
+
*
|
|
34
|
+
* @throws Error if the name is empty or would be interpreted as a git option.
|
|
35
|
+
*/
|
|
36
|
+
export declare function assertValidBranchName(branch: string): void;
|
|
37
|
+
/**
|
|
38
|
+
* `git push origin <branch>` with option-injection hardening:
|
|
39
|
+
* 1. {@link assertValidBranchName} rejects leading `-`
|
|
40
|
+
* 2. `--` ends option parsing so a hostile ref cannot be read as a flag
|
|
41
|
+
*
|
|
42
|
+
* Prefer this over `git.push(remote, branch)` whenever the branch comes from
|
|
43
|
+
* repo state rather than a hard-coded literal.
|
|
44
|
+
*/
|
|
45
|
+
export declare function pushOrigin(git: SimpleGit, branch: string): Promise<void>;
|
|
19
46
|
/** Parsed representation of a git source string (GitHub, generic URL, or local path). */
|
|
20
47
|
export interface GitSource {
|
|
21
48
|
type: 'github' | 'url' | 'local';
|
package/dist/lib/git.js
CHANGED
|
@@ -51,6 +51,36 @@ export function assertSafeGitTransport(source) {
|
|
|
51
51
|
}
|
|
52
52
|
// No scheme -> SCP-style SSH ("git@host:path") or a local path; both safe.
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Validate a branch name before it is passed to `git push` / `git pull`.
|
|
56
|
+
*
|
|
57
|
+
* A name beginning with `-` is parsed by git as a command-line option
|
|
58
|
+
* (e.g. `--mirror`, `--receive-pack=…`), not a ref. Pure string check —
|
|
59
|
+
* identical on every OS, no spawn.
|
|
60
|
+
*
|
|
61
|
+
* @throws Error if the name is empty or would be interpreted as a git option.
|
|
62
|
+
*/
|
|
63
|
+
export function assertValidBranchName(branch) {
|
|
64
|
+
const b = branch.trim();
|
|
65
|
+
if (!b) {
|
|
66
|
+
throw new Error(`Invalid branch name ${JSON.stringify(branch)}: branch name is empty.`);
|
|
67
|
+
}
|
|
68
|
+
if (b.startsWith('-')) {
|
|
69
|
+
throw new Error(`Invalid branch name ${JSON.stringify(branch)}: a name starting with "-" is interpreted as a git option.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* `git push origin <branch>` with option-injection hardening:
|
|
74
|
+
* 1. {@link assertValidBranchName} rejects leading `-`
|
|
75
|
+
* 2. `--` ends option parsing so a hostile ref cannot be read as a flag
|
|
76
|
+
*
|
|
77
|
+
* Prefer this over `git.push(remote, branch)` whenever the branch comes from
|
|
78
|
+
* repo state rather than a hard-coded literal.
|
|
79
|
+
*/
|
|
80
|
+
export async function pushOrigin(git, branch) {
|
|
81
|
+
assertValidBranchName(branch);
|
|
82
|
+
await git.raw(['push', '--', 'origin', branch]);
|
|
83
|
+
}
|
|
54
84
|
/**
|
|
55
85
|
* Whether installing a cloned/pulled repo's `.githooks/` is enabled.
|
|
56
86
|
*
|
|
@@ -377,6 +407,7 @@ export async function commitAndPush(repoPath, message) {
|
|
|
377
407
|
const git = simpleGit(repoPath);
|
|
378
408
|
let status = await git.status();
|
|
379
409
|
const branch = status.current || 'main';
|
|
410
|
+
assertValidBranchName(branch);
|
|
380
411
|
let committed = false;
|
|
381
412
|
if (status.files.length > 0) {
|
|
382
413
|
await git.add('-A');
|
|
@@ -402,7 +433,7 @@ export async function commitAndPush(repoPath, message) {
|
|
|
402
433
|
catch {
|
|
403
434
|
/* origin/<branch> may not exist yet (first push) */
|
|
404
435
|
}
|
|
405
|
-
await git
|
|
436
|
+
await pushOrigin(git, branch);
|
|
406
437
|
let after = '';
|
|
407
438
|
try {
|
|
408
439
|
after = (await git.raw(['rev-parse', '--short=8', 'HEAD'])).trim();
|
|
@@ -735,12 +766,13 @@ export async function syncRepoGit(dir, opts) {
|
|
|
735
766
|
};
|
|
736
767
|
}
|
|
737
768
|
const branch = status.current || 'main';
|
|
769
|
+
assertValidBranchName(branch);
|
|
738
770
|
await git.fetch('origin');
|
|
739
771
|
await git.pull('origin', branch, { '--rebase': 'true' });
|
|
740
772
|
installGithooksSymlinks(dir);
|
|
741
773
|
let pushed = false;
|
|
742
774
|
if (opts.push) {
|
|
743
|
-
await git
|
|
775
|
+
await pushOrigin(git, branch);
|
|
744
776
|
pushed = true;
|
|
745
777
|
}
|
|
746
778
|
const log = await git.log({ maxCount: 1 });
|
|
@@ -62,24 +62,37 @@ export interface DispatchOptions {
|
|
|
62
62
|
prompt: string;
|
|
63
63
|
/** Explicit agent version pin (e.g. "2.1.207") to forward as `agent@version`. */
|
|
64
64
|
version?: string;
|
|
65
|
-
/**
|
|
65
|
+
/** Run strategy (e.g. "balanced") — the remote picks among ITS signed-in accounts. */
|
|
66
66
|
strategy?: string;
|
|
67
|
+
balanced?: boolean;
|
|
68
|
+
fallback?: string;
|
|
67
69
|
mode?: string;
|
|
68
70
|
model?: string;
|
|
69
|
-
/** Reasoning effort
|
|
71
|
+
/** Reasoning effort — forwarded unless 'auto' (the remote default). */
|
|
70
72
|
effort?: string;
|
|
71
|
-
/**
|
|
73
|
+
/** `--env k=v` pairs, forwarded verbatim (the remote CLI parses them). */
|
|
74
|
+
env?: string[];
|
|
75
|
+
/** `--add-dir` grants, already made remote-portable. */
|
|
72
76
|
addDir?: string[];
|
|
73
|
-
/**
|
|
77
|
+
/** Agent wall-clock cap, forwarded as `--timeout <duration>` for the REMOTE to enforce. */
|
|
78
|
+
timeout?: string;
|
|
79
|
+
/** Loop family — the loop driver runs on the host. */
|
|
80
|
+
loop?: boolean;
|
|
81
|
+
maxIterations?: string;
|
|
82
|
+
budget?: string;
|
|
83
|
+
until?: string;
|
|
84
|
+
interval?: string;
|
|
85
|
+
/** Remote emits ndjson into its log; the local follow streams it verbatim. */
|
|
74
86
|
json?: boolean;
|
|
75
|
-
/** Show detailed execution logs. */
|
|
76
87
|
verbose?: boolean;
|
|
77
|
-
/**
|
|
78
|
-
timeout?: string;
|
|
79
|
-
/** Skip the interactive budget-confirm prompt. */
|
|
88
|
+
/** Skip the budget-confirm prompt — a detached remote run can't answer one. */
|
|
80
89
|
yes?: boolean;
|
|
81
90
|
/** Route through the Agent Client Protocol. */
|
|
82
91
|
acp?: boolean;
|
|
92
|
+
/** False forwards --no-auto-secrets (workflow secrets resolve on the REMOTE keychain). */
|
|
93
|
+
autoSecrets?: boolean;
|
|
94
|
+
/** Native-CLI passthrough (everything after `--`), appended last. */
|
|
95
|
+
passthroughArgs?: string[];
|
|
83
96
|
remoteCwd?: string;
|
|
84
97
|
/**
|
|
85
98
|
* Force the remote run's NEW session to use this exact id (Claude only, via
|
|
@@ -105,6 +118,10 @@ export interface DispatchOptions {
|
|
|
105
118
|
* session-id / resume flag wiring is unit-testable without an SSH round-trip.
|
|
106
119
|
* `--session-id` and `--resume` are mutually exclusive (the CLI rejects both);
|
|
107
120
|
* resume wins when — defensively — both are set.
|
|
121
|
+
*
|
|
122
|
+
* Every field here is classified 'forward' in RUN_OPTION_FORWARDING
|
|
123
|
+
* (remote-cmd.ts) — keep the two in lockstep; run-forwarding.test.ts asserts
|
|
124
|
+
* the table side.
|
|
108
125
|
*/
|
|
109
126
|
export declare function buildRunForwardedArgs(opts: DispatchOptions): string[];
|
|
110
127
|
export interface InteractiveDispatchOptions {
|
|
@@ -139,6 +156,10 @@ export interface InteractiveDispatchOptions {
|
|
|
139
156
|
raw?: boolean;
|
|
140
157
|
/** Forward `--interactive` to the remote so a prompt-bearing run still starts the TUI. */
|
|
141
158
|
forceInteractive?: boolean;
|
|
159
|
+
/** `--env k=v` pairs, forwarded verbatim (the remote CLI parses them). */
|
|
160
|
+
env?: string[];
|
|
161
|
+
balanced?: boolean;
|
|
162
|
+
fallback?: string;
|
|
142
163
|
/** Copy runtime credentials to the host before the run and shred them after. */
|
|
143
164
|
copyCreds?: HostCredentials;
|
|
144
165
|
}
|
|
@@ -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
|
+
}
|