@phnx-labs/agents-cli 1.20.64 → 1.20.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/CHANGELOG.md +39 -3
  2. package/README.md +37 -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 +94 -49
  10. package/dist/commands/feed.js +25 -11
  11. package/dist/commands/hosts.js +44 -6
  12. package/dist/commands/mcp.js +55 -5
  13. package/dist/commands/monitors.d.ts +12 -0
  14. package/dist/commands/monitors.js +740 -0
  15. package/dist/commands/output.js +2 -2
  16. package/dist/commands/routines.js +23 -2
  17. package/dist/commands/secrets.d.ts +16 -0
  18. package/dist/commands/secrets.js +215 -64
  19. package/dist/commands/serve.js +31 -0
  20. package/dist/commands/sessions-export.js +8 -3
  21. package/dist/commands/sessions.d.ts +16 -0
  22. package/dist/commands/sessions.js +36 -6
  23. package/dist/commands/ssh.js +45 -2
  24. package/dist/commands/versions.js +7 -3
  25. package/dist/commands/view.d.ts +26 -0
  26. package/dist/commands/view.js +32 -9
  27. package/dist/commands/webhook.js +10 -2
  28. package/dist/index.js +34 -14
  29. package/dist/lib/agents.d.ts +18 -0
  30. package/dist/lib/agents.js +53 -3
  31. package/dist/lib/auto-dispatch-provider.js +7 -2
  32. package/dist/lib/auto-dispatch.d.ts +3 -0
  33. package/dist/lib/auto-dispatch.js +3 -0
  34. package/dist/lib/browser/chrome.js +2 -2
  35. package/dist/lib/cloud/antigravity.js +2 -2
  36. package/dist/lib/cloud/host.d.ts +59 -0
  37. package/dist/lib/cloud/host.js +224 -0
  38. package/dist/lib/cloud/registry.js +4 -0
  39. package/dist/lib/cloud/types.d.ts +6 -4
  40. package/dist/lib/computer-rpc.js +3 -1
  41. package/dist/lib/crabbox/cli.js +5 -1
  42. package/dist/lib/crabbox/runtimes.js +11 -2
  43. package/dist/lib/daemon.d.ts +20 -4
  44. package/dist/lib/daemon.js +62 -19
  45. package/dist/lib/devices/fleet.d.ts +3 -2
  46. package/dist/lib/devices/fleet.js +9 -0
  47. package/dist/lib/devices/registry.d.ts +15 -0
  48. package/dist/lib/devices/registry.js +9 -0
  49. package/dist/lib/exec.d.ts +19 -2
  50. package/dist/lib/exec.js +41 -13
  51. package/dist/lib/fleet/apply.d.ts +63 -0
  52. package/dist/lib/fleet/apply.js +214 -0
  53. package/dist/lib/fleet/auth-sync.d.ts +67 -0
  54. package/dist/lib/fleet/auth-sync.js +142 -0
  55. package/dist/lib/fleet/manifest.d.ts +29 -0
  56. package/dist/lib/fleet/manifest.js +127 -0
  57. package/dist/lib/fleet/types.d.ts +129 -0
  58. package/dist/lib/fleet/types.js +13 -0
  59. package/dist/lib/git.d.ts +27 -0
  60. package/dist/lib/git.js +34 -2
  61. package/dist/lib/hosts/dispatch.d.ts +29 -8
  62. package/dist/lib/hosts/dispatch.js +46 -18
  63. package/dist/lib/hosts/passthrough.js +2 -0
  64. package/dist/lib/hosts/providers/devices.d.ts +27 -0
  65. package/dist/lib/hosts/providers/devices.js +98 -0
  66. package/dist/lib/hosts/registry.d.ts +10 -16
  67. package/dist/lib/hosts/registry.js +17 -50
  68. package/dist/lib/hosts/remote-cmd.d.ts +23 -0
  69. package/dist/lib/hosts/remote-cmd.js +71 -0
  70. package/dist/lib/hosts/run-target.d.ts +84 -0
  71. package/dist/lib/hosts/run-target.js +99 -0
  72. package/dist/lib/hosts/types.d.ts +23 -5
  73. package/dist/lib/hosts/types.js +22 -4
  74. package/dist/lib/linear-autoclose.d.ts +30 -0
  75. package/dist/lib/linear-autoclose.js +22 -0
  76. package/dist/lib/mcp.d.ts +27 -1
  77. package/dist/lib/mcp.js +126 -12
  78. package/dist/lib/monitors/config.d.ts +161 -0
  79. package/dist/lib/monitors/config.js +372 -0
  80. package/dist/lib/monitors/dispatch.d.ts +28 -0
  81. package/dist/lib/monitors/dispatch.js +91 -0
  82. package/dist/lib/monitors/engine.d.ts +61 -0
  83. package/dist/lib/monitors/engine.js +201 -0
  84. package/dist/lib/monitors/sources/command.d.ts +11 -0
  85. package/dist/lib/monitors/sources/command.js +31 -0
  86. package/dist/lib/monitors/sources/device.d.ts +13 -0
  87. package/dist/lib/monitors/sources/device.js +35 -0
  88. package/dist/lib/monitors/sources/file.d.ts +14 -0
  89. package/dist/lib/monitors/sources/file.js +57 -0
  90. package/dist/lib/monitors/sources/http.d.ts +10 -0
  91. package/dist/lib/monitors/sources/http.js +34 -0
  92. package/dist/lib/monitors/sources/index.d.ts +14 -0
  93. package/dist/lib/monitors/sources/index.js +31 -0
  94. package/dist/lib/monitors/sources/poll.d.ts +9 -0
  95. package/dist/lib/monitors/sources/poll.js +9 -0
  96. package/dist/lib/monitors/sources/types.d.ts +18 -0
  97. package/dist/lib/monitors/sources/types.js +9 -0
  98. package/dist/lib/monitors/sources/webhook.d.ts +23 -0
  99. package/dist/lib/monitors/sources/webhook.js +47 -0
  100. package/dist/lib/monitors/sources/ws.d.ts +14 -0
  101. package/dist/lib/monitors/sources/ws.js +45 -0
  102. package/dist/lib/monitors/state.d.ts +69 -0
  103. package/dist/lib/monitors/state.js +144 -0
  104. package/dist/lib/platform/exec.d.ts +16 -0
  105. package/dist/lib/platform/exec.js +17 -0
  106. package/dist/lib/plugins.js +101 -2
  107. package/dist/lib/redact.d.ts +14 -1
  108. package/dist/lib/redact.js +47 -1
  109. package/dist/lib/remote-agents-json.js +7 -1
  110. package/dist/lib/rotate.d.ts +6 -3
  111. package/dist/lib/rotate.js +0 -1
  112. package/dist/lib/routines.d.ts +16 -0
  113. package/dist/lib/routines.js +19 -0
  114. package/dist/lib/runner.d.ts +1 -0
  115. package/dist/lib/runner.js +102 -9
  116. package/dist/lib/secrets/agent.d.ts +48 -10
  117. package/dist/lib/secrets/agent.js +123 -15
  118. package/dist/lib/secrets/bundles.d.ts +26 -0
  119. package/dist/lib/secrets/bundles.js +59 -8
  120. package/dist/lib/secrets/mcp.js +4 -2
  121. package/dist/lib/secrets/remote.d.ts +17 -0
  122. package/dist/lib/secrets/remote.js +40 -0
  123. package/dist/lib/self-update.d.ts +20 -0
  124. package/dist/lib/self-update.js +54 -1
  125. package/dist/lib/serve/control.d.ts +95 -0
  126. package/dist/lib/serve/control.js +260 -0
  127. package/dist/lib/serve/server.d.ts +35 -1
  128. package/dist/lib/serve/server.js +106 -76
  129. package/dist/lib/serve/stream.d.ts +43 -0
  130. package/dist/lib/serve/stream.js +116 -0
  131. package/dist/lib/serve/token.d.ts +35 -0
  132. package/dist/lib/serve/token.js +85 -0
  133. package/dist/lib/session/bundle.d.ts +14 -0
  134. package/dist/lib/session/bundle.js +12 -1
  135. package/dist/lib/session/remote-list.js +5 -1
  136. package/dist/lib/session/state.d.ts +7 -25
  137. package/dist/lib/session/state.js +16 -6
  138. package/dist/lib/session/sync/config.js +8 -2
  139. package/dist/lib/session/types.d.ts +30 -0
  140. package/dist/lib/ssh-tunnel.d.ts +19 -1
  141. package/dist/lib/ssh-tunnel.js +86 -7
  142. package/dist/lib/startup/command-registry.d.ts +2 -0
  143. package/dist/lib/startup/command-registry.js +4 -0
  144. package/dist/lib/state.d.ts +5 -0
  145. package/dist/lib/state.js +12 -0
  146. package/dist/lib/tmux/session.d.ts +7 -0
  147. package/dist/lib/tmux/session.js +3 -1
  148. package/dist/lib/triggers/webhook.d.ts +18 -0
  149. package/dist/lib/triggers/webhook.js +105 -0
  150. package/dist/lib/types.d.ts +26 -1
  151. package/dist/lib/usage.js +7 -5
  152. package/dist/lib/versions.js +14 -11
  153. package/dist/lib/workflows.d.ts +20 -0
  154. package/dist/lib/workflows.js +24 -0
  155. package/package.json +2 -1
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Login/token propagation — the "one login instead of ~48 OAuth flows" core of
3
+ * `agents apply`.
4
+ *
5
+ * A source machine that is already signed in to a set of harnesses has portable
6
+ * credential files on disk (verified per-agent locations below). `snapshotAuth`
7
+ * captures those; `materializeAuth` writes them into the corresponding paths on
8
+ * a target device. Transport is the caller's job — `apply` streams the bundle
9
+ * over the existing (encrypted, authenticated) SSH channel via `sshExec`'s
10
+ * stdin `input`, so no app-layer crypto is layered on top of SSH here.
11
+ *
12
+ * Honest boundary: on macOS, claude and antigravity keep their tokens in the
13
+ * login keychain, ACL-bound to the harness process — unreadable by us. Those are
14
+ * classified `bound` and surfaced for a one-time manual login, never faked.
15
+ */
16
+ import * as fs from 'fs';
17
+ import * as path from 'path';
18
+ /**
19
+ * Verified portable auth-file locations per agent (home-relative). Sourced from
20
+ * live inspection of a Linux fleet box + the agent registry. Agents absent here
21
+ * have no portable credential file we can propagate.
22
+ */
23
+ export const FLEET_AUTH_FILES = {
24
+ claude: [{ rel: '.claude/.credentials.json', mode: 0o600 }],
25
+ codex: [{ rel: '.codex/auth.json', mode: 0o600 }],
26
+ gemini: [{ rel: '.gemini/oauth_creds.json', mode: 0o600 }],
27
+ grok: [{ rel: '.grok/auth.json', mode: 0o600 }],
28
+ kimi: [{ rel: '.kimi-code/credentials/kimi-code.json', mode: 0o600 }],
29
+ opencode: [{ rel: '.local/share/opencode/auth.json', mode: 0o600 }],
30
+ droid: [
31
+ { rel: '.factory/auth.v2.file', mode: 0o600 },
32
+ { rel: '.factory/auth.v2.key', mode: 0o600 },
33
+ ],
34
+ antigravity: [{ rel: '.gemini/antigravity-cli/antigravity-oauth-token', mode: 0o600 }],
35
+ };
36
+ /** Agents whose macOS credentials live in the ACL-bound login keychain. */
37
+ export const KEYCHAIN_BOUND_ON_MAC = new Set(['claude', 'antigravity']);
38
+ /** Which agents `apply` can propagate auth for at all. */
39
+ export function isPropagatableAgent(agent) {
40
+ return agent in FLEET_AUTH_FILES;
41
+ }
42
+ /**
43
+ * Capture portable credential files for the given agents from a source home.
44
+ * Returns the readable file payloads plus the list of agents whose auth is
45
+ * device-bound (macOS keychain) and therefore cannot be captured. Agents that
46
+ * are simply not signed in (no file on disk) are silently omitted — nothing to
47
+ * propagate, not an error.
48
+ */
49
+ export function snapshotAuth(agents, opts) {
50
+ const files = [];
51
+ const bound = [];
52
+ for (const agent of agents) {
53
+ const specs = FLEET_AUTH_FILES[agent];
54
+ if (!specs)
55
+ continue; // not propagatable — caller surfaces separately if desired
56
+ if (opts.platform === 'darwin' && KEYCHAIN_BOUND_ON_MAC.has(agent)) {
57
+ bound.push(agent);
58
+ continue;
59
+ }
60
+ for (const spec of specs) {
61
+ const abs = path.join(opts.home, spec.rel);
62
+ let stat;
63
+ try {
64
+ stat = fs.statSync(abs);
65
+ }
66
+ catch {
67
+ continue; // not signed in for this agent — nothing to carry
68
+ }
69
+ if (!stat.isFile())
70
+ continue;
71
+ const content = fs.readFileSync(abs); // follows symlinks into version homes
72
+ files.push({
73
+ agent,
74
+ rel: spec.rel,
75
+ contentB64: content.toString('base64'),
76
+ mode: (stat.mode & 0o777) || spec.mode,
77
+ });
78
+ }
79
+ }
80
+ return { files, bound };
81
+ }
82
+ /** Assemble the plaintext bundle shipped to a target. */
83
+ export function buildAuthBundle(source, files) {
84
+ return { v: 1, source, files };
85
+ }
86
+ /**
87
+ * Write a captured auth bundle into a target home. Parent dirs are created; the
88
+ * captured POSIX mode is restored (0600 for credentials). Writing through an
89
+ * existing symlink lands in the agent's active version home, which is exactly
90
+ * what per-version credential carry-forward expects.
91
+ */
92
+ export function materializeAuth(bundle, opts) {
93
+ const written = new Set();
94
+ const errors = [];
95
+ for (const f of bundle.files) {
96
+ const abs = path.join(opts.home, f.rel);
97
+ try {
98
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
99
+ fs.writeFileSync(abs, Buffer.from(f.contentB64, 'base64'));
100
+ fs.chmodSync(abs, f.mode || 0o600);
101
+ written.add(f.agent);
102
+ }
103
+ catch (e) {
104
+ errors.push(`${f.rel}: ${e.message}`);
105
+ }
106
+ }
107
+ return { written: [...written], errors };
108
+ }
109
+ /** Parse + validate a bundle received on stdin (the remote `--_recv-auth` path). */
110
+ export function parseAuthBundle(raw) {
111
+ let doc;
112
+ try {
113
+ doc = JSON.parse(raw);
114
+ }
115
+ catch (e) {
116
+ throw new Error(`auth bundle is not valid JSON: ${e.message}`);
117
+ }
118
+ if (typeof doc !== 'object' || doc === null)
119
+ throw new Error('auth bundle must be an object.');
120
+ const o = doc;
121
+ if (o.v !== 1)
122
+ throw new Error(`unsupported auth bundle version ${JSON.stringify(o.v)}.`);
123
+ if (typeof o.source !== 'string')
124
+ throw new Error('auth bundle missing source.');
125
+ if (!Array.isArray(o.files))
126
+ throw new Error('auth bundle missing files[].');
127
+ for (const f of o.files) {
128
+ if (typeof f !== 'object' || f === null ||
129
+ typeof f.agent !== 'string' ||
130
+ typeof f.rel !== 'string' ||
131
+ typeof f.contentB64 !== 'string' ||
132
+ typeof f.mode !== 'number') {
133
+ throw new Error('auth bundle has a malformed file entry.');
134
+ }
135
+ // Reject path traversal — rel must stay under $HOME.
136
+ const rel = f.rel;
137
+ if (rel.startsWith('/') || rel.split('/').includes('..')) {
138
+ throw new Error(`auth bundle rejected unsafe path: ${rel}`);
139
+ }
140
+ }
141
+ return doc;
142
+ }
@@ -0,0 +1,29 @@
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 type { FleetManifest, DeviceDesired } from './types.js';
11
+ /** Validate a raw `fleet:` object (already YAML-parsed) into a typed manifest. */
12
+ export declare function parseFleetManifest(raw: unknown): FleetManifest;
13
+ /** Read a YAML file and extract + validate its `fleet:` block. */
14
+ export declare function readFleetFile(filePath: string): FleetManifest;
15
+ export interface ResolveContext {
16
+ /** Device names currently online (used to expand `devices: all`). */
17
+ onlineDevices: string[];
18
+ /** All registered device names (used to validate explicit entries). */
19
+ registeredDevices: string[];
20
+ /** The source machine, always excluded from the target set. */
21
+ source: string;
22
+ }
23
+ /**
24
+ * Expand a manifest into concrete per-device desired states. `devices: all`
25
+ * expands to every online registered device minus the source; an explicit map
26
+ * validates each name against the registry. Devices with no desired agents,
27
+ * sync scopes, and `login: skip` are still returned (probe/report only).
28
+ */
29
+ export declare function resolveDesired(manifest: FleetManifest, ctx: ResolveContext): DeviceDesired[];
@@ -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
  }