@phnx-labs/agents-cli 1.20.68 → 1.20.70

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 (53) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +30 -7
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/computer.d.ts +26 -0
  5. package/dist/commands/computer.js +173 -149
  6. package/dist/commands/doctor.js +5 -0
  7. package/dist/commands/exec.d.ts +18 -0
  8. package/dist/commands/exec.js +88 -6
  9. package/dist/commands/fork.d.ts +9 -0
  10. package/dist/commands/fork.js +67 -0
  11. package/dist/commands/run-account-picker.d.ts +14 -0
  12. package/dist/commands/run-account-picker.js +131 -0
  13. package/dist/commands/setup-browser.d.ts +18 -0
  14. package/dist/commands/setup-browser.js +142 -0
  15. package/dist/commands/setup-computer.d.ts +19 -0
  16. package/dist/commands/setup-computer.js +133 -0
  17. package/dist/commands/setup-share.d.ts +17 -0
  18. package/dist/commands/setup-share.js +85 -0
  19. package/dist/commands/setup.d.ts +1 -1
  20. package/dist/commands/setup.js +58 -1
  21. package/dist/commands/share.d.ts +15 -0
  22. package/dist/commands/share.js +10 -4
  23. package/dist/commands/ssh.js +202 -9
  24. package/dist/commands/view.d.ts +4 -14
  25. package/dist/commands/view.js +32 -30
  26. package/dist/index.js +2 -1
  27. package/dist/lib/agents.d.ts +10 -0
  28. package/dist/lib/agents.js +32 -0
  29. package/dist/lib/auth-health.d.ts +141 -0
  30. package/dist/lib/auth-health.js +277 -0
  31. package/dist/lib/browser/chrome.d.ts +9 -0
  32. package/dist/lib/browser/chrome.js +22 -0
  33. package/dist/lib/computer/download.d.ts +54 -0
  34. package/dist/lib/computer/download.js +146 -0
  35. package/dist/lib/computer-rpc.js +9 -3
  36. package/dist/lib/daemon.js +38 -0
  37. package/dist/lib/devices/connect.d.ts +15 -0
  38. package/dist/lib/devices/connect.js +17 -5
  39. package/dist/lib/devices/health-report.d.ts +11 -0
  40. package/dist/lib/devices/health-report.js +73 -4
  41. package/dist/lib/devices/stats-cache.d.ts +36 -0
  42. package/dist/lib/devices/stats-cache.js +115 -0
  43. package/dist/lib/devices/terminfo.d.ts +44 -0
  44. package/dist/lib/devices/terminfo.js +167 -0
  45. package/dist/lib/rotate.d.ts +12 -8
  46. package/dist/lib/rotate.js +22 -23
  47. package/dist/lib/session/fork.d.ts +32 -0
  48. package/dist/lib/session/fork.js +101 -0
  49. package/dist/lib/startup/command-registry.d.ts +1 -0
  50. package/dist/lib/startup/command-registry.js +2 -0
  51. package/dist/lib/usage.d.ts +23 -0
  52. package/dist/lib/usage.js +84 -0
  53. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,71 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.20.70
4
+
5
+ - **Fix `agents setup computer` / `agents computer setup` refusing to install a
6
+ valid downloaded helper.** The signature check read `codesign -dv` from stdout,
7
+ but that command writes its details to **stderr** on success — so the Team-ID
8
+ check saw an empty string, found no `TeamIdentifier`, and rejected every
9
+ validly-signed, notarized helper with "signed by unexpected Team (none)". It now
10
+ reads both streams via `spawnSync`. Verified end-to-end against the real
11
+ published `v1.20.69` release asset (download → sha256 → extract → codesign +
12
+ Team `2HTP252L87` + `spctl` notarization → install). Source:
13
+ `apps/cli/src/lib/computer/download.ts`.
14
+
15
+ - **The bundled macOS menu-bar helper is now a true universal binary on
16
+ Xcode-less release hosts.** `menubar/scripts/build.sh release` used
17
+ `swift build --arch arm64 --arch x86_64` (needs Xcode's xcbuild) and, on a
18
+ Command-Line-Tools-only host, silently fell back to a **single-arch** build —
19
+ shipping an arm64-only `MenubarHelper.app` in the tarball that could not run on
20
+ Intel Macs. It now builds each slice via `--triple` and `lipo`s them into one
21
+ universal binary, matching the computer helper. Source:
22
+ `apps/cli/menubar/scripts/build.sh`.
23
+
24
+ ## 1.20.69
25
+
26
+ - **Choose a safe account with `agents run <agent>@`.** A trailing `@` opens a
27
+ per-run picker showing each installed version's account identity, login state,
28
+ plan, and available session/weekly/monthly capacity. Logged-out, rate-limited,
29
+ and out-of-credit accounts remain visible but disabled; signed-in accounts
30
+ without quota data remain selectable and say `limits unavailable`. Source:
31
+ `apps/cli/src/commands/run-account-picker.ts`,
32
+ `apps/cli/src/commands/exec.ts`, `apps/cli/src/lib/rotate.ts`.
33
+
34
+ - **The macOS `agents computer` helper now ships as a signed + notarized release asset,
35
+ downloaded on demand.** A fresh `npm i -g @phnx-labs/agents-cli` no longer needs to build
36
+ the Swift helper from source: `agents computer setup` / `agents setup computer` fetch
37
+ `ComputerHelper.app.zip` from the matching `v<version>` GitHub release, verify it against
38
+ the published `.sha256`, and re-check the code signature (Developer ID Team `2HTP252L87`)
39
+ and notarization (`spctl --assess`) before it is ever copied to /Applications — mirroring
40
+ the Windows helper's distribution. The download cache is never a trusted resolver source;
41
+ a cached bundle is only ever read back through the verifying downloader. The helper is
42
+ version-stamped at build time and the release pipeline publishes the asset automatically.
43
+ Source: `apps/cli/src/lib/computer/download.ts`, `apps/cli/src/lib/computer-rpc.ts`,
44
+ `apps/cli/src/commands/computer.ts`, `native/computer-mac/scripts/build.sh`,
45
+ `apps/cli/scripts/publish-computer-helper-mac.sh`, `apps/cli/scripts/release.sh`.
46
+
47
+ - **Live fleet auth health (`agents fleet ping`) + `agents view` chip (#1285).** New `agents fleet ping` completes a real authenticated request for every agent account across the fleet — the ground truth the local "signed in" flag can't give (it can't tell a revoked-but-unexpired token from a good one). Claude/Kimi/Droid are network-verified; Codex/Grok are best-effort. `agents view` now shows a live-status chip per version, read from the shared cache the ping writes. The probe hits the usage endpoint (no model tokens, no session created). Source: `apps/cli/src/lib/auth-health.ts`, `apps/cli/src/commands/ssh.ts`, `apps/cli/src/commands/view.ts`.
48
+
49
+ - **`agents fork` — branch a session into a new independent copy.** Copies a Claude
50
+ session transcript to a fresh session id (rewriting only the `sessionId` field so the
51
+ per-message uuid chain stays intact) beside the original, then registers it so it
52
+ resumes independently from the same cwd and version — the original is left untouched.
53
+ `--name` labels the fork. Source: `apps/cli/src/lib/session/fork.ts`,
54
+ `apps/cli/src/commands/fork.ts`.
55
+
56
+ - **`agents setup` is now a capability hub with guided `browser` / `computer` / `share`
57
+ subcommands.** Bare `agents setup` still clones the system repo and imports unmanaged
58
+ agents, but on a TTY it now also offers to set up the optional capabilities a fresh
59
+ machine needs. Each is also runnable on its own and is idempotent (re-run to change
60
+ settings): `agents setup browser` detects an installed Chromium-family browser and
61
+ creates/points the `default` profile; `agents setup share` provisions or joins a
62
+ Cloudflare share endpoint (reusing `agents share setup`/`join`); `agents setup computer`
63
+ installs the macOS helper and walks you through the Accessibility + Screen-Recording
64
+ grants — opening the exact System Settings panes and polling until trust lands. The
65
+ existing `agents share setup` / `agents computer setup` remain for scripted use. Source:
66
+ `apps/cli/src/commands/setup.ts`, `setup-browser.ts`, `setup-computer.ts`,
67
+ `setup-share.ts`, `apps/cli/src/lib/browser/chrome.ts`, `apps/cli/src/commands/share.ts`.
68
+
3
69
  ## 1.20.68
4
70
 
5
71
  - **MCP resource handler now syncs project-level agent configs alongside user-level configs (RUSH-671).** `McpHandler.sync` previously wrote resolved MCP servers only to the version-home (user-level) config path. It now also writes project-layer MCP servers to each agent CLI's project-level config path (e.g., `.mcp.json` for Claude, `.codex/config.toml` for Codex) so agent CLIs can discover project-scoped MCPs natively. User-level sync is unchanged. Source: `apps/cli/src/lib/resources/mcp.ts`, `apps/cli/src/lib/agents.ts` (`getProjectMcpConfigPath` exported), `apps/cli/src/lib/resources/mcp.test.ts`.
package/README.md CHANGED
@@ -151,10 +151,18 @@ agents run claude "refactor auth module" --mode edit --fallback codex,gemini
151
151
  ```bash
152
152
  # Picks the signed-in account you haven't used recently.
153
153
  agents run claude "summarize recent commits" --strategy balanced
154
+
155
+ # Or choose one account/version interactively for only this run.
156
+ agents run claude@
157
+ agents run codex@ "review this branch"
154
158
  ```
155
159
 
156
160
  `--strategy balanced` spreads work across available versions of the same agent -- useful when you have multiple accounts and want to avoid burning through one.
157
161
 
162
+ A trailing `@` opens an account picker before either an interactive or prompt-based run. Each installed version shows its account identity, exact version, login state, plan, and every available session, weekly, or monthly limit. Logged-out, rate-limited, and out-of-credit accounts remain visible with the reason they cannot be selected; signed-in accounts whose provider does not expose quota data stay selectable and say `limits unavailable`. The choice pins only that run and does not change your default version.
163
+
164
+ Account selection is available for Claude, Codex, Gemini, Grok, Antigravity, Kimi, Droid, and OpenCode. It requires a terminal and cannot be combined with `--resume`, `--strategy`/`--balanced`, `--lease`, or `--host`/`--device`; profiles and workflows must use their concrete host agent instead.
165
+
158
166
  ### Chain agents
159
167
 
160
168
  ```bash
@@ -424,17 +432,21 @@ agents sync --host gpu-box # make the remote machine current
424
432
  agents doctor --devices # readiness matrix for every registered device
425
433
  agents doctor --devices --json # machine-readable fleet readiness
426
434
  agents doctor --device mac-mini # same matrix, scoped to one device
427
- agents fleet status # warnings rollup + health/sync/version matrix
435
+ agents fleet status # warnings rollup + health/sync/version/Auth matrix (cache-first)
436
+ agents fleet status --live # force a live resource probe (alias of --refresh)
428
437
  agents fleet status --json --strict # scriptable fleet health gate
429
438
  agents check --devices # CI drift gate across every registered device
430
439
 
431
440
  # Your Tailscale fleet, auto-discovered
432
441
  agents devices sync # ingest `tailscale status`
433
- agents devices list # fleet + live headroom: load, mem, idle/busy — which box has room
442
+ agents devices list # fleet + headroom: load, mem, idle/busy — which box has room (cache-first)
443
+ agents devices list --live # force a live probe of every device (alias of --refresh)
434
444
  agents devices list --full # add per-device cores and free/total RAM
435
445
  agents devices list --no-stats # instant: names/addresses only, skip the probe
436
446
  agents ssh mac-mini # hardened SSH: fails fast if offline,
437
- # PowerShell on Windows, password-from-Keychain
447
+ # PowerShell on Windows, password-from-Keychain,
448
+ # auto-syncs your terminfo (Ghostty/kitty/…) so
449
+ # backspace, colors & clear work on the remote
438
450
  agents hosts list # devices show up here too (one host pool)
439
451
  agents hosts add mac-mini --cap gpu # tag a device for capability routing (--host gpu)
440
452
 
@@ -443,11 +455,22 @@ agents cloud run "nightly benchmark" --host gpu-box --agent claude # task in c
443
455
  agents routines add nightly -s "0 2 * * *" -a claude -p "run the sweep" --run-on gpu-box
444
456
  ```
445
457
 
446
- `agents devices list` probes every reachable box in parallel (bounded timeout, so a
447
- slow node degrades to `—` instead of hanging) and shows normalized load, memory
448
- pressure, and an idle/light/busy/loaded headroom badge, plus a fleet-capacity summary
458
+ `agents devices list` shows normalized load, memory pressure, and an
459
+ idle/light/busy/loaded headroom badge, plus a fleet-capacity summary
449
460
  (`164 cores · 421G free / 518G RAM`). It answers "which machine has room right now?" —
450
- the utilization signal the teammate scheduler doesn't yet see.
461
+ the utilization signal the teammate scheduler doesn't yet see. It's **cache-first**:
462
+ reads serve instantly from a stats cache the daemon warms (~every 3 min), probing only
463
+ this machine locally plus any device missing from the cache; pass `--refresh` (or the
464
+ shorter `--live`) to force a full live probe of every box. Cache-served output notes its
465
+ age (`updated 2m ago — pass --refresh (--live) for a live probe`).
466
+
467
+ `agents fleet status` adds an **Auth column** — which agent accounts are actually
468
+ logged in, per device, read from the auth-health cache (no network). Four buckets keep
469
+ it from crying wolf: `●live` (verified), `·present` (signed in but the agent has no
470
+ live-probe endpoint — e.g. codex/grok — benign), `◐degraded` (soft/self-healing:
471
+ expired-but-refreshing, rate-limited), and `○revoked` (server rejected — re-login now).
472
+ Only `○` means a real re-login is needed. Run `agents fleet ping` to force a live
473
+ re-verification across the fleet.
451
474
 
452
475
  **Hosts** (`agents hosts`) are git-synced dispatch targets in `agents.yaml`; **devices** (`agents devices`) are your Tailscale machines in a local registry. Both ride SSH and feed one host pool: devices appear in `agents hosts list` and capability routing without a second enrollment. On `--host` runs every `agents run` option is either forwarded (`--effort --env --timeout --loop …`), rejected loud (`--secrets` never crosses SSH implicitly), or consumed locally — nothing silently drops. See [docs/00-concepts.md](apps/cli/docs/00-concepts.md#devices--hosts).
453
476
 
package/dist/bin/agents CHANGED
Binary file
@@ -38,5 +38,31 @@ export declare function reconcileScreenshotExt(outPath: string, buf: Buffer): {
38
38
  export declare function registerComputerCommand(program: Command): void;
39
39
  export declare function registerComputerSubcommands(program: Command): void;
40
40
  export declare function buildRestartTaskScript(taskName: string, exeName: string): string;
41
+ /**
42
+ * Install the macOS helper locally: resolve (or download + verify) the signed,
43
+ * notarized .app, copy it to /Applications, verify the destination signature,
44
+ * and write the LaunchAgent plist (inactive — activation is a separate opt-in
45
+ * step via `start`). Throws on any failure. Shared by `agents computer setup`
46
+ * and the unified `agents setup computer` wizard so there is one install path.
47
+ */
48
+ export declare function installComputerHelperMacLocal(): Promise<{
49
+ appDest: string;
50
+ plistPath: string;
51
+ }>;
52
+ /**
53
+ * Activate the local helper daemon via launchd (render policy + peers, bootout
54
+ * → bootstrap → kickstart, wait for the socket, probe AX trust). Throws on any
55
+ * failure. Returns the trust status so callers can guide the permission grant.
56
+ * Shared by `agents computer start` and the `agents setup computer` wizard.
57
+ */
58
+ export declare function activateComputerHelperMacLocal(): Promise<{
59
+ trusted: boolean;
60
+ socketPath: string;
61
+ logPath: string;
62
+ }>;
63
+ /** Probe the running daemon's Accessibility trust status without re-activating.
64
+ * Returns false (never throws) if the socket is down or the RPC errors — used to
65
+ * poll while the user grants permissions in System Settings. */
66
+ export declare function probeComputerTrust(): Promise<boolean>;
41
67
  export { resolveHelperExec as resolveHelperPath };
42
68
  export { resolveSocketPath };
@@ -441,6 +441,172 @@ const HELPER_BUNDLE_ID = 'com.phnx-labs.computer-helper';
441
441
  const HELPER_APP_NAME = 'Computer Helper.app';
442
442
  const HELPER_APP_DEST = `/Applications/${HELPER_APP_NAME}`;
443
443
  const HELPER_LABEL = HELPER_BUNDLE_ID;
444
+ /**
445
+ * Install the macOS helper locally: resolve (or download + verify) the signed,
446
+ * notarized .app, copy it to /Applications, verify the destination signature,
447
+ * and write the LaunchAgent plist (inactive — activation is a separate opt-in
448
+ * step via `start`). Throws on any failure. Shared by `agents computer setup`
449
+ * and the unified `agents setup computer` wizard so there is one install path.
450
+ */
451
+ export async function installComputerHelperMacLocal() {
452
+ let srcApp = resolveHelperApp();
453
+ if (!srcApp || !fs.existsSync(srcApp)) {
454
+ // No local build / bundled copy (the normal case on an npm-installed CLI).
455
+ // Fetch the signed + notarized helper release asset for this CLI version;
456
+ // it is sha256- and signature-verified before we touch /Applications.
457
+ const { ensureMacHelperApp } = await import('../lib/computer/download.js');
458
+ srcApp = await ensureMacHelperApp();
459
+ console.log(`helper: ${srcApp} (downloaded + verified)`);
460
+ }
461
+ const socketPath = resolveSocketPath();
462
+ const logPath = resolveLogPath();
463
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${HELPER_LABEL}.plist`);
464
+ console.log(`source: ${srcApp}`);
465
+ console.log(`dest: ${HELPER_APP_DEST}`);
466
+ // 1. Copy to /Applications/ via ditto (preserves xattrs + codesign metadata).
467
+ if (fs.existsSync(HELPER_APP_DEST)) {
468
+ try {
469
+ fs.rmSync(HELPER_APP_DEST, { recursive: true, force: true });
470
+ }
471
+ catch (err) {
472
+ throw new Error(`failed to remove prior install at ${HELPER_APP_DEST}: ${err.message}. try: sudo rm -rf "${HELPER_APP_DEST}"`);
473
+ }
474
+ }
475
+ try {
476
+ execFileSync('/usr/bin/ditto', [srcApp, HELPER_APP_DEST], { stdio: 'inherit' });
477
+ }
478
+ catch (err) {
479
+ throw new Error(`ditto copy failed: ${err.message}`);
480
+ }
481
+ console.log(`copied to ${HELPER_APP_DEST}`);
482
+ // 2. Verify codesign on the destination — TCC needs a valid signature.
483
+ try {
484
+ execFileSync('/usr/bin/codesign', ['--verify', '--deep', '--strict', HELPER_APP_DEST], { stdio: 'inherit' });
485
+ console.log('codesign verify: OK');
486
+ }
487
+ catch {
488
+ throw new Error(`codesign verify FAILED for ${HELPER_APP_DEST}. The destination .app is unsigned or its signature was stripped.`);
489
+ }
490
+ // 3. Ensure socket + log parent dirs exist.
491
+ fs.mkdirSync(path.dirname(socketPath), { recursive: true });
492
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
493
+ // 4. Write the LaunchAgent plist but DO NOT bootstrap it (opt-in via `start`).
494
+ const execInsideApp = path.join(HELPER_APP_DEST, 'Contents', 'MacOS', 'ComputerHelper');
495
+ const plistContent = renderLaunchAgentPlist({ label: HELPER_LABEL, exec: execInsideApp, socketPath, logPath });
496
+ fs.mkdirSync(path.dirname(plistPath), { recursive: true });
497
+ fs.writeFileSync(plistPath, plistContent);
498
+ console.log(`wrote plist: ${plistPath} (NOT activated)`);
499
+ return { appDest: HELPER_APP_DEST, plistPath };
500
+ }
501
+ /**
502
+ * Activate the local helper daemon via launchd (render policy + peers, bootout
503
+ * → bootstrap → kickstart, wait for the socket, probe AX trust). Throws on any
504
+ * failure. Returns the trust status so callers can guide the permission grant.
505
+ * Shared by `agents computer start` and the `agents setup computer` wizard.
506
+ */
507
+ export async function activateComputerHelperMacLocal() {
508
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${HELPER_LABEL}.plist`);
509
+ const socketPath = resolveSocketPath();
510
+ const logPath = resolveLogPath();
511
+ if (!fs.existsSync(plistPath))
512
+ throw new Error(`plist not found at ${plistPath}. run: agents computer setup`);
513
+ if (!fs.existsSync(HELPER_APP_DEST))
514
+ throw new Error(`helper app not found at ${HELPER_APP_DEST}. run: agents computer setup`);
515
+ const uid = process.getuid?.();
516
+ if (typeof uid !== 'number')
517
+ throw new Error('cannot resolve uid');
518
+ const domain = `gui/${uid}`;
519
+ // Render the policy file BEFORE bootstrap so the daemon reads a fresh allow
520
+ // list at startup (fail-safe: missing/unparseable → everything denied).
521
+ const allowed = loadComputerAllowList();
522
+ writeComputerPolicy(allowed);
523
+ console.log(`policy: ${allowed.length} app${allowed.length === 1 ? '' : 's'} allowed (${resolvePolicyPath()})`);
524
+ if (allowed.length > 0) {
525
+ const preview = allowed.slice(0, 5).join(', ');
526
+ const more = allowed.length > 5 ? ` (+${allowed.length - 5} more)` : '';
527
+ console.log(` ${preview}${more}`);
528
+ }
529
+ else {
530
+ console.log(` (no Computer(...) patterns found — everything will be denied)`);
531
+ console.log(` add to ~/.agents/permissions/groups/<name>.yaml under allow:`);
532
+ console.log(` - "Computer(com.apple.finder)"`);
533
+ }
534
+ // Peer-auth allow list — which caller executables may connect to the socket.
535
+ const callers = loadDefaultPeers();
536
+ writeComputerPeers(callers);
537
+ console.log(`peers: ${callers.length} caller${callers.length === 1 ? '' : 's'} allowed (${resolvePeersPath()})`);
538
+ for (const p of callers)
539
+ console.log(` ${p}`);
540
+ // Bootout first to clear any prior registration (best-effort).
541
+ try {
542
+ execFileSync('/bin/launchctl', ['bootout', domain, plistPath], { stdio: 'pipe' });
543
+ }
544
+ catch {
545
+ // expected when not previously loaded
546
+ }
547
+ try {
548
+ execFileSync('/bin/launchctl', ['bootstrap', domain, plistPath], { stdio: 'pipe' });
549
+ }
550
+ catch (err) {
551
+ throw new Error(`launchctl bootstrap failed: ${err.message}`);
552
+ }
553
+ // Force restart so we pick up the latest binary.
554
+ try {
555
+ execFileSync('/bin/launchctl', ['kickstart', '-k', `${domain}/${HELPER_LABEL}`], { stdio: 'pipe' });
556
+ }
557
+ catch (err) {
558
+ throw new Error(`launchctl kickstart failed: ${err.message}`);
559
+ }
560
+ // Wait up to 5s for the socket.
561
+ const deadline = Date.now() + 5000;
562
+ while (Date.now() < deadline) {
563
+ if (fs.existsSync(socketPath))
564
+ break;
565
+ await sleep(100);
566
+ }
567
+ if (!fs.existsSync(socketPath)) {
568
+ throw new Error(`socket did not appear at ${socketPath} within 5s. check ${logPath} for helper startup errors`);
569
+ }
570
+ // Probe trust through the socket.
571
+ let trusted = false;
572
+ let trustStr = 'unknown';
573
+ try {
574
+ const client = openComputerClient();
575
+ try {
576
+ const r = await client.call('trust_status');
577
+ trusted = Boolean(r.result?.trusted);
578
+ trustStr = r.error ? `error (${r.error.code})` : (trusted ? 'granted' : 'denied');
579
+ }
580
+ finally {
581
+ await client.close();
582
+ }
583
+ }
584
+ catch (err) {
585
+ trustStr = `error (${err.message})`;
586
+ }
587
+ console.log(`daemon: running`);
588
+ console.log(`socket: ${socketPath}`);
589
+ console.log(`trust: ${trustStr}`);
590
+ return { trusted, socketPath, logPath };
591
+ }
592
+ /** Probe the running daemon's Accessibility trust status without re-activating.
593
+ * Returns false (never throws) if the socket is down or the RPC errors — used to
594
+ * poll while the user grants permissions in System Settings. */
595
+ export async function probeComputerTrust() {
596
+ try {
597
+ const client = openComputerClient();
598
+ try {
599
+ const r = await client.call('trust_status');
600
+ return Boolean(r.result?.trusted);
601
+ }
602
+ finally {
603
+ await client.close();
604
+ }
605
+ }
606
+ catch {
607
+ return false;
608
+ }
609
+ }
444
610
  function registerSetupCommand(program) {
445
611
  program
446
612
  .command('setup')
@@ -462,66 +628,14 @@ function registerSetupCommand(program) {
462
628
  }
463
629
  return;
464
630
  }
465
- const srcApp = resolveHelperApp();
466
- if (!srcApp || !fs.existsSync(srcApp)) {
467
- console.error('helper not built. Run: ./native/computer-mac/scripts/build.sh debug');
468
- process.exit(1);
469
- }
470
- const home = os.homedir();
471
- const socketPath = resolveSocketPath();
472
- const logPath = resolveLogPath();
473
- const plistPath = path.join(home, 'Library', 'LaunchAgents', `${HELPER_LABEL}.plist`);
474
- console.log(`source: ${srcApp}`);
475
- console.log(`dest: ${HELPER_APP_DEST}`);
476
- // 1. Copy to /Applications/. Use ditto to preserve xattrs (Gatekeeper
477
- // provenance + codesign metadata). Wipe any prior install first.
478
- if (fs.existsSync(HELPER_APP_DEST)) {
479
- try {
480
- fs.rmSync(HELPER_APP_DEST, { recursive: true, force: true });
481
- console.log(`removed prior install`);
482
- }
483
- catch (err) {
484
- console.error(`failed to remove prior install at ${HELPER_APP_DEST}: ${err.message}`);
485
- console.error('try: sudo rm -rf "' + HELPER_APP_DEST + '"');
486
- process.exit(1);
487
- }
488
- }
631
+ let plistPath;
489
632
  try {
490
- execFileSync('/usr/bin/ditto', [srcApp, HELPER_APP_DEST], { stdio: 'inherit' });
633
+ ({ plistPath } = await installComputerHelperMacLocal());
491
634
  }
492
635
  catch (err) {
493
- console.error(`ditto copy failed: ${err.message}`);
494
- process.exit(1);
495
- }
496
- console.log(`copied to ${HELPER_APP_DEST}`);
497
- // 2. Verify codesign on the destination. Fail loud if the copy
498
- // somehow stripped the signature — TCC needs a valid signature.
499
- try {
500
- execFileSync('/usr/bin/codesign', ['--verify', '--deep', '--strict', HELPER_APP_DEST], { stdio: 'inherit' });
501
- console.log('codesign verify: OK');
502
- }
503
- catch {
504
- console.error('codesign verify FAILED. The destination .app is unsigned or its signature was stripped.');
505
- console.error('rebuild the helper with a Developer ID cert: ./native/computer-mac/scripts/build.sh release');
636
+ console.error(`error: ${err.message}`);
506
637
  process.exit(1);
507
638
  }
508
- // 3. Ensure socket + log parent dirs exist.
509
- fs.mkdirSync(path.dirname(socketPath), { recursive: true });
510
- fs.mkdirSync(path.dirname(logPath), { recursive: true });
511
- // 4. Write the LaunchAgent plist but DO NOT bootstrap it. The user
512
- // explicitly opts into running the daemon via `agents computer start`.
513
- // Screen Recording + Accessibility are scary permissions; we don't
514
- // want an always-on listener that can drive any app the user could.
515
- const execInsideApp = path.join(HELPER_APP_DEST, 'Contents', 'MacOS', 'ComputerHelper');
516
- const plistContent = renderLaunchAgentPlist({
517
- label: HELPER_LABEL,
518
- exec: execInsideApp,
519
- socketPath,
520
- logPath,
521
- });
522
- fs.mkdirSync(path.dirname(plistPath), { recursive: true });
523
- fs.writeFileSync(plistPath, plistContent);
524
- console.log(`wrote plist: ${plistPath} (NOT activated)`);
525
639
  console.log('');
526
640
  console.log('Helper installed (inactive).');
527
641
  console.log('');
@@ -563,105 +677,15 @@ function registerStartCommand(program) {
563
677
  }
564
678
  return;
565
679
  }
566
- const home = os.homedir();
567
- const plistPath = path.join(home, 'Library', 'LaunchAgents', `${HELPER_LABEL}.plist`);
568
- const socketPath = resolveSocketPath();
569
- const logPath = resolveLogPath();
570
- if (!fs.existsSync(plistPath)) {
571
- console.error(`plist not found at ${plistPath}`);
572
- console.error('run: agents computer setup');
573
- process.exit(1);
574
- }
575
- if (!fs.existsSync(HELPER_APP_DEST)) {
576
- console.error(`helper app not found at ${HELPER_APP_DEST}`);
577
- console.error('run: agents computer setup');
578
- process.exit(1);
579
- }
580
- const uid = process.getuid?.();
581
- if (typeof uid !== 'number') {
582
- console.error('cannot resolve uid');
583
- process.exit(1);
584
- }
585
- const domain = `gui/${uid}`;
586
- // Render the policy file BEFORE launchctl bootstrap so the daemon
587
- // reads a fresh allow list at startup. The helper falls back to an
588
- // empty allow list (everything denied) if this file is missing or
589
- // unparseable — fail-safe.
590
- const allowed = loadComputerAllowList();
591
- writeComputerPolicy(allowed);
592
- console.log(`policy: ${allowed.length} app${allowed.length === 1 ? '' : 's'} allowed (${resolvePolicyPath()})`);
593
- if (allowed.length > 0) {
594
- const preview = allowed.slice(0, 5).join(', ');
595
- const more = allowed.length > 5 ? ` (+${allowed.length - 5} more)` : '';
596
- console.log(` ${preview}${more}`);
597
- }
598
- else {
599
- console.log(` (no Computer(...) patterns found — everything will be denied)`);
600
- console.log(` add to ~/.agents/permissions/groups/<name>.yaml under allow:`);
601
- console.log(` - "Computer(com.apple.finder)"`);
602
- }
603
- // Peer-auth allow list — which caller executables may connect to
604
- // the socket. Default: this CLI's Node binary, plus Rush.app if
605
- // installed. A `nc -U socket` from a malicious npm postinstall has
606
- // a different exec path and gets refused at accept().
607
- const callers = loadDefaultPeers();
608
- writeComputerPeers(callers);
609
- console.log(`peers: ${callers.length} caller${callers.length === 1 ? '' : 's'} allowed (${resolvePeersPath()})`);
610
- for (const p of callers)
611
- console.log(` ${p}`);
612
- // Bootout first to clear any prior registration. Best-effort.
613
- try {
614
- execFileSync('/bin/launchctl', ['bootout', domain, plistPath], { stdio: 'pipe' });
615
- }
616
- catch {
617
- // expected when not previously loaded
618
- }
619
- try {
620
- execFileSync('/bin/launchctl', ['bootstrap', domain, plistPath], { stdio: 'pipe' });
621
- }
622
- catch (err) {
623
- console.error(`launchctl bootstrap failed: ${err.message}`);
624
- process.exit(1);
625
- }
626
- // Force restart so we pick up the latest binary.
680
+ let trusted;
627
681
  try {
628
- execFileSync('/bin/launchctl', ['kickstart', '-k', `${domain}/${HELPER_LABEL}`], { stdio: 'pipe' });
682
+ ({ trusted } = await activateComputerHelperMacLocal());
629
683
  }
630
684
  catch (err) {
631
- console.error(`launchctl kickstart failed: ${err.message}`);
685
+ console.error(`error: ${err.message}`);
632
686
  process.exit(1);
633
687
  }
634
- // Wait up to 5s for the socket.
635
- const deadline = Date.now() + 5000;
636
- while (Date.now() < deadline) {
637
- if (fs.existsSync(socketPath))
638
- break;
639
- await sleep(100);
640
- }
641
- if (!fs.existsSync(socketPath)) {
642
- console.error(`socket did not appear at ${socketPath} within 5s`);
643
- console.error(`check ${logPath} for helper startup errors`);
644
- process.exit(1);
645
- }
646
- // Probe trust through the socket.
647
- let trustStr = 'unknown';
648
- try {
649
- const client = openComputerClient();
650
- try {
651
- const r = await client.call('trust_status');
652
- trustStr = r.error ? `error (${r.error.code})` : (r.result?.trusted ? 'granted' : 'denied');
653
- }
654
- finally {
655
- await client.close();
656
- }
657
- }
658
- catch (err) {
659
- trustStr = `error (${err.message})`;
660
- }
661
- console.log(`daemon: running`);
662
- console.log(`socket: ${socketPath}`);
663
- console.log(`trust: ${trustStr}`);
664
- if (trustStr === 'denied') {
688
+ if (!trusted) {
665
689
  console.log('');
666
690
  console.log('Grant Accessibility + Screen Recording to Computer Helper.app, then run `agents computer start` again.');
667
691
  }
@@ -14,6 +14,7 @@ import { getGlobalDefault, getVersionHomePath, isVersionInstalled, listInstalled
14
14
  import { loadManifest, isStale } from '../lib/staleness/index.js';
15
15
  import { diffVersionResources, DOCTOR_ALL_KINDS, } from '../lib/doctor-diff.js';
16
16
  import { checkSyncStatus, countOrphans } from '../lib/drift.js';
17
+ import { readAuthHealthCache, summarizeHostAuth } from '../lib/auth-health.js';
17
18
  import { unifiedDiff, colorizeUnifiedDiff } from '../lib/diff-text.js';
18
19
  import { listCliStatus } from '../lib/cli-resources.js';
19
20
  import { setHelpSections } from '../lib/help.js';
@@ -741,6 +742,10 @@ export function registerDoctorCommand(program) {
741
742
  console.log(JSON.stringify({
742
743
  clis,
743
744
  signIn,
745
+ // Cached auth-health rollup for THIS host — lets `agents fleet status`
746
+ // show a live-verified Auth column from the same fan-out it already
747
+ // runs, without a separate fleet-wide `fleet ping`.
748
+ auth: summarizeHostAuth(readAuthHealthCache(), machineId()),
744
749
  sync: syncRows,
745
750
  orphans: orphanRows,
746
751
  hostClis: {
@@ -8,6 +8,24 @@
8
8
  import { type Command } from 'commander';
9
9
  import type { ExecEffort } from '../lib/exec.js';
10
10
  import { type SshGResult } from '../lib/hosts/ssh-config.js';
11
+ export interface RunAccountPickerRequest {
12
+ requested: boolean;
13
+ normalizedAgentSpec: string;
14
+ valid: boolean;
15
+ }
16
+ /** Distinguish a terminal account-picker marker from an explicit @version pin. */
17
+ export declare function parseRunAccountPickerRequest(agentSpec: string): RunAccountPickerRequest;
18
+ /** Return every option whose routing semantics conflict with a local account choice. */
19
+ export declare function runAccountPickerConflicts(options: {
20
+ resume?: string | boolean;
21
+ strategy?: string;
22
+ balanced?: boolean;
23
+ lease?: string | boolean;
24
+ host?: string;
25
+ device?: string;
26
+ on?: string;
27
+ computer?: string;
28
+ }): string[];
11
29
  /** The host descriptor fields the `--copy-creds` security gate reads. */
12
30
  export interface CopyCredsGateHost {
13
31
  name: string;