@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.
Files changed (197) hide show
  1. package/CHANGELOG.md +56 -3
  2. package/README.md +46 -2
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/apply.d.ts +12 -0
  5. package/dist/commands/apply.js +274 -0
  6. package/dist/commands/browser.js +2 -2
  7. package/dist/commands/cloud.js +32 -2
  8. package/dist/commands/doctor.js +4 -1
  9. package/dist/commands/exec.js +149 -77
  10. package/dist/commands/feed.d.ts +4 -0
  11. package/dist/commands/feed.js +52 -19
  12. package/dist/commands/hosts.js +44 -6
  13. package/dist/commands/lease.d.ts +23 -0
  14. package/dist/commands/lease.js +201 -0
  15. package/dist/commands/mailboxes.d.ts +20 -0
  16. package/dist/commands/mailboxes.js +390 -0
  17. package/dist/commands/mcp.js +55 -5
  18. package/dist/commands/monitors.d.ts +12 -0
  19. package/dist/commands/monitors.js +740 -0
  20. package/dist/commands/output.js +2 -2
  21. package/dist/commands/routines.js +43 -16
  22. package/dist/commands/secrets.d.ts +16 -0
  23. package/dist/commands/secrets.js +215 -64
  24. package/dist/commands/serve.js +31 -0
  25. package/dist/commands/sessions-export.d.ts +2 -0
  26. package/dist/commands/sessions-export.js +284 -0
  27. package/dist/commands/sessions-import.d.ts +2 -0
  28. package/dist/commands/sessions-import.js +230 -0
  29. package/dist/commands/sessions.d.ts +16 -0
  30. package/dist/commands/sessions.js +40 -6
  31. package/dist/commands/ssh.js +143 -5
  32. package/dist/commands/usage.d.ts +2 -0
  33. package/dist/commands/usage.js +7 -2
  34. package/dist/commands/versions.js +7 -3
  35. package/dist/commands/view.d.ts +27 -1
  36. package/dist/commands/view.js +32 -9
  37. package/dist/commands/webhook.js +10 -2
  38. package/dist/index.js +35 -14
  39. package/dist/lib/agents.d.ts +36 -0
  40. package/dist/lib/agents.js +80 -20
  41. package/dist/lib/auto-dispatch-provider.js +7 -2
  42. package/dist/lib/auto-dispatch.d.ts +3 -0
  43. package/dist/lib/auto-dispatch.js +3 -0
  44. package/dist/lib/browser/chrome.js +2 -2
  45. package/dist/lib/browser/drivers/ssh.js +19 -2
  46. package/dist/lib/cloud/antigravity.js +2 -2
  47. package/dist/lib/cloud/host.d.ts +59 -0
  48. package/dist/lib/cloud/host.js +224 -0
  49. package/dist/lib/cloud/registry.js +4 -0
  50. package/dist/lib/cloud/types.d.ts +6 -4
  51. package/dist/lib/comms-render.d.ts +37 -0
  52. package/dist/lib/comms-render.js +89 -0
  53. package/dist/lib/computer-rpc.js +3 -1
  54. package/dist/lib/crabbox/cli.d.ts +72 -0
  55. package/dist/lib/crabbox/cli.js +162 -9
  56. package/dist/lib/crabbox/runtimes.d.ts +13 -0
  57. package/dist/lib/crabbox/runtimes.js +35 -2
  58. package/dist/lib/daemon.d.ts +20 -4
  59. package/dist/lib/daemon.js +68 -20
  60. package/dist/lib/devices/fleet.d.ts +3 -2
  61. package/dist/lib/devices/fleet.js +9 -0
  62. package/dist/lib/devices/health.d.ts +77 -0
  63. package/dist/lib/devices/health.js +186 -0
  64. package/dist/lib/devices/registry.d.ts +15 -0
  65. package/dist/lib/devices/registry.js +9 -0
  66. package/dist/lib/exec.d.ts +19 -2
  67. package/dist/lib/exec.js +41 -13
  68. package/dist/lib/fleet/apply.d.ts +63 -0
  69. package/dist/lib/fleet/apply.js +214 -0
  70. package/dist/lib/fleet/auth-sync.d.ts +67 -0
  71. package/dist/lib/fleet/auth-sync.js +142 -0
  72. package/dist/lib/fleet/manifest.d.ts +29 -0
  73. package/dist/lib/fleet/manifest.js +127 -0
  74. package/dist/lib/fleet/types.d.ts +129 -0
  75. package/dist/lib/fleet/types.js +13 -0
  76. package/dist/lib/git.d.ts +27 -0
  77. package/dist/lib/git.js +34 -2
  78. package/dist/lib/hosts/dispatch.d.ts +29 -8
  79. package/dist/lib/hosts/dispatch.js +46 -18
  80. package/dist/lib/hosts/passthrough.js +2 -0
  81. package/dist/lib/hosts/providers/devices.d.ts +27 -0
  82. package/dist/lib/hosts/providers/devices.js +98 -0
  83. package/dist/lib/hosts/registry.d.ts +10 -16
  84. package/dist/lib/hosts/registry.js +17 -50
  85. package/dist/lib/hosts/remote-cmd.d.ts +23 -0
  86. package/dist/lib/hosts/remote-cmd.js +71 -0
  87. package/dist/lib/hosts/run-target.d.ts +84 -0
  88. package/dist/lib/hosts/run-target.js +99 -0
  89. package/dist/lib/hosts/types.d.ts +23 -5
  90. package/dist/lib/hosts/types.js +22 -4
  91. package/dist/lib/linear-autoclose.d.ts +30 -0
  92. package/dist/lib/linear-autoclose.js +22 -0
  93. package/dist/lib/mailbox.d.ts +39 -0
  94. package/dist/lib/mailbox.js +112 -0
  95. package/dist/lib/mcp.d.ts +27 -1
  96. package/dist/lib/mcp.js +126 -12
  97. package/dist/lib/monitors/config.d.ts +161 -0
  98. package/dist/lib/monitors/config.js +372 -0
  99. package/dist/lib/monitors/dispatch.d.ts +28 -0
  100. package/dist/lib/monitors/dispatch.js +91 -0
  101. package/dist/lib/monitors/engine.d.ts +61 -0
  102. package/dist/lib/monitors/engine.js +201 -0
  103. package/dist/lib/monitors/sources/command.d.ts +11 -0
  104. package/dist/lib/monitors/sources/command.js +31 -0
  105. package/dist/lib/monitors/sources/device.d.ts +13 -0
  106. package/dist/lib/monitors/sources/device.js +35 -0
  107. package/dist/lib/monitors/sources/file.d.ts +14 -0
  108. package/dist/lib/monitors/sources/file.js +57 -0
  109. package/dist/lib/monitors/sources/http.d.ts +10 -0
  110. package/dist/lib/monitors/sources/http.js +34 -0
  111. package/dist/lib/monitors/sources/index.d.ts +14 -0
  112. package/dist/lib/monitors/sources/index.js +31 -0
  113. package/dist/lib/monitors/sources/poll.d.ts +9 -0
  114. package/dist/lib/monitors/sources/poll.js +9 -0
  115. package/dist/lib/monitors/sources/types.d.ts +18 -0
  116. package/dist/lib/monitors/sources/types.js +9 -0
  117. package/dist/lib/monitors/sources/webhook.d.ts +23 -0
  118. package/dist/lib/monitors/sources/webhook.js +47 -0
  119. package/dist/lib/monitors/sources/ws.d.ts +14 -0
  120. package/dist/lib/monitors/sources/ws.js +45 -0
  121. package/dist/lib/monitors/state.d.ts +69 -0
  122. package/dist/lib/monitors/state.js +144 -0
  123. package/dist/lib/paths.d.ts +13 -0
  124. package/dist/lib/paths.js +26 -4
  125. package/dist/lib/platform/exec.d.ts +16 -0
  126. package/dist/lib/platform/exec.js +17 -0
  127. package/dist/lib/plugins.js +101 -2
  128. package/dist/lib/redact.d.ts +14 -1
  129. package/dist/lib/redact.js +47 -1
  130. package/dist/lib/remote-agents-json.js +7 -1
  131. package/dist/lib/rotate.d.ts +6 -3
  132. package/dist/lib/rotate.js +0 -1
  133. package/dist/lib/routines.d.ts +37 -2
  134. package/dist/lib/routines.js +54 -12
  135. package/dist/lib/runner.d.ts +1 -0
  136. package/dist/lib/runner.js +348 -13
  137. package/dist/lib/sandbox.d.ts +9 -1
  138. package/dist/lib/sandbox.js +11 -2
  139. package/dist/lib/secrets/agent.d.ts +48 -10
  140. package/dist/lib/secrets/agent.js +123 -15
  141. package/dist/lib/secrets/bundles.d.ts +26 -0
  142. package/dist/lib/secrets/bundles.js +59 -8
  143. package/dist/lib/secrets/mcp.js +4 -2
  144. package/dist/lib/secrets/remote.d.ts +17 -0
  145. package/dist/lib/secrets/remote.js +40 -0
  146. package/dist/lib/self-update.d.ts +20 -0
  147. package/dist/lib/self-update.js +54 -1
  148. package/dist/lib/serve/control.d.ts +95 -0
  149. package/dist/lib/serve/control.js +260 -0
  150. package/dist/lib/serve/server.d.ts +35 -1
  151. package/dist/lib/serve/server.js +106 -76
  152. package/dist/lib/serve/stream.d.ts +43 -0
  153. package/dist/lib/serve/stream.js +116 -0
  154. package/dist/lib/serve/token.d.ts +35 -0
  155. package/dist/lib/serve/token.js +85 -0
  156. package/dist/lib/session/bundle.d.ts +164 -0
  157. package/dist/lib/session/bundle.js +200 -0
  158. package/dist/lib/session/remote-bundle.d.ts +12 -0
  159. package/dist/lib/session/remote-bundle.js +61 -0
  160. package/dist/lib/session/remote-list.js +5 -1
  161. package/dist/lib/session/state.d.ts +7 -25
  162. package/dist/lib/session/state.js +16 -6
  163. package/dist/lib/session/sync/agents.d.ts +54 -6
  164. package/dist/lib/session/sync/agents.js +0 -0
  165. package/dist/lib/session/sync/config.js +8 -2
  166. package/dist/lib/session/sync/manifest.d.ts +14 -3
  167. package/dist/lib/session/sync/manifest.js +4 -0
  168. package/dist/lib/session/sync/sync.d.ts +23 -2
  169. package/dist/lib/session/sync/sync.js +177 -74
  170. package/dist/lib/session/types.d.ts +30 -0
  171. package/dist/lib/ssh-tunnel.d.ts +19 -1
  172. package/dist/lib/ssh-tunnel.js +99 -8
  173. package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
  174. package/dist/lib/staleness/detectors/subagents.js +5 -192
  175. package/dist/lib/staleness/writers/subagents.d.ts +10 -0
  176. package/dist/lib/staleness/writers/subagents.js +11 -102
  177. package/dist/lib/startup/command-registry.d.ts +4 -0
  178. package/dist/lib/startup/command-registry.js +9 -0
  179. package/dist/lib/state.d.ts +5 -0
  180. package/dist/lib/state.js +12 -0
  181. package/dist/lib/subagents-registry.d.ts +85 -0
  182. package/dist/lib/subagents-registry.js +393 -0
  183. package/dist/lib/subagents.d.ts +8 -8
  184. package/dist/lib/subagents.js +32 -663
  185. package/dist/lib/sync-umbrella.d.ts +1 -0
  186. package/dist/lib/sync-umbrella.js +14 -3
  187. package/dist/lib/tmux/session.d.ts +7 -0
  188. package/dist/lib/tmux/session.js +3 -1
  189. package/dist/lib/triggers/webhook.d.ts +18 -0
  190. package/dist/lib/triggers/webhook.js +105 -0
  191. package/dist/lib/types.d.ts +35 -1
  192. package/dist/lib/usage.d.ts +42 -3
  193. package/dist/lib/usage.js +163 -21
  194. package/dist/lib/versions.js +14 -11
  195. package/dist/lib/workflows.d.ts +20 -0
  196. package/dist/lib/workflows.js +24 -0
  197. 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.push('origin', branch);
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.push('origin', branch);
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
- /** Explicit run strategy (e.g. "balanced") to forward as `--strategy <strategy>`. */
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 to forward as `--effort <effort>`. */
71
+ /** Reasoning effort forwarded unless 'auto' (the remote default). */
70
72
  effort?: string;
71
- /** Additional directories to grant, already made remote-portable. */
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
- /** Stream events as JSON lines. */
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
- /** Kill the agent after this duration, forwarded as `--timeout <duration>`. */
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
- if (opts.effort)
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
- if (opts.addDir) {
251
- for (const dir of opts.addDir)
252
- args.push('--add-dir', dir);
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
- if (opts.effort)
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
- if (opts.addDir) {
295
- for (const dir of opts.addDir)
296
- args.push('--add-dir', dir);
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
+ }