@phnx-labs/agents-cli 1.22.86 → 1.22.88

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.22.88
4
+
5
+ - **`agents run` no longer fails with `secrets request failed: spawnSync sh
6
+ ETIMEDOUT` when resolving an account credential.** The synchronous secrets path
7
+ (`agents view`, the account catalog, and account resolution on the `agents run`
8
+ hot path) fed the standalone `secrets __serve` its request over a named FIFO on
9
+ fd 3. The standalone wraps fd 3 in a `net.Socket`, and a Socket over a named FIFO
10
+ reads the request but never fires EOF on macOS — so its read loop blocked until
11
+ the 3s sync bound fired and every credential lookup timed out. The request now
12
+ rides `spawnSync`'s stdin (a real pipe/socketpair — the same fd type the async
13
+ path already hands the standalone) dup'd onto fd 3, so it EOFs and a real
14
+ handshake/lookup completes in tens of ms. Source: `cli/src/lib/secrets-client.ts`
15
+ (`serveOnceSync`).
16
+
3
17
  ## 1.22.86
4
18
 
5
19
  - **Workers now materialize a slot for every registered Claude account, not only
@@ -17,12 +17,17 @@
17
17
  * - async: `spawn` with stdio ['ignore','ignore','inherit','pipe','pipe'];
18
18
  * write+end child.stdio[3], read child.stdio[4] to EOF.
19
19
  * - sync: `spawnSync` only wires stdio 0-2 portably (Bun drops numbered fds
20
- * 3+), so the fd 3/4 wiring is done in a POSIX shell: the request rides a
21
- * named FIFO fed by a backgrounded `cat` (fd 3), and fd 4 is redirected onto
22
- * the child's stdout (`4>&1`), which `spawnSync` captures natively on every
23
- * runtime. Bounded to `SYNC_SERVE_TIMEOUT_MS` so a broken standalone fails
24
- * fast, never for the server's 60s deadline. POSIX only; Windows has no
25
- * `mkfifo` and fails loud pointing at the async path.
20
+ * 3+), so the fd 3/4 wiring is done in a POSIX shell: the request rides
21
+ * `spawnSync`'s STDIN — a real pipe/socketpair on every runtime — which the
22
+ * shell dups onto fd 3 (`3<&0`), and fd 4 is redirected onto the child's
23
+ * stdout (`4>&1`), which `spawnSync` captures natively. Both fds are then the
24
+ * SAME anonymous pipe/socketpair the async path hands the standalone, which
25
+ * is load-bearing: the standalone wraps fd 3 in a `net.Socket`, and a Socket
26
+ * over a NAMED FIFO reads the request but never fires EOF on macOS, so the
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.
26
31
  *
27
32
  * State root (MIG-1): the standalone selects its state root from `SECRETS_HOME`.
28
33
  * agents-cli points it at the user agents dir (`~/.agents`) by default so the
@@ -42,9 +47,6 @@
42
47
  * `@phnx-labs/secrets-cli` package is the only implementation.
43
48
  */
44
49
  import { spawn, spawnSync } from 'node:child_process';
45
- import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
46
- import { tmpdir } from 'node:os';
47
- import { join } from 'node:path';
48
50
  import { findInPath } from './agent-spec/agents.js';
49
51
  import { getUserAgentsDir } from './state.js';
50
52
  /**
@@ -371,69 +373,59 @@ function shQuote(token) {
371
373
  }
372
374
  function serveOnceSync(op, args, context) {
373
375
  if (process.platform === 'win32') {
374
- throw new SecretsClientError('SYNC_UNSUPPORTED', 'The synchronous secrets path needs a POSIX shell and FIFO; use secretsRequest (async) on Windows.');
376
+ throw new SecretsClientError('SYNC_UNSUPPORTED', 'The synchronous secrets path needs a POSIX shell; use secretsRequest (async) on Windows.');
375
377
  }
376
378
  const { command, prefix } = invocation(resolveSecretsBin());
377
379
  const request = Buffer.from(JSON.stringify(buildRequest(op, args, context)));
378
380
  // The standalone's private protocol reads the request from fd 3 and writes the
379
- // response to fd 4, and it REFUSES a plain file or tty for either (it fails with
380
- // `PRIVATE_PIPE_REQUIRED` so no secret is ever staged to disk). But `spawnSync`
381
- // only wires stdio 0-2 portably — the Bun runtime (this repo runs its whole CLI
382
- // suite as `bun src/index.ts`, so the 71 CLI-integration tests spawn agents-cli
383
- // under Bun) silently DROPS numbered fds 3+, so the old direct-fd wiring left the
384
- // child blocked on an empty fd 3 for the full timeout. Do the numbered-fd wiring
385
- // in a POSIX shell against PIPES instead: the request rides a FIFO fed by a
386
- // backgrounded `cat` (fd 3), and fd 4 is dup'd from the captured stdout pipe
387
- // (`4>&1`), which `spawnSync` captures natively as an anonymous pipe on every
388
- // runtime. The child's own fd 1 is then redirected to /dev/null (`1>/dev/null`,
389
- // applied AFTER `4>&1` so fd 4 keeps the pipe): the response channel (fd 4) is
390
- // structurally the ONLY writer to the captured stream, so even a stray write to
391
- // the standalone's own stdout can never corrupt the fd-4 bytes. Both fd 3/4 are
392
- // S_ISFIFO, satisfying the standalone; the response never touches disk.
393
- // Identical on Node and Bun.
394
- const dir = mkdtempSync(join(tmpdir(), 'agents-secrets-'));
395
- const reqFile = join(dir, 'req');
396
- const reqFifo = join(dir, 'reqfifo');
397
- try {
398
- writeFileSync(reqFile, request);
399
- const mk = spawnSync('mkfifo', [reqFifo]);
400
- if (mk.status !== 0) {
401
- throw new SecretsClientError('SYNC_UNSUPPORTED', 'mkfifo is unavailable for the synchronous secrets path');
402
- }
403
- const serve = [command, ...prefix, '__serve'].map(shQuote).join(' ');
404
- // `exec` so the wrapper `sh` BECOMES the standalone (same PID). Two things ride
405
- // on that: `spawnSync` waits on the real process, so on return the store is
406
- // fully flushed and its proper-lockfile lock dir released (the sync path never
407
- // had the async path's resolve-before-exit race — spawnSync is a hard barrier);
408
- // and `spawnSync`'s timeout SIGTERM lands on the standalone itself rather than
409
- // orphaning a wedged child behind a dead wrapper. The backgrounded `cat` only
410
- // feeds the tiny request into the FIFO and exits the instant the reader (fd 3)
411
- // opens — long before the standalone finishes — so it is reaped by its own exit
412
- // and never outlives the call. fd wiring is order-sensitive: `4>&1` dups the
413
- // response channel from the captured stdout pipe, THEN `1>/dev/null` sends the
414
- // child's own stdout to the bit bucket, so fd 4 stays the sole writer to the
415
- // captured stream.
416
- const script = `cat ${shQuote(reqFile)} > ${shQuote(reqFifo)} & exec ${serve} 3<${shQuote(reqFifo)} 4>&1 1>/dev/null`;
417
- const result = spawnSync('sh', ['-c', script], {
418
- stdio: ['ignore', 'pipe', 'inherit'],
419
- env: buildServeEnv(),
420
- timeout: SYNC_SERVE_TIMEOUT_MS,
421
- maxBuffer: MAX_PROTOCOL_BYTES + 4096,
422
- });
423
- if (result.error) {
424
- const err = result.error;
425
- const code = err.code === 'ETIMEDOUT' ? 'TIMEOUT' : 'SPAWN_FAILED';
426
- throw new SecretsClientError(code, `secrets request failed: ${err.message}`);
427
- }
428
- const raw = result.stdout ?? Buffer.alloc(0);
429
- if (raw.length > MAX_PROTOCOL_BYTES) {
430
- throw new SecretsClientError('RESPONSE_TOO_LARGE', 'secrets response exceeds the protocol limit');
431
- }
432
- return parseResponse(raw);
381
+ // response to fd 4, wrapping BOTH in a `net.Socket` (secrets-cli
382
+ // `runProtocolServer`). It also REFUSES a plain file or tty for either — fd 3/4
383
+ // MUST be a pipe or socket (`PRIVATE_PIPE_REQUIRED`) — so no secret is ever
384
+ // staged to disk. But `spawnSync` only wires stdio 0-2 portably — the Bun
385
+ // runtime (this repo runs its whole CLI suite as `bun src/index.ts`, so the CLI-
386
+ // integration tests spawn agents-cli under Bun) silently DROPS numbered fds 3+,
387
+ // so the numbered-fd wiring is done in a POSIX shell.
388
+ //
389
+ // The request rides `spawnSync`'s STDIN, which every runtime backs with a real
390
+ // pipe/socketpair, and the shell dups it onto fd 3 (`3<&0`); fd 4 is dup'd from
391
+ // the captured stdout pipe (`4>&1`), then the child's own fd 1 is sent to
392
+ // /dev/null (`1>/dev/null`, AFTER `4>&1` so fd 4 keeps the pipe) — so the
393
+ // response channel (fd 4) is structurally the ONLY writer to the captured
394
+ // stream and a stray write to the standalone's own stdout can never corrupt the
395
+ // fd-4 bytes. Crucially, fd 3 and fd 4 are now the SAME anonymous
396
+ // pipe/socketpair the async path hands the standalone via `spawn` numbered
397
+ // stdio, rather than a named FIFO: a `net.Socket` wrapped around a named FIFO
398
+ // reads the request but never fires EOF on macOS, so the standalone's
399
+ // `for await (const chunk of input)` on fd 3 would hang for the full timeout
400
+ // (the bug the old FIFO wiring hit — spawnSync's stdout IS a socketpair, so fd 4
401
+ // was fine, but the FIFO on fd 3 never EOF'd). `exec` so the wrapper `sh`
402
+ // BECOMES the standalone (same PID): `spawnSync` waits on the real process, so
403
+ // on return the store is fully flushed and its lock dir released, and a timeout
404
+ // SIGTERM lands on the standalone itself rather than orphaning a wedged child.
405
+ // The response never touches disk. The shell wiring is parent-runtime-agnostic
406
+ // — `spawnSync`'s stdio is a socketpair under both Node and Bun — so fd 3 EOFs
407
+ // whenever the standalone itself runs under Node. (A standalone that `invocation`
408
+ // launches under BUN still hits its own PHNX-3989 EOF hang regardless of the
409
+ // wiring; that stays bounded by SYNC_SERVE_TIMEOUT_MS below, as before.)
410
+ const serve = [command, ...prefix, '__serve'].map(shQuote).join(' ');
411
+ const script = `exec ${serve} 3<&0 4>&1 1>/dev/null`;
412
+ const result = spawnSync('sh', ['-c', script], {
413
+ input: request,
414
+ stdio: ['pipe', 'pipe', 'inherit'],
415
+ env: buildServeEnv(),
416
+ timeout: SYNC_SERVE_TIMEOUT_MS,
417
+ maxBuffer: MAX_PROTOCOL_BYTES + 4096,
418
+ });
419
+ if (result.error) {
420
+ const err = result.error;
421
+ const code = err.code === 'ETIMEDOUT' ? 'TIMEOUT' : 'SPAWN_FAILED';
422
+ throw new SecretsClientError(code, `secrets request failed: ${err.message}`);
433
423
  }
434
- finally {
435
- rmSync(dir, { recursive: true, force: true });
424
+ const raw = result.stdout ?? Buffer.alloc(0);
425
+ if (raw.length > MAX_PROTOCOL_BYTES) {
426
+ throw new SecretsClientError('RESPONSE_TOO_LARGE', 'secrets response exceeds the protocol limit');
436
427
  }
428
+ return parseResponse(raw);
437
429
  }
438
430
  // --- handshake (once per process) ------------------------------------------
439
431
  let handshakeReady = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.22.86",
3
+ "version": "1.22.88",
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",