@phnx-labs/agents-cli 1.22.102 → 1.22.104

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 (76) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +1 -1
  3. package/dist/bootstrap.js +15 -0
  4. package/dist/commands/accounts.js +2 -2
  5. package/dist/commands/computer.d.ts +100 -79
  6. package/dist/commands/computer.js +290 -830
  7. package/dist/commands/setup-computer.js +20 -1
  8. package/dist/commands/setup-secrets.d.ts +2 -2
  9. package/dist/commands/setup-secrets.js +1 -1
  10. package/dist/commands/view.js +4 -6
  11. package/dist/lib/account-catalog.d.ts +22 -1
  12. package/dist/lib/account-catalog.js +72 -38
  13. package/dist/lib/accounting/usage.js +18 -14
  14. package/dist/lib/agent-spec/agents.d.ts +1 -0
  15. package/dist/lib/agent-spec/agents.js +1 -1
  16. package/dist/lib/browser/drivers/ssh.js +1 -1
  17. package/dist/lib/computer/context.d.ts +83 -0
  18. package/dist/lib/computer/context.js +91 -0
  19. package/dist/lib/computer/policy.d.ts +46 -0
  20. package/dist/lib/computer/policy.js +160 -0
  21. package/dist/lib/computer/record.d.ts +38 -0
  22. package/dist/lib/computer/record.js +86 -0
  23. package/dist/lib/computer/sessions-list.js +6 -6
  24. package/dist/lib/computer-client.d.ts +150 -0
  25. package/dist/lib/computer-client.js +222 -0
  26. package/dist/lib/exec.js +35 -1
  27. package/dist/lib/harness/adapters/claude.d.ts +37 -0
  28. package/dist/lib/harness/adapters/claude.js +69 -0
  29. package/dist/lib/helper-download.d.ts +1 -1
  30. package/dist/lib/helper-download.js +1 -1
  31. package/dist/lib/helper-versions.d.ts +9 -4
  32. package/dist/lib/helper-versions.js +9 -9
  33. package/dist/lib/installations/shims.js +8 -4
  34. package/dist/lib/menubar/download-menubar.d.ts +2 -1
  35. package/dist/lib/menubar/download-menubar.js +2 -1
  36. package/dist/lib/menubar/install-menubar.d.ts +56 -0
  37. package/dist/lib/menubar/install-menubar.js +133 -17
  38. package/dist/lib/menubar/resolve-version.d.ts +82 -0
  39. package/dist/lib/menubar/resolve-version.js +133 -0
  40. package/dist/lib/profiles.js +13 -2
  41. package/dist/lib/secrets-client.d.ts +23 -3
  42. package/dist/lib/secrets-client.js +57 -57
  43. package/dist/lib/self-heal/checks/menubar-helper.d.ts +2 -0
  44. package/dist/lib/self-heal/checks/menubar-helper.js +21 -0
  45. package/dist/lib/self-heal/registry.js +3 -0
  46. package/dist/lib/self-heal/types.d.ts +1 -1
  47. package/dist/lib/session/db.js +1 -1
  48. package/dist/lib/sha256-asset.d.ts +2 -1
  49. package/dist/lib/sha256-asset.js +2 -1
  50. package/dist/lib/ssh-tunnel.d.ts +61 -0
  51. package/dist/lib/ssh-tunnel.js +105 -0
  52. package/dist/lib/summarizer/summarize.d.ts +2 -2
  53. package/dist/lib/summarizer/summarize.js +10 -3
  54. package/package.json +2 -3
  55. package/dist/commands/computer-actions.d.ts +0 -55
  56. package/dist/commands/computer-actions.js +0 -594
  57. package/dist/computer.d.ts +0 -2
  58. package/dist/computer.js +0 -7
  59. package/dist/lib/computer/actions.d.ts +0 -36
  60. package/dist/lib/computer/actions.js +0 -162
  61. package/dist/lib/computer/computer-rpc.d.ts +0 -39
  62. package/dist/lib/computer/computer-rpc.js +0 -447
  63. package/dist/lib/computer/des.d.ts +0 -1
  64. package/dist/lib/computer/des.js +0 -114
  65. package/dist/lib/computer/dispatch.d.ts +0 -10
  66. package/dist/lib/computer/dispatch.js +0 -133
  67. package/dist/lib/computer/download.d.ts +0 -54
  68. package/dist/lib/computer/download.js +0 -83
  69. package/dist/lib/computer/loop.d.ts +0 -62
  70. package/dist/lib/computer/loop.js +0 -98
  71. package/dist/lib/computer/model.d.ts +0 -44
  72. package/dist/lib/computer/model.js +0 -157
  73. package/dist/lib/computer/rfb-client.d.ts +0 -53
  74. package/dist/lib/computer/rfb-client.js +0 -562
  75. package/dist/lib/computer/ssh-tunnel.d.ts +0 -189
  76. package/dist/lib/computer/ssh-tunnel.js +0 -584
@@ -25,9 +25,10 @@
25
25
  * is load-bearing: the standalone wraps fd 3 in a `net.Socket`, and a Socket
26
26
  * over a NAMED FIFO reads the request but never fires EOF on macOS, so the
27
27
  * older FIFO wiring hung the read loop for the full timeout. Bounded to
28
- * `SYNC_SERVE_TIMEOUT_MS` so a broken standalone fails fast, never for the
29
- * server's 60s deadline. POSIX only; Windows fails loud pointing at the
30
- * async path.
28
+ * `SYNC_SERVE_TIMEOUT_MS` (30s — sized for a cold Node boot of the
29
+ * standalone on a loaded box, see the constant) so a broken standalone
30
+ * fails before the server's 60s deadline. POSIX only; Windows fails loud
31
+ * pointing at the async path.
31
32
  *
32
33
  * State root (MIG-1): the standalone selects its state root from `SECRETS_HOME`.
33
34
  * agents-cli points it at the user agents dir (`~/.agents`) by default so the
@@ -59,20 +60,25 @@ const MAX_PROTOCOL_BYTES = 8 * 1024 * 1024;
59
60
  /** Just over the server's own 60s deadline, so the server times out first. */
60
61
  const SERVE_TIMEOUT_MS = 65_000;
61
62
  /**
62
- * The synchronous path only serves read-only STATUS surfaces — `agents view`,
63
- * the account-catalog rows, and run-config / account-rotation resolution on the
64
- * `agents run` hot path. Those must never hang the whole render or launch on a
65
- * missing or unreachable standalone, so the sync serve carries a short, hard
66
- * bound instead of the async path's 65s: a broken `secrets` fails loud in a few
67
- * seconds and the caller renders the rest of its output (or launches on the
68
- * native login) with one clear line, rather than sitting for the standalone's
69
- * own 60s deadline (the exact 60s hang PHNX-3989 hit when the child ran under
70
- * Bun). A real sync op (a handshake, a bundle list, one item read) completes in
71
- * tens of milliseconds, so this is ~100x headroom. It is NOT a fallback to the
72
- * embedded engine (DIST-1); the standalone stays the only implementation, it
73
- * just fails fast.
63
+ * The synchronous path serves the surfaces that resolve secrets before they can
64
+ * continue — `agents view`, the account-catalog rows, and the account listing and
65
+ * setup-token read on the `agents run` launch path. Those must never hang for the
66
+ * standalone's own 60s deadline (the exact hang PHNX-3989 hit when the child ran
67
+ * under Bun), so the sync serve carries a hard bound below it instead of the
68
+ * async path's 65s. The bound covers a COLD PROCESS, not just the operation: every
69
+ * request spawns `secrets __serve` afresh, so it pays a Node boot plus the
70
+ * standalone's module load before the tens-of-milliseconds op runs: ~0.3s on a
71
+ * quiet box, 0.4–2.6s and occasionally more on a desktop at load average ~100
72
+ * (measured 2026-09-12), where the previous 3s bound turned load into a failed
73
+ * launch (`secrets request failed: spawnSync sh ETIMEDOUT` from `agents run`).
74
+ * 30s absorbs that boot under load and still fails a broken or wedged standalone
75
+ * well before its 60s self-deadline. It is NOT a fallback to the embedded engine
76
+ * (DIST-1); the standalone stays the only implementation. Exported so the
77
+ * real-standalone test can assert a round-trip beats the bound.
74
78
  */
75
- const SYNC_SERVE_TIMEOUT_MS = 3_000;
79
+ export const SYNC_SERVE_TIMEOUT_MS = 30_000;
80
+ /** Test seam: a hang test plants a never-answering standalone and must not wait 30s. */
81
+ let syncServeTimeoutMs = SYNC_SERVE_TIMEOUT_MS;
76
82
  /** Serialize `Map`s the way the server's `decodeWire` expects to receive them. */
77
83
  export function encodeWire(value) {
78
84
  if (value instanceof Map) {
@@ -291,7 +297,18 @@ function parseResponse(raw) {
291
297
  catch {
292
298
  throw new SecretsClientError('INVALID_RESPONSE', `secrets returned a non-JSON response${describeNonJson(text)}`);
293
299
  }
294
- if (!parsed || parsed.v !== PROTOCOL_VERSION || typeof parsed.id !== 'string') {
300
+ if (!parsed || typeof parsed !== 'object') {
301
+ throw new SecretsClientError('INVALID_RESPONSE', 'secrets returned a malformed response envelope');
302
+ }
303
+ // Every reply carries the protocol it was written against, so version
304
+ // negotiation rides the answer itself — there is no separate handshake spawn
305
+ // to pay a cold Node boot for (PHNX-4082). A mismatch is the standalone being
306
+ // too old or too new, which is actionable; anything else is a broken envelope.
307
+ if (typeof parsed.v === 'number' && parsed.v !== PROTOCOL_VERSION) {
308
+ throw new SecretsClientError('PROTOCOL_UNSUPPORTED', `secrets speaks protocol ${String(parsed.v)}; this agents-cli needs ${PROTOCOL_VERSION}. ` +
309
+ 'Update the standalone CLI (npm i -g @phnx-labs/secrets-cli).');
310
+ }
311
+ if (parsed.v !== PROTOCOL_VERSION || typeof parsed.id !== 'string') {
295
312
  throw new SecretsClientError('INVALID_RESPONSE', 'secrets returned a malformed response envelope');
296
313
  }
297
314
  if (parsed.ok)
@@ -413,13 +430,25 @@ function serveOnceSync(op, args, context) {
413
430
  input: request,
414
431
  stdio: ['pipe', 'pipe', 'inherit'],
415
432
  env: buildServeEnv(),
416
- timeout: SYNC_SERVE_TIMEOUT_MS,
433
+ timeout: syncServeTimeoutMs,
417
434
  maxBuffer: MAX_PROTOCOL_BYTES + 4096,
418
435
  });
419
436
  if (result.error) {
420
437
  const err = result.error;
421
- const code = err.code === 'ETIMEDOUT' ? 'TIMEOUT' : 'SPAWN_FAILED';
422
- throw new SecretsClientError(code, `secrets request failed: ${err.message}`);
438
+ if (err.code === 'ETIMEDOUT') {
439
+ throw new SecretsClientError('TIMEOUT', `the standalone \`secrets\` CLI did not answer within ${Math.round(syncServeTimeoutMs / 1000)}s ` +
440
+ `(${[command, ...prefix, '__serve'].join(' ')}). The machine may be too loaded to boot it in time, or the install is broken: ` +
441
+ 'check with `secrets --version`.');
442
+ }
443
+ // A standalone that exits before draining fd 3 leaves `spawnSync`'s stdin
444
+ // write with EPIPE, yet its exit status and everything it wrote to fd 4 are
445
+ // still returned. Like the async path (`input.on('error', () => {})`), the
446
+ // outcome is whatever reached fd 4 — an empty or non-JSON answer is the
447
+ // diagnostic, not the errno. Seen on the GitHub-hosted runner, where a
448
+ // planted `exit 0` standalone exits faster than the request is written.
449
+ if (err.code !== 'EPIPE') {
450
+ throw new SecretsClientError('SPAWN_FAILED', `secrets request failed: ${err.message}`);
451
+ }
423
452
  }
424
453
  const raw = result.stdout ?? Buffer.alloc(0);
425
454
  if (raw.length > MAX_PROTOCOL_BYTES) {
@@ -427,45 +456,14 @@ function serveOnceSync(op, args, context) {
427
456
  }
428
457
  return parseResponse(raw);
429
458
  }
430
- // --- handshake (once per process) ------------------------------------------
431
- let handshakeReady = false;
432
- let handshakePromise = null;
433
- function checkHandshake(result) {
434
- const protocol = result?.protocol;
435
- if (protocol !== PROTOCOL_VERSION) {
436
- throw new SecretsClientError('PROTOCOL_UNSUPPORTED', `secrets speaks protocol ${String(protocol)}; this agents-cli needs ${PROTOCOL_VERSION}. ` +
437
- 'Update the standalone CLI (npm i -g @phnx-labs/secrets-cli).');
438
- }
439
- }
440
- async function ensureHandshake() {
441
- if (handshakeReady)
442
- return;
443
- if (!handshakePromise) {
444
- handshakePromise = (async () => {
445
- checkHandshake(await serveOnce('handshake', []));
446
- handshakeReady = true;
447
- })().catch((error) => {
448
- handshakePromise = null;
449
- throw error;
450
- });
451
- }
452
- await handshakePromise;
453
- }
454
- function ensureHandshakeSync() {
455
- if (handshakeReady)
456
- return;
457
- checkHandshake(serveOnceSync('handshake', []));
458
- handshakeReady = true;
459
- }
460
459
  // --- primitives ------------------------------------------------------------
461
460
  /**
462
461
  * Send one operation to the standalone secrets CLI and await its typed result.
463
- * Verifies the executable speaks protocol v1 once per process (cached), then
464
- * spawns `secrets __serve` for the operation. Throws a {@link SecretsClientError}
462
+ * Spawns `secrets __serve` once for the operation; `parseResponse` checks the
463
+ * protocol on the reply it already carries. Throws a {@link SecretsClientError}
465
464
  * carrying the server's `{code, message}` on failure.
466
465
  */
467
466
  export async function secretsRequest(op, args = [], context) {
468
- await ensureHandshake();
469
467
  return (await serveOnce(op, args, context));
470
468
  }
471
469
  /**
@@ -474,15 +472,17 @@ export async function secretsRequest(op, args = [], context) {
474
472
  * Bounded by a spawn timeout. POSIX only.
475
473
  */
476
474
  export function secretsRequestSync(op, args = [], context) {
477
- ensureHandshakeSync();
478
475
  return serveOnceSync(op, args, context);
479
476
  }
480
- /** Test hook: forget the cached binary + handshake so a new env is re-resolved. */
477
+ /** Test hook: forget the cached binary so a new env is re-resolved. */
481
478
  export function _resetSecretsClientForTest() {
482
479
  cachedBin = undefined;
483
- handshakeReady = false;
484
- handshakePromise = null;
485
480
  requestCounter = 0;
481
+ syncServeTimeoutMs = SYNC_SERVE_TIMEOUT_MS;
482
+ }
483
+ /** Shorten the sync bound for a test that plants a hanging standalone; reset restores it. */
484
+ export function _setSyncServeTimeoutForTest(ms) {
485
+ syncServeTimeoutMs = ms;
486
486
  }
487
487
  // --- typed wrappers -------------------------------------------------------
488
488
  //
@@ -0,0 +1,2 @@
1
+ import type { HealCheck } from '../types.js';
2
+ export declare const menubarHelperCheck: HealCheck;
@@ -0,0 +1,21 @@
1
+ // menubar-helper check — keeps the installed AGI Menu on the newest published
2
+ // build. Discovery + trigger for the helper's auto-update: the daemon runs this
3
+ // on the periodic cadence; the same call runs right after `agents upgrade`.
4
+ // Detect-only under dryRun (what `agents doctor` shows); the repair is the
5
+ // verified download + atomic swap + restart in `updateMenubarHelperIfNewer`.
6
+ import { resultOf } from '../types.js';
7
+ export const menubarHelperCheck = {
8
+ id: 'menubar-helper',
9
+ title: 'AGI Menu helper is the newest published build',
10
+ platforms: ['darwin'],
11
+ cadence: 'periodic',
12
+ async run(ctx) {
13
+ const { updateMenubarHelperIfNewer } = await import('../../menubar/install-menubar.js');
14
+ const r = await updateMenubarHelperIfNewer({ dryRun: ctx.dryRun });
15
+ switch (r.outcome) {
16
+ case 'updated': return resultOf([r.detail], []);
17
+ case 'failed': return resultOf([], [`AGI Menu ${r.installed ?? '?'} → ${r.available}: ${r.detail}`]);
18
+ default: return resultOf([], []);
19
+ }
20
+ },
21
+ };
@@ -11,6 +11,7 @@ import { shimsCheck } from './checks/shims.js';
11
11
  import { shadowingCheck } from './checks/shadowing.js';
12
12
  import { pathCheck } from './checks/path.js';
13
13
  import { installStagingCheck } from './checks/install-staging.js';
14
+ import { menubarHelperCheck } from './checks/menubar-helper.js';
14
15
  // Order matters: cheap structural fixes (shims, shadow adoption, PATH, generated
15
16
  // hook wrappers) before the heavier resource reconciliation, so a freshly-
16
17
  // repaired shim is in place first.
@@ -24,6 +25,8 @@ export const HEAL_CHECKS = [
24
25
  hookManifestCheck,
25
26
  resourcesCheck,
26
27
  installStagingCheck,
28
+ // Last and network-touching: the menu-bar helper's auto-update (macOS only).
29
+ menubarHelperCheck,
27
30
  ];
28
31
  /** Run the selected checks, isolating per-check failures. */
29
32
  export async function runSelfHeal(opts = {}) {
@@ -1,4 +1,4 @@
1
- export type HealCheckId = 'resources' | 'hook-runtime' | 'hook-manifest' | 'shims' | 'shadowing' | 'path' | 'install-staging';
1
+ export type HealCheckId = 'resources' | 'hook-runtime' | 'hook-manifest' | 'shims' | 'shadowing' | 'path' | 'install-staging' | 'menubar-helper';
2
2
  /** When the daemon schedules a check. */
3
3
  export type HealCadence = 'startup' | 'frequent' | 'periodic';
4
4
  export interface HealCtx {
@@ -488,7 +488,7 @@ CREATE INDEX IF NOT EXISTS idx_browser_sessions_started ON browser_sessions(star
488
488
  -- silently vanished on day 8. This row carries the same identity into the durable
489
489
  -- store; the ledger is untouched and remains the audit log, with no second pruner.
490
490
  --
491
- -- Keyed on invocation_id, the id emitComputerAction stamps once per emitting CLI
491
+ -- Keyed on invocation_id, the id recordComputerAction stamps once per emitting CLI
492
492
  -- process: one explicit verb is one row, and a whole "computer run" observe/act
493
493
  -- loop is also one row. task_preview is already bounded by events.ts truncate()
494
494
  -- before it is ever written, and typed-text content is never captured at all.
@@ -2,7 +2,8 @@
2
2
  * sha256 helpers for verifying downloaded release assets.
3
3
  *
4
4
  * These live in their own LEAF module — importing only `node:crypto` and
5
- * `node:fs` — on purpose. They used to sit in `computer/ssh-tunnel.ts`, whose
5
+ * `node:fs` — on purpose. They used to sit in the computer subsystem's ssh-tunnel
6
+ * module (since extracted, PHNX-4075), whose
6
7
  * own import graph reached `browser/drivers/ssh.ts` -> `browser/chrome.ts` ->
7
8
  * the in-repo secrets engine's own keychain-helper downloader, which imported
8
9
  * back into `helper-download.ts` while it was still evaluating — before
@@ -2,7 +2,8 @@
2
2
  * sha256 helpers for verifying downloaded release assets.
3
3
  *
4
4
  * These live in their own LEAF module — importing only `node:crypto` and
5
- * `node:fs` — on purpose. They used to sit in `computer/ssh-tunnel.ts`, whose
5
+ * `node:fs` — on purpose. They used to sit in the computer subsystem's ssh-tunnel
6
+ * module (since extracted, PHNX-4075), whose
6
7
  * own import graph reached `browser/drivers/ssh.ts` -> `browser/chrome.ts` ->
7
8
  * the in-repo secrets engine's own keychain-helper downloader, which imported
8
9
  * back into `helper-download.ts` while it was still evaluating — before
@@ -0,0 +1,61 @@
1
+ /**
2
+ * ssh-tunnel.ts — the generic `ssh -L localPort:127.0.0.1:remotePort -N`
3
+ * port-forward, plus the fleet-device resolution that names its far end.
4
+ *
5
+ * WHY IT LIVES HERE. These primitives used to sit in `lib/computer/ssh-tunnel.ts`
6
+ * alongside the Windows computer-helper provisioning, because `agents computer
7
+ * --device` was their second caller after the browser CDP driver. The computer
8
+ * engine has since moved to the standalone `computer` CLI (PHNX-4075), so a
9
+ * generic tunnel parked in a deleted subsystem's directory would have gone with
10
+ * it and taken `agents browser`'s remote path down. It is fleet plumbing — the
11
+ * devices registry, the hardened ssh baseline, a local loopback port — and
12
+ * belongs in the fleet layer, not under a feature.
13
+ *
14
+ * Its callers are thin: `browser/drivers/ssh.ts` holds a tunnel for one CDP
15
+ * session, and `lib/computer/context.ts` uses `resolveRemoteDevice` alone —
16
+ * `agents computer --device` resolves the fleet name here and forwards the
17
+ * answer to the standalone engine, which opens and owns its own tunnel (it
18
+ * holds the helper token that tunnel's transport needs).
19
+ */
20
+ import { type ChildProcess } from 'child_process';
21
+ import { type DeviceProfile } from './devices/registry.js';
22
+ export interface StartTunnelOptions {
23
+ extraSshArgs?: string[];
24
+ }
25
+ /** Build the ssh argv (after the `ssh` program name) for an `-L` tunnel. Pure.
26
+ *
27
+ * Composes the shared hardened baseline (`SSH_OPTS`) rather than re-listing it,
28
+ * so the tunnel inherits the same options — crucially the keepalive, which lets
29
+ * a dropped `-N` tunnel exit instead of lingering as a zombie on the laptop. */
30
+ export declare function buildTunnelArgs(user: string, host: string, localPort: number, remotePort: number, extraSshArgs?: string[]): string[];
31
+ /**
32
+ * Spawn `ssh -L localPort:127.0.0.1:remotePort -N user@host`.
33
+ *
34
+ * stderr is captured so a tunnel that dies inside 500ms rejects with the ssh
35
+ * error — the browser driver's original contract. The tunnel is held by this
36
+ * process for the lifetime of the session that opened it.
37
+ */
38
+ export declare function startSSHTunnel(user: string, host: string, localPort: number, remotePort: number, opts?: StartTunnelOptions): Promise<ChildProcess>;
39
+ /** One registered device, resolved to everything an ssh invocation needs. */
40
+ export interface ResolvedRemoteDevice {
41
+ device: DeviceProfile;
42
+ target: string;
43
+ user: string;
44
+ host: string;
45
+ /** Per-device identity flags (`-i <key> -o IdentitiesOnly=yes`), possibly empty. */
46
+ identityArgs: string[];
47
+ }
48
+ /**
49
+ * Resolve a registered device to its ssh pieces, or throw a clear error.
50
+ *
51
+ * `expectPlatform` is how a caller keeps a platform requirement it used to
52
+ * hard-code: `agents computer --device` drives the Windows helper daemon, so it
53
+ * passes `'windows'` and gets the same refusal as before. Callers with no
54
+ * platform requirement (the browser driver) omit it. The gate is a parameter
55
+ * rather than a baked-in check so this module stays fleet-generic — a
56
+ * hard-coded `windows` here would be a feature rule in shared plumbing.
57
+ */
58
+ export declare function resolveRemoteDevice(name: string, opts?: {
59
+ expectPlatform?: DeviceProfile['platform'];
60
+ forWhat?: string;
61
+ }): Promise<ResolvedRemoteDevice>;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * ssh-tunnel.ts — the generic `ssh -L localPort:127.0.0.1:remotePort -N`
3
+ * port-forward, plus the fleet-device resolution that names its far end.
4
+ *
5
+ * WHY IT LIVES HERE. These primitives used to sit in `lib/computer/ssh-tunnel.ts`
6
+ * alongside the Windows computer-helper provisioning, because `agents computer
7
+ * --device` was their second caller after the browser CDP driver. The computer
8
+ * engine has since moved to the standalone `computer` CLI (PHNX-4075), so a
9
+ * generic tunnel parked in a deleted subsystem's directory would have gone with
10
+ * it and taken `agents browser`'s remote path down. It is fleet plumbing — the
11
+ * devices registry, the hardened ssh baseline, a local loopback port — and
12
+ * belongs in the fleet layer, not under a feature.
13
+ *
14
+ * Its callers are thin: `browser/drivers/ssh.ts` holds a tunnel for one CDP
15
+ * session, and `lib/computer/context.ts` uses `resolveRemoteDevice` alone —
16
+ * `agents computer --device` resolves the fleet name here and forwards the
17
+ * answer to the standalone engine, which opens and owns its own tunnel (it
18
+ * holds the helper token that tunnel's transport needs).
19
+ */
20
+ import { spawn } from 'child_process';
21
+ import { SSH_OPTS, assertValidSshTarget } from './ssh-exec.js';
22
+ import { getDevice } from './devices/registry.js';
23
+ import { deviceIdentityArgs, sshTargetFor } from './devices/connect.js';
24
+ import { hostNameFor } from './devices/ssh-config.js';
25
+ /** Build the ssh argv (after the `ssh` program name) for an `-L` tunnel. Pure.
26
+ *
27
+ * Composes the shared hardened baseline (`SSH_OPTS`) rather than re-listing it,
28
+ * so the tunnel inherits the same options — crucially the keepalive, which lets
29
+ * a dropped `-N` tunnel exit instead of lingering as a zombie on the laptop. */
30
+ export function buildTunnelArgs(user, host, localPort, remotePort, extraSshArgs = []) {
31
+ return [
32
+ '-L',
33
+ `${localPort}:127.0.0.1:${remotePort}`,
34
+ `${user}@${host}`,
35
+ '-N',
36
+ ...extraSshArgs,
37
+ ...SSH_OPTS,
38
+ ];
39
+ }
40
+ /**
41
+ * Spawn `ssh -L localPort:127.0.0.1:remotePort -N user@host`.
42
+ *
43
+ * stderr is captured so a tunnel that dies inside 500ms rejects with the ssh
44
+ * error — the browser driver's original contract. The tunnel is held by this
45
+ * process for the lifetime of the session that opened it.
46
+ */
47
+ export function startSSHTunnel(user, host, localPort, remotePort, opts = {}) {
48
+ return new Promise((resolve, reject) => {
49
+ // `user`/`host` can originate from a browser ssh:// profile or a device
50
+ // record. buildTunnelArgs places `${user}@${host}` before `-N`/SSH_OPTS, so
51
+ // a `-`-leading user would be parsed as an ssh option flag (option
52
+ // injection). Validate at the spawn sink so every caller is covered; reject
53
+ // (rather than throw synchronously) to keep the Promise contract.
54
+ try {
55
+ assertValidSshTarget(`${user}@${host}`);
56
+ }
57
+ catch (err) {
58
+ reject(err);
59
+ return;
60
+ }
61
+ const args = buildTunnelArgs(user, host, localPort, remotePort, opts.extraSshArgs);
62
+ const tunnel = spawn('ssh', args, {
63
+ stdio: ['ignore', 'ignore', 'pipe'],
64
+ detached: false,
65
+ windowsHide: true,
66
+ });
67
+ let stderr = '';
68
+ tunnel.stderr?.on('data', (data) => {
69
+ stderr += data.toString();
70
+ });
71
+ tunnel.on('error', (err) => {
72
+ reject(new Error(`SSH tunnel failed: ${err.message}`));
73
+ });
74
+ setTimeout(() => {
75
+ if (tunnel.killed)
76
+ reject(new Error(`SSH tunnel died: ${stderr}`));
77
+ else
78
+ resolve(tunnel);
79
+ }, 500);
80
+ });
81
+ }
82
+ /**
83
+ * Resolve a registered device to its ssh pieces, or throw a clear error.
84
+ *
85
+ * `expectPlatform` is how a caller keeps a platform requirement it used to
86
+ * hard-code: `agents computer --device` drives the Windows helper daemon, so it
87
+ * passes `'windows'` and gets the same refusal as before. Callers with no
88
+ * platform requirement (the browser driver) omit it. The gate is a parameter
89
+ * rather than a baked-in check so this module stays fleet-generic — a
90
+ * hard-coded `windows` here would be a feature rule in shared plumbing.
91
+ */
92
+ export async function resolveRemoteDevice(name, opts = {}) {
93
+ const device = await getDevice(name);
94
+ if (!device) {
95
+ throw new Error(`Unknown device '${name}'. Register it with \`agents devices add\` / \`agents devices sync\`, then retry.`);
96
+ }
97
+ if (opts.expectPlatform && device.platform !== opts.expectPlatform) {
98
+ const what = opts.forWhat ?? `this command`;
99
+ throw new Error(`Device '${name}' is ${device.platform}, not ${opts.expectPlatform}. ${what} needs a ${opts.expectPlatform} device.`);
100
+ }
101
+ const target = sshTargetFor(device); // validates address + injection guard
102
+ const host = hostNameFor(device); // sshTargetFor already threw if absent
103
+ const user = device.user || process.env.USER || 'Administrator';
104
+ return { device, target, user, host, identityArgs: deviceIdentityArgs(device) };
105
+ }
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * A pure boundary: given the session's first user turn (the goal source) and its
5
5
  * live progress (todos / plan / phase off the state engine), it asks a local
6
- * Anthropic-wire endpoint (Ollama / vLLM / LiteLLM — the same configurable base
7
- * URL `computer/model.ts` speaks) for a strict JSON `{goal, checkpoints, checklist}`
6
+ * Anthropic-wire endpoint (Ollama / vLLM / LiteLLM) for a strict JSON
7
+ * `{goal, checkpoints, checklist}`
8
8
  * and validates the shape. On ANY failure — network, non-2xx, non-JSON, wrong
9
9
  * shape — it returns `undefined`, and the caller records `summaryState: 'skipped'`.
10
10
  *
@@ -3,15 +3,22 @@
3
3
  *
4
4
  * A pure boundary: given the session's first user turn (the goal source) and its
5
5
  * live progress (todos / plan / phase off the state engine), it asks a local
6
- * Anthropic-wire endpoint (Ollama / vLLM / LiteLLM — the same configurable base
7
- * URL `computer/model.ts` speaks) for a strict JSON `{goal, checkpoints, checklist}`
6
+ * Anthropic-wire endpoint (Ollama / vLLM / LiteLLM) for a strict JSON
7
+ * `{goal, checkpoints, checklist}`
8
8
  * and validates the shape. On ANY failure — network, non-2xx, non-JSON, wrong
9
9
  * shape — it returns `undefined`, and the caller records `summaryState: 'skipped'`.
10
10
  *
11
11
  * NEVER called on the request path: only the background SessionSummarizerService
12
12
  * invokes it, debounced and reader-gated.
13
13
  */
14
- import { ANTHROPIC_VERSION } from '../computer/model.js';
14
+ /**
15
+ * The `anthropic-version` header this request sends. Declared here because this
16
+ * is now its only caller: it used to be imported from the computer subsystem's
17
+ * model client, which left with the standalone `computer` engine (PHNX-4075).
18
+ * A shared constants module for one string used in one request would be more
19
+ * indirection than the string.
20
+ */
21
+ const ANTHROPIC_VERSION = '2023-06-01';
15
22
  const SYSTEM_PROMPT = [
16
23
  'You summarize a coding-agent session for an operator dashboard.',
17
24
  'Reply with ONLY a single JSON object, no prose and no code fences, of exactly this shape:',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.22.102",
3
+ "version": "1.22.104",
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",
@@ -17,8 +17,7 @@
17
17
  "bin": {
18
18
  "agents": "dist/index.js",
19
19
  "ag": "dist/index.js",
20
- "browser": "dist/browser.js",
21
- "computer": "dist/computer.js"
20
+ "browser": "dist/browser.js"
22
21
  },
23
22
  "files": [
24
23
  "dist/**/*.js",
@@ -1,55 +0,0 @@
1
- import { Command } from 'commander';
2
- import { type ComputerClient, type RPCResponse } from '../lib/computer/computer-rpc.js';
3
- export { emitComputerAction, pickTarget, resolveTargetPidDecision, type AppInfo } from '../lib/computer/actions.js';
4
- export declare function parseXY(s: string, flag: string): {
5
- x: number;
6
- y: number;
7
- };
8
- export declare function buildElementOrCoords(opts: {
9
- id?: string;
10
- x?: number;
11
- y?: number;
12
- }): {
13
- ok: true;
14
- params: Record<string, unknown>;
15
- } | {
16
- ok: false;
17
- error: string;
18
- };
19
- export declare function buildRaiseParams(opts: {
20
- windowId?: number;
21
- title?: string;
22
- }): Record<string, unknown>;
23
- export declare const CHAR_DELAY_MIN_MS = 1;
24
- export declare const CHAR_DELAY_MAX_MS = 250;
25
- export declare function clampCharDelay(ms: number | undefined): number | undefined;
26
- export declare function buildWaitParams(opts: {
27
- duration?: number;
28
- id?: string;
29
- until?: string;
30
- role?: string;
31
- label?: string;
32
- identifier?: string;
33
- timeout?: number;
34
- }): {
35
- ok: true;
36
- params: Record<string, unknown>;
37
- } | {
38
- ok: false;
39
- error: string;
40
- };
41
- export declare function withClient<T>(fn: (client: ComputerClient) => Promise<T>): Promise<T>;
42
- export declare function unwrap(r: RPCResponse): Record<string, unknown>;
43
- export declare function focusStealNotes(opts: {
44
- id?: string;
45
- x?: number;
46
- y?: number;
47
- raise?: boolean;
48
- }): string[];
49
- export declare function shouldRaise(opts: {
50
- id?: string;
51
- raise?: boolean;
52
- }): boolean;
53
- export declare function appPathIsElectron(appPath: string | null, exists?: (p: string) => boolean): boolean;
54
- export declare function electronWebviewTip(appLabel: string): string;
55
- export declare function registerActionCommands(program: Command): void;