@phnx-labs/agents-cli 1.20.64 → 1.20.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/CHANGELOG.md +51 -3
  2. package/README.md +156 -3
  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.d.ts +48 -0
  10. package/dist/commands/exec.js +159 -49
  11. package/dist/commands/feed.js +25 -11
  12. package/dist/commands/hosts.js +44 -6
  13. package/dist/commands/mcp.js +55 -5
  14. package/dist/commands/monitors.d.ts +12 -0
  15. package/dist/commands/monitors.js +748 -0
  16. package/dist/commands/output.js +2 -2
  17. package/dist/commands/plugins.js +28 -7
  18. package/dist/commands/routines.js +23 -2
  19. package/dist/commands/secrets.d.ts +16 -0
  20. package/dist/commands/secrets.js +215 -64
  21. package/dist/commands/serve.js +31 -0
  22. package/dist/commands/sessions-browser.d.ts +82 -0
  23. package/dist/commands/sessions-browser.js +320 -0
  24. package/dist/commands/sessions-export.js +8 -3
  25. package/dist/commands/sessions.d.ts +17 -0
  26. package/dist/commands/sessions.js +157 -10
  27. package/dist/commands/share.d.ts +2 -0
  28. package/dist/commands/share.js +150 -0
  29. package/dist/commands/ssh.js +54 -3
  30. package/dist/commands/versions.js +7 -3
  31. package/dist/commands/view.d.ts +26 -0
  32. package/dist/commands/view.js +32 -9
  33. package/dist/commands/webhook.js +10 -2
  34. package/dist/index.js +35 -14
  35. package/dist/lib/agents.d.ts +46 -0
  36. package/dist/lib/agents.js +121 -3
  37. package/dist/lib/auto-dispatch-provider.js +7 -2
  38. package/dist/lib/auto-dispatch.d.ts +3 -0
  39. package/dist/lib/auto-dispatch.js +3 -0
  40. package/dist/lib/browser/chrome.js +2 -2
  41. package/dist/lib/cloud/antigravity.js +2 -2
  42. package/dist/lib/cloud/host.d.ts +59 -0
  43. package/dist/lib/cloud/host.js +224 -0
  44. package/dist/lib/cloud/registry.js +4 -0
  45. package/dist/lib/cloud/types.d.ts +6 -4
  46. package/dist/lib/computer-rpc.js +3 -1
  47. package/dist/lib/crabbox/cli.js +5 -1
  48. package/dist/lib/crabbox/runtimes.js +11 -2
  49. package/dist/lib/daemon.d.ts +20 -4
  50. package/dist/lib/daemon.js +62 -19
  51. package/dist/lib/devices/connect.d.ts +18 -1
  52. package/dist/lib/devices/connect.js +10 -2
  53. package/dist/lib/devices/fleet.d.ts +3 -2
  54. package/dist/lib/devices/fleet.js +9 -0
  55. package/dist/lib/devices/known-hosts.d.ts +62 -0
  56. package/dist/lib/devices/known-hosts.js +137 -0
  57. package/dist/lib/devices/registry.d.ts +15 -0
  58. package/dist/lib/devices/registry.js +9 -0
  59. package/dist/lib/exec.d.ts +19 -2
  60. package/dist/lib/exec.js +41 -13
  61. package/dist/lib/fleet/apply.d.ts +63 -0
  62. package/dist/lib/fleet/apply.js +214 -0
  63. package/dist/lib/fleet/auth-sync.d.ts +67 -0
  64. package/dist/lib/fleet/auth-sync.js +142 -0
  65. package/dist/lib/fleet/manifest.d.ts +29 -0
  66. package/dist/lib/fleet/manifest.js +127 -0
  67. package/dist/lib/fleet/types.d.ts +129 -0
  68. package/dist/lib/fleet/types.js +13 -0
  69. package/dist/lib/git.d.ts +27 -0
  70. package/dist/lib/git.js +34 -2
  71. package/dist/lib/hosts/dispatch.d.ts +29 -8
  72. package/dist/lib/hosts/dispatch.js +66 -20
  73. package/dist/lib/hosts/passthrough.js +2 -0
  74. package/dist/lib/hosts/providers/devices.d.ts +27 -0
  75. package/dist/lib/hosts/providers/devices.js +98 -0
  76. package/dist/lib/hosts/registry.d.ts +10 -16
  77. package/dist/lib/hosts/registry.js +17 -50
  78. package/dist/lib/hosts/remote-cmd.d.ts +23 -0
  79. package/dist/lib/hosts/remote-cmd.js +79 -4
  80. package/dist/lib/hosts/run-target.d.ts +84 -0
  81. package/dist/lib/hosts/run-target.js +99 -0
  82. package/dist/lib/hosts/types.d.ts +23 -5
  83. package/dist/lib/hosts/types.js +22 -4
  84. package/dist/lib/linear-autoclose.d.ts +30 -0
  85. package/dist/lib/linear-autoclose.js +22 -0
  86. package/dist/lib/mcp.d.ts +27 -1
  87. package/dist/lib/mcp.js +126 -12
  88. package/dist/lib/monitors/config.d.ts +161 -0
  89. package/dist/lib/monitors/config.js +372 -0
  90. package/dist/lib/monitors/dispatch.d.ts +28 -0
  91. package/dist/lib/monitors/dispatch.js +91 -0
  92. package/dist/lib/monitors/engine.d.ts +61 -0
  93. package/dist/lib/monitors/engine.js +205 -0
  94. package/dist/lib/monitors/sources/command.d.ts +11 -0
  95. package/dist/lib/monitors/sources/command.js +31 -0
  96. package/dist/lib/monitors/sources/device.d.ts +13 -0
  97. package/dist/lib/monitors/sources/device.js +45 -0
  98. package/dist/lib/monitors/sources/file.d.ts +14 -0
  99. package/dist/lib/monitors/sources/file.js +57 -0
  100. package/dist/lib/monitors/sources/http.d.ts +10 -0
  101. package/dist/lib/monitors/sources/http.js +34 -0
  102. package/dist/lib/monitors/sources/index.d.ts +14 -0
  103. package/dist/lib/monitors/sources/index.js +31 -0
  104. package/dist/lib/monitors/sources/poll.d.ts +9 -0
  105. package/dist/lib/monitors/sources/poll.js +9 -0
  106. package/dist/lib/monitors/sources/types.d.ts +18 -0
  107. package/dist/lib/monitors/sources/types.js +9 -0
  108. package/dist/lib/monitors/sources/webhook.d.ts +23 -0
  109. package/dist/lib/monitors/sources/webhook.js +47 -0
  110. package/dist/lib/monitors/sources/ws.d.ts +14 -0
  111. package/dist/lib/monitors/sources/ws.js +45 -0
  112. package/dist/lib/monitors/state.d.ts +69 -0
  113. package/dist/lib/monitors/state.js +144 -0
  114. package/dist/lib/picker.d.ts +53 -0
  115. package/dist/lib/picker.js +214 -1
  116. package/dist/lib/platform/exec.d.ts +16 -0
  117. package/dist/lib/platform/exec.js +17 -0
  118. package/dist/lib/plugins.d.ts +31 -1
  119. package/dist/lib/plugins.js +175 -15
  120. package/dist/lib/redact.d.ts +14 -1
  121. package/dist/lib/redact.js +47 -1
  122. package/dist/lib/remote-agents-json.js +7 -1
  123. package/dist/lib/rotate.d.ts +6 -3
  124. package/dist/lib/rotate.js +0 -1
  125. package/dist/lib/routines.d.ts +16 -0
  126. package/dist/lib/routines.js +19 -0
  127. package/dist/lib/runner.d.ts +1 -0
  128. package/dist/lib/runner.js +102 -9
  129. package/dist/lib/secrets/agent.d.ts +83 -10
  130. package/dist/lib/secrets/agent.js +237 -70
  131. package/dist/lib/secrets/bundles.d.ts +26 -0
  132. package/dist/lib/secrets/bundles.js +59 -8
  133. package/dist/lib/secrets/filestore.d.ts +9 -0
  134. package/dist/lib/secrets/filestore.js +21 -8
  135. package/dist/lib/secrets/index.d.ts +7 -0
  136. package/dist/lib/secrets/index.js +26 -6
  137. package/dist/lib/secrets/mcp.js +4 -2
  138. package/dist/lib/secrets/remote.d.ts +17 -0
  139. package/dist/lib/secrets/remote.js +40 -0
  140. package/dist/lib/self-update.d.ts +20 -0
  141. package/dist/lib/self-update.js +54 -1
  142. package/dist/lib/serve/control.d.ts +95 -0
  143. package/dist/lib/serve/control.js +260 -0
  144. package/dist/lib/serve/server.d.ts +35 -1
  145. package/dist/lib/serve/server.js +106 -76
  146. package/dist/lib/serve/stream.d.ts +43 -0
  147. package/dist/lib/serve/stream.js +116 -0
  148. package/dist/lib/serve/token.d.ts +35 -0
  149. package/dist/lib/serve/token.js +85 -0
  150. package/dist/lib/session/bundle.d.ts +14 -0
  151. package/dist/lib/session/bundle.js +12 -1
  152. package/dist/lib/session/remote-list.js +5 -1
  153. package/dist/lib/session/state.d.ts +7 -25
  154. package/dist/lib/session/state.js +16 -6
  155. package/dist/lib/session/sync/config.js +8 -2
  156. package/dist/lib/session/types.d.ts +30 -0
  157. package/dist/lib/share/capture.d.ts +29 -0
  158. package/dist/lib/share/capture.js +140 -0
  159. package/dist/lib/share/config.d.ts +35 -0
  160. package/dist/lib/share/config.js +100 -0
  161. package/dist/lib/share/og.d.ts +25 -0
  162. package/dist/lib/share/og.js +84 -0
  163. package/dist/lib/share/provision.d.ts +10 -0
  164. package/dist/lib/share/provision.js +91 -0
  165. package/dist/lib/share/publish.d.ts +57 -0
  166. package/dist/lib/share/publish.js +145 -0
  167. package/dist/lib/share/worker-template.d.ts +2 -0
  168. package/dist/lib/share/worker-template.js +82 -0
  169. package/dist/lib/shims.d.ts +13 -0
  170. package/dist/lib/shims.js +42 -2
  171. package/dist/lib/ssh-exec.d.ts +24 -0
  172. package/dist/lib/ssh-exec.js +15 -3
  173. package/dist/lib/ssh-tunnel.d.ts +19 -1
  174. package/dist/lib/ssh-tunnel.js +86 -7
  175. package/dist/lib/startup/command-registry.d.ts +3 -0
  176. package/dist/lib/startup/command-registry.js +6 -0
  177. package/dist/lib/state.d.ts +5 -0
  178. package/dist/lib/state.js +12 -0
  179. package/dist/lib/tmux/session.d.ts +7 -0
  180. package/dist/lib/tmux/session.js +3 -1
  181. package/dist/lib/triggers/webhook.d.ts +18 -0
  182. package/dist/lib/triggers/webhook.js +105 -0
  183. package/dist/lib/types.d.ts +36 -1
  184. package/dist/lib/usage.js +7 -5
  185. package/dist/lib/versions.js +14 -11
  186. package/dist/lib/workflows.d.ts +20 -0
  187. package/dist/lib/workflows.js +24 -0
  188. package/package.json +2 -1
@@ -12,7 +12,9 @@
12
12
  * verification, idempotency, source allow-listing, and rate limiting.
13
13
  */
14
14
  import * as crypto from 'crypto';
15
+ import * as fs from 'fs';
15
16
  import * as http from 'http';
17
+ import * as path from 'path';
16
18
  import { jobRunsOnThisDevice, listJobs } from '../routines.js';
17
19
  import { executeJobDetached } from '../runner.js';
18
20
  /** Read `repository.full_name` (`owner/name`) from a webhook payload, if present. */
@@ -236,6 +238,88 @@ export function createMemoryDeliveryStore(maxEntries = 1000) {
236
238
  },
237
239
  };
238
240
  }
241
+ /**
242
+ * A durable, disk-backed delivery store. Unlike `createMemoryDeliveryStore`,
243
+ * seen delivery ids survive a process restart and are bounded by AGE, not by a
244
+ * fixed entry count — so a captured valid delivery cannot re-fire after a
245
+ * restart or after count-based LRU eviction would have dropped it.
246
+ *
247
+ * `retentionMs` doubles as the replay-acceptance window: a delivery whose id is
248
+ * still on record (younger than the window) is rejected as a duplicate; entries
249
+ * older than the window are pruned (keeping the file bounded) since a webhook
250
+ * source will not legitimately retry a delivery that old.
251
+ */
252
+ export function createFileDeliveryStore(filePath, retentionMs = 14 * 24 * 60 * 60 * 1000) {
253
+ const seen = new Map();
254
+ // Load persisted state (best-effort: a corrupt/missing file starts empty).
255
+ try {
256
+ const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
257
+ const loadedAt = Date.now();
258
+ for (const [id, entry] of Object.entries(raw)) {
259
+ if (typeof entry?.updatedAt !== 'number' || loadedAt - entry.updatedAt > retentionMs)
260
+ continue;
261
+ seen.set(id, {
262
+ complete: entry.complete === true,
263
+ jobs: new Set(Array.isArray(entry.jobs) ? entry.jobs : []),
264
+ updatedAt: entry.updatedAt,
265
+ });
266
+ }
267
+ }
268
+ catch {
269
+ // no prior file / unreadable — start empty
270
+ }
271
+ const persist = () => {
272
+ const snapshot = {};
273
+ for (const [id, entry] of seen) {
274
+ snapshot[id] = { complete: entry.complete, jobs: [...entry.jobs], updatedAt: entry.updatedAt };
275
+ }
276
+ try {
277
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
278
+ const tmp = `${filePath}.${process.pid}.tmp`;
279
+ fs.writeFileSync(tmp, JSON.stringify(snapshot), 'utf-8');
280
+ fs.renameSync(tmp, filePath);
281
+ }
282
+ catch {
283
+ // best-effort durability; an unwritable dir must not crash ingress
284
+ }
285
+ };
286
+ const prune = (now) => {
287
+ for (const [id, entry] of seen) {
288
+ if (now - entry.updatedAt > retentionMs)
289
+ seen.delete(id);
290
+ }
291
+ };
292
+ const touch = (id) => {
293
+ const now = Date.now();
294
+ prune(now);
295
+ let current = seen.get(id);
296
+ if (!current) {
297
+ current = { complete: false, jobs: new Set(), updatedAt: now };
298
+ seen.set(id, current);
299
+ }
300
+ current.updatedAt = now;
301
+ return current;
302
+ };
303
+ return {
304
+ seen: (id) => {
305
+ const entry = seen.get(id);
306
+ if (!entry)
307
+ return false;
308
+ if (Date.now() - entry.updatedAt > retentionMs)
309
+ return false;
310
+ return entry.complete === true;
311
+ },
312
+ mark: (id) => {
313
+ touch(id).complete = true;
314
+ persist();
315
+ },
316
+ completedJobs: (id) => new Set(seen.get(id)?.jobs ?? []),
317
+ markJob: (id, jobName) => {
318
+ touch(id).jobs.add(jobName);
319
+ persist();
320
+ },
321
+ };
322
+ }
239
323
  export function createMemoryRateLimiter(limit, windowMs) {
240
324
  const buckets = new Map();
241
325
  return {
@@ -285,6 +369,7 @@ async function readRawBody(req, maxBytes) {
285
369
  export function startWebhookServer(options) {
286
370
  const deliveryStore = options.deliveryStore ?? createMemoryDeliveryStore();
287
371
  const rateLimiter = options.rateLimiter ?? createMemoryRateLimiter(options.rateLimitPerMinute ?? 60, 60_000);
372
+ const ipRateLimiter = options.ipRateLimiter ?? createMemoryRateLimiter(options.ipRateLimitPerMinute ?? 120, 60_000);
288
373
  const maxBodyBytes = options.maxBodyBytes ?? 1024 * 1024;
289
374
  const server = http.createServer((req, res) => {
290
375
  void (async () => {
@@ -305,6 +390,23 @@ export function startWebhookServer(options) {
305
390
  res.end(JSON.stringify({ ok: false, error: `missing ${source} webhook secret` }));
306
391
  return;
307
392
  }
393
+ // Per-IP throttle + declared-size cap BEFORE the (expensive) body read +
394
+ // HMAC. A bad-signature flood of 1 MiB POSTs must be rejected without
395
+ // forcing a full body read and an HMAC per request — the signed-delivery
396
+ // rate limit further down runs only after a signature passes, so it can't
397
+ // shed this load on its own.
398
+ const ip = req.socket.remoteAddress ?? 'unknown';
399
+ if (!ipRateLimiter.take(ip)) {
400
+ res.writeHead(429, { 'content-type': 'application/json' });
401
+ res.end(JSON.stringify({ ok: false, error: 'rate limit exceeded' }));
402
+ return;
403
+ }
404
+ const declaredLength = Number.parseInt(header(req.headers, 'content-length') ?? '', 10);
405
+ if (Number.isFinite(declaredLength) && declaredLength > maxBodyBytes) {
406
+ res.writeHead(413, { 'content-type': 'application/json' });
407
+ res.end(JSON.stringify({ ok: false, error: `payload exceeds ${maxBodyBytes} bytes` }));
408
+ return;
409
+ }
308
410
  try {
309
411
  const rawBody = await readRawBody(req, maxBodyBytes);
310
412
  const valid = source === 'github'
@@ -357,6 +459,9 @@ export function startWebhookServer(options) {
357
459
  }
358
460
  })();
359
461
  });
462
+ // Connection cap: bound how many concurrent TCP connections the receiver
463
+ // will hold open, so a flood cannot exhaust file descriptors / memory.
464
+ server.maxConnections = options.maxConnections ?? 256;
360
465
  server.listen(options.port ?? 0, options.host ?? '127.0.0.1');
361
466
  return server;
362
467
  }
@@ -515,6 +515,20 @@ export interface PluginManifest {
515
515
  userConfig?: PluginUserConfigField[];
516
516
  /** Other plugin names this plugin depends on. Missing deps produce a warning. */
517
517
  dependencies?: string[];
518
+ /**
519
+ * Inline hook configuration (or a path to a hooks JSON file) declared directly
520
+ * in the manifest, per the official plugin format — an execution surface even
521
+ * when the plugin ships no `hooks/` directory. Untyped because the shape is a
522
+ * path string or an inline event map; capability detection only needs to know
523
+ * whether it is present and non-empty.
524
+ */
525
+ hooks?: unknown;
526
+ /**
527
+ * Inline MCP-server configuration (or a path to an MCP JSON file) declared
528
+ * directly in the manifest — an execution surface even when the plugin ships
529
+ * no `.mcp.json`. Untyped for the same reason as `hooks`.
530
+ */
531
+ mcpServers?: unknown;
518
532
  }
519
533
  /** A plugin found on disk with its parsed manifest and resource inventory. */
520
534
  export interface DiscoveredPlugin {
@@ -637,11 +651,15 @@ export interface Meta {
637
651
  * bundles without an explicit per-bundle policy: `daily` (the default) asks
638
652
  * once per ~7 days, `always` asks every time. `auto` (default on) lets the
639
653
  * first real keychain read of a `daily` bundle populate the broker so
640
- * concurrent runs read silently — set it `false` to force a prompt on every read. */
654
+ * concurrent runs read silently — set it `false` to force a prompt on every read.
655
+ * `holdMs` caps how long an unlocked/auto-cached bundle is held before the next
656
+ * read re-prompts (default 7 days; e.g. 86400000 for a 24h cap). Clamped to
657
+ * [1 minute, 30 days]. */
641
658
  secrets?: {
642
659
  policy?: 'always' | 'daily';
643
660
  agent?: {
644
661
  auto?: boolean;
662
+ holdMs?: number;
645
663
  };
646
664
  };
647
665
  /** Spend guardrails (issue #346). User-global caps; project agents.yaml overrides. */
@@ -698,6 +716,23 @@ export interface Meta {
698
716
  * are never copied. `inline` hosts carry their own address/user.
699
717
  */
700
718
  hosts?: Record<string, HostEntry>;
719
+ /**
720
+ * Declarative fleet profile (`agents apply` / `ag apply`). Additive to the
721
+ * schema — project `agents:` version-pins are untouched. Declares which agents
722
+ * every device should have, which config to sync, and how login propagates.
723
+ * Full shape in `lib/fleet/types.ts` (FleetManifest).
724
+ */
725
+ fleet?: import('./fleet/types.js').FleetManifest;
726
+ /** `agents share` endpoint (Cloudflare R2 + Worker). Set by `agents share
727
+ * setup`/`join`; syncs fleet-wide via `agents repo push/pull`. The write token
728
+ * lives in the `share` secrets bundle, not here. */
729
+ share?: {
730
+ baseUrl?: string;
731
+ accountId?: string;
732
+ workerName?: string;
733
+ bucketName?: string;
734
+ domain?: string;
735
+ };
701
736
  }
702
737
  /** Persisted agent-host entry in agents.yaml (overlay or inline). */
703
738
  export interface HostEntry {
package/dist/lib/usage.js CHANGED
@@ -207,12 +207,14 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3) {
207
207
  parts.push(chalk.gray(plan.padEnd(planWidth)));
208
208
  }
209
209
  if (snapshot) {
210
- // Compact rows show the two windows every agent shares (session + week);
211
- // extra windows (Claude's Sonnet week, Droid's month) render only in the
212
- // full per-version usage section. An exhausted month window still surfaces
213
- // here as the rate-limited badge via deriveUsageStatusFromSnapshot.
210
+ // Compact rows show every BLOCKING window the same set
211
+ // deriveUsageStatusFromSnapshot uses for the rate-limited badge so an
212
+ // account throttled by its month window (Droid meters on 5h/week/month)
213
+ // shows the bar that explains why. Claude's Sonnet week is a per-model
214
+ // sub-limit, not a blocking window; it renders only in the full
215
+ // per-version usage section.
214
216
  const windows = snapshot.windows
215
- .filter((window) => window.key === 'session' || window.key === 'week')
217
+ .filter((window) => window.key !== 'sonnet_week')
216
218
  .map((window) => `${chalk.gray(`${window.shortLabel}:`)} ${renderCompactUsageBar(window.usedPercent)}`);
217
219
  if (windows.length > 0) {
218
220
  parts.push(windows.join(' '));
@@ -30,7 +30,7 @@ import { listResources } from './resources.js';
30
30
  import { VERSION_RE, compareVersions } from './agent-spec/primitives.js';
31
31
  import { AGENTS, agentConfigDirName, getAccountEmail, resolveAgentName, formatAgentError, findInPath, isSelfUpdatingAgent } from './agents.js';
32
32
  import { discoverPermissionGroups, getActivePermissionPresetName, readPermissionPresetRecipe, PERMISSION_PRESET_ENV_VAR } from './permissions.js';
33
- import { parseMcpServerConfig } from './mcp.js';
33
+ import { parseMcpServerConfig, isProjectMcpTrusted } from './mcp.js';
34
34
  import { createVersionedAlias, removeVersionedAlias, getConfigSymlinkVersion, ensureClaudeInsideSymlink } from './shims.js';
35
35
  import { importInstallScriptBinary } from './import.js';
36
36
  import { IS_WINDOWS, composeWin32CommandLine } from './platform/index.js';
@@ -2511,20 +2511,23 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
2511
2511
  // For Claude/Codex: uses CLI commands (claude mcp add, codex mcp add)
2512
2512
  // For others: edits config files directly
2513
2513
  //
2514
- // Mirror the hooks defense: exclude project-scoped MCPs from the sync. An
2515
- // MCP server is an executable invoked under the agent's authority, so a
2516
- // cloned public repo's .agents/mcp/foo.yaml could install an arbitrary
2517
- // command. We pre-compute the set of project-scoped names and drop them
2518
- // before handing the list to installMcpServers. (The deeper helper-side
2519
- // dedup in lib/mcp.ts still lets a project entry shadow a same-named
2520
- // user entry, so name-collision shadowing is not fully closed here
2521
- // tracked separately for a follow-up in lib/mcp.ts.)
2522
- const projectScopedMcpNames = new Set(getScopedMcpResources(cwd).filter(r => r.scope === 'project').map(r => r.name));
2514
+ // Mirror the hooks defense: an MCP server is an executable invoked under the
2515
+ // agent's authority, so a cloned public repo's .agents/mcp/foo.yaml could
2516
+ // install an arbitrary command (RUSH-1776). Project-scoped MCPs are UNTRUSTED
2517
+ // by default drop them before handing the list to installMcpServers unless
2518
+ // the user has explicitly trusted this project (`agents mcp trust`). The
2519
+ // register/spawn choke in lib/mcp.ts (getMcpServersByName) enforces the same
2520
+ // trust gate and drops untrusted project entries before dedup, so a project
2521
+ // entry can no longer shadow a same-named user entry either.
2522
+ const projectMcpTrusted = projectAgentsDir ? isProjectMcpTrusted(projectAgentsDir) : false;
2523
+ const untrustedProjectMcpNames = new Set(projectMcpTrusted
2524
+ ? []
2525
+ : getScopedMcpResources(cwd).filter(r => r.scope === 'project').map(r => r.name));
2523
2526
  const mcpWriter = getWriter('mcp', agent);
2524
2527
  const mcpToSyncAll = selection
2525
2528
  ? resolveSelection(selection.mcp, available.mcp)
2526
2529
  : (mcpWriter ? available.mcp : []);
2527
- const mcpToSync = mcpToSyncAll.filter(n => !projectScopedMcpNames.has(n));
2530
+ const mcpToSync = mcpToSyncAll.filter(n => !untrustedProjectMcpNames.has(n));
2528
2531
  if (mcpToSync.length > 0 && mcpWriter) {
2529
2532
  const r = mcpWriter.write({ version, versionHome, selection: mcpToSync, cwd });
2530
2533
  result.mcp = r.synced;
@@ -240,6 +240,26 @@ export declare function resolveAllowedSubagents(available: string[], allowedAgen
240
240
  allowedStems: string[];
241
241
  missing: string[];
242
242
  };
243
+ /** The Claude tool an orchestrator uses to dispatch subagents. Must stay in a
244
+ * workflow's `--tools` allowlist whenever the workflow ships dispatchable
245
+ * subagents, or the orchestrator has no way to reach them. */
246
+ export declare const SUBAGENT_DISPATCH_TOOL = "Task";
247
+ /**
248
+ * Keep the subagent-dispatch tool (`Task`) in a `tools:`-restricted workflow's
249
+ * allowlist when the workflow actually ships subagents to dispatch.
250
+ *
251
+ * A WORKFLOW.md `tools:` list becomes Claude's `--tools` set, which *restricts*
252
+ * the available built-ins. An orchestrator whose `subagents/` files were just
253
+ * copied into the shared agents dir but whose `tools:` omits `Task` cannot
254
+ * reach any of them — it silently no-ops (observed: the run emits only "I'll
255
+ * wait for the completion notification" and exits). Omitting `Task` while
256
+ * shipping a `subagents/` dir is always an authoring miss, so we re-add it at
257
+ * the source rather than relying on every workflow author to remember.
258
+ *
259
+ * Returns `tools` unchanged when the workflow has no subagents or already lists
260
+ * `Task`; otherwise appends `Task`.
261
+ */
262
+ export declare function ensureSubagentDispatchTool(tools: string[], hasSubagents: boolean): string[];
243
263
  /**
244
264
  * Prune stale workflow-managed subagent files from the shared per-agent agents
245
265
  * dir before a scoped run writes the permitted set (issue #401, follow-up to
@@ -278,6 +278,30 @@ export function resolveAllowedSubagents(available, allowedAgents) {
278
278
  missing: allowedAgents.filter(a => !present.has(a)),
279
279
  };
280
280
  }
281
+ /** The Claude tool an orchestrator uses to dispatch subagents. Must stay in a
282
+ * workflow's `--tools` allowlist whenever the workflow ships dispatchable
283
+ * subagents, or the orchestrator has no way to reach them. */
284
+ export const SUBAGENT_DISPATCH_TOOL = 'Task';
285
+ /**
286
+ * Keep the subagent-dispatch tool (`Task`) in a `tools:`-restricted workflow's
287
+ * allowlist when the workflow actually ships subagents to dispatch.
288
+ *
289
+ * A WORKFLOW.md `tools:` list becomes Claude's `--tools` set, which *restricts*
290
+ * the available built-ins. An orchestrator whose `subagents/` files were just
291
+ * copied into the shared agents dir but whose `tools:` omits `Task` cannot
292
+ * reach any of them — it silently no-ops (observed: the run emits only "I'll
293
+ * wait for the completion notification" and exits). Omitting `Task` while
294
+ * shipping a `subagents/` dir is always an authoring miss, so we re-add it at
295
+ * the source rather than relying on every workflow author to remember.
296
+ *
297
+ * Returns `tools` unchanged when the workflow has no subagents or already lists
298
+ * `Task`; otherwise appends `Task`.
299
+ */
300
+ export function ensureSubagentDispatchTool(tools, hasSubagents) {
301
+ if (!hasSubagents || tools.includes(SUBAGENT_DISPATCH_TOOL))
302
+ return tools;
303
+ return [...tools, SUBAGENT_DISPATCH_TOOL];
304
+ }
281
305
  /**
282
306
  * Prune stale workflow-managed subagent files from the shared per-agent agents
283
307
  * dir before a scoped run writes the permitted set (issue #401, follow-up to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.20.64",
3
+ "version": "1.20.66",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -51,6 +51,7 @@
51
51
  "prepack": "cp ../../README.md README.md && scripts/verify-keychain-helper.sh && scripts/verify-menubar-helper.sh && scripts/verify-cli-binary.sh",
52
52
  "postinstall": "node scripts/postinstall.js",
53
53
  "dev": "tsx src/index.ts",
54
+ "changelog": "bun scripts/gen-changelog.ts",
54
55
  "start": "node dist/index.js",
55
56
  "test": "node ./node_modules/vitest/vitest.mjs run",
56
57
  "test:remote": "scripts/sandbox.sh 'bun install && bun run build && bun run test'",