@phnx-labs/agents-cli 1.20.63 → 1.20.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/CHANGELOG.md +56 -3
  2. package/README.md +46 -2
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/apply.d.ts +12 -0
  5. package/dist/commands/apply.js +274 -0
  6. package/dist/commands/browser.js +2 -2
  7. package/dist/commands/cloud.js +32 -2
  8. package/dist/commands/doctor.js +4 -1
  9. package/dist/commands/exec.js +149 -77
  10. package/dist/commands/feed.d.ts +4 -0
  11. package/dist/commands/feed.js +52 -19
  12. package/dist/commands/hosts.js +44 -6
  13. package/dist/commands/lease.d.ts +23 -0
  14. package/dist/commands/lease.js +201 -0
  15. package/dist/commands/mailboxes.d.ts +20 -0
  16. package/dist/commands/mailboxes.js +390 -0
  17. package/dist/commands/mcp.js +55 -5
  18. package/dist/commands/monitors.d.ts +12 -0
  19. package/dist/commands/monitors.js +740 -0
  20. package/dist/commands/output.js +2 -2
  21. package/dist/commands/routines.js +43 -16
  22. package/dist/commands/secrets.d.ts +16 -0
  23. package/dist/commands/secrets.js +215 -64
  24. package/dist/commands/serve.js +31 -0
  25. package/dist/commands/sessions-export.d.ts +2 -0
  26. package/dist/commands/sessions-export.js +284 -0
  27. package/dist/commands/sessions-import.d.ts +2 -0
  28. package/dist/commands/sessions-import.js +230 -0
  29. package/dist/commands/sessions.d.ts +16 -0
  30. package/dist/commands/sessions.js +40 -6
  31. package/dist/commands/ssh.js +143 -5
  32. package/dist/commands/usage.d.ts +2 -0
  33. package/dist/commands/usage.js +7 -2
  34. package/dist/commands/versions.js +7 -3
  35. package/dist/commands/view.d.ts +27 -1
  36. package/dist/commands/view.js +32 -9
  37. package/dist/commands/webhook.js +10 -2
  38. package/dist/index.js +35 -14
  39. package/dist/lib/agents.d.ts +36 -0
  40. package/dist/lib/agents.js +80 -20
  41. package/dist/lib/auto-dispatch-provider.js +7 -2
  42. package/dist/lib/auto-dispatch.d.ts +3 -0
  43. package/dist/lib/auto-dispatch.js +3 -0
  44. package/dist/lib/browser/chrome.js +2 -2
  45. package/dist/lib/browser/drivers/ssh.js +19 -2
  46. package/dist/lib/cloud/antigravity.js +2 -2
  47. package/dist/lib/cloud/host.d.ts +59 -0
  48. package/dist/lib/cloud/host.js +224 -0
  49. package/dist/lib/cloud/registry.js +4 -0
  50. package/dist/lib/cloud/types.d.ts +6 -4
  51. package/dist/lib/comms-render.d.ts +37 -0
  52. package/dist/lib/comms-render.js +89 -0
  53. package/dist/lib/computer-rpc.js +3 -1
  54. package/dist/lib/crabbox/cli.d.ts +72 -0
  55. package/dist/lib/crabbox/cli.js +162 -9
  56. package/dist/lib/crabbox/runtimes.d.ts +13 -0
  57. package/dist/lib/crabbox/runtimes.js +35 -2
  58. package/dist/lib/daemon.d.ts +20 -4
  59. package/dist/lib/daemon.js +68 -20
  60. package/dist/lib/devices/fleet.d.ts +3 -2
  61. package/dist/lib/devices/fleet.js +9 -0
  62. package/dist/lib/devices/health.d.ts +77 -0
  63. package/dist/lib/devices/health.js +186 -0
  64. package/dist/lib/devices/registry.d.ts +15 -0
  65. package/dist/lib/devices/registry.js +9 -0
  66. package/dist/lib/exec.d.ts +19 -2
  67. package/dist/lib/exec.js +41 -13
  68. package/dist/lib/fleet/apply.d.ts +63 -0
  69. package/dist/lib/fleet/apply.js +214 -0
  70. package/dist/lib/fleet/auth-sync.d.ts +67 -0
  71. package/dist/lib/fleet/auth-sync.js +142 -0
  72. package/dist/lib/fleet/manifest.d.ts +29 -0
  73. package/dist/lib/fleet/manifest.js +127 -0
  74. package/dist/lib/fleet/types.d.ts +129 -0
  75. package/dist/lib/fleet/types.js +13 -0
  76. package/dist/lib/git.d.ts +27 -0
  77. package/dist/lib/git.js +34 -2
  78. package/dist/lib/hosts/dispatch.d.ts +29 -8
  79. package/dist/lib/hosts/dispatch.js +46 -18
  80. package/dist/lib/hosts/passthrough.js +2 -0
  81. package/dist/lib/hosts/providers/devices.d.ts +27 -0
  82. package/dist/lib/hosts/providers/devices.js +98 -0
  83. package/dist/lib/hosts/registry.d.ts +10 -16
  84. package/dist/lib/hosts/registry.js +17 -50
  85. package/dist/lib/hosts/remote-cmd.d.ts +23 -0
  86. package/dist/lib/hosts/remote-cmd.js +71 -0
  87. package/dist/lib/hosts/run-target.d.ts +84 -0
  88. package/dist/lib/hosts/run-target.js +99 -0
  89. package/dist/lib/hosts/types.d.ts +23 -5
  90. package/dist/lib/hosts/types.js +22 -4
  91. package/dist/lib/linear-autoclose.d.ts +30 -0
  92. package/dist/lib/linear-autoclose.js +22 -0
  93. package/dist/lib/mailbox.d.ts +39 -0
  94. package/dist/lib/mailbox.js +112 -0
  95. package/dist/lib/mcp.d.ts +27 -1
  96. package/dist/lib/mcp.js +126 -12
  97. package/dist/lib/monitors/config.d.ts +161 -0
  98. package/dist/lib/monitors/config.js +372 -0
  99. package/dist/lib/monitors/dispatch.d.ts +28 -0
  100. package/dist/lib/monitors/dispatch.js +91 -0
  101. package/dist/lib/monitors/engine.d.ts +61 -0
  102. package/dist/lib/monitors/engine.js +201 -0
  103. package/dist/lib/monitors/sources/command.d.ts +11 -0
  104. package/dist/lib/monitors/sources/command.js +31 -0
  105. package/dist/lib/monitors/sources/device.d.ts +13 -0
  106. package/dist/lib/monitors/sources/device.js +35 -0
  107. package/dist/lib/monitors/sources/file.d.ts +14 -0
  108. package/dist/lib/monitors/sources/file.js +57 -0
  109. package/dist/lib/monitors/sources/http.d.ts +10 -0
  110. package/dist/lib/monitors/sources/http.js +34 -0
  111. package/dist/lib/monitors/sources/index.d.ts +14 -0
  112. package/dist/lib/monitors/sources/index.js +31 -0
  113. package/dist/lib/monitors/sources/poll.d.ts +9 -0
  114. package/dist/lib/monitors/sources/poll.js +9 -0
  115. package/dist/lib/monitors/sources/types.d.ts +18 -0
  116. package/dist/lib/monitors/sources/types.js +9 -0
  117. package/dist/lib/monitors/sources/webhook.d.ts +23 -0
  118. package/dist/lib/monitors/sources/webhook.js +47 -0
  119. package/dist/lib/monitors/sources/ws.d.ts +14 -0
  120. package/dist/lib/monitors/sources/ws.js +45 -0
  121. package/dist/lib/monitors/state.d.ts +69 -0
  122. package/dist/lib/monitors/state.js +144 -0
  123. package/dist/lib/paths.d.ts +13 -0
  124. package/dist/lib/paths.js +26 -4
  125. package/dist/lib/platform/exec.d.ts +16 -0
  126. package/dist/lib/platform/exec.js +17 -0
  127. package/dist/lib/plugins.js +101 -2
  128. package/dist/lib/redact.d.ts +14 -1
  129. package/dist/lib/redact.js +47 -1
  130. package/dist/lib/remote-agents-json.js +7 -1
  131. package/dist/lib/rotate.d.ts +6 -3
  132. package/dist/lib/rotate.js +0 -1
  133. package/dist/lib/routines.d.ts +37 -2
  134. package/dist/lib/routines.js +54 -12
  135. package/dist/lib/runner.d.ts +1 -0
  136. package/dist/lib/runner.js +348 -13
  137. package/dist/lib/sandbox.d.ts +9 -1
  138. package/dist/lib/sandbox.js +11 -2
  139. package/dist/lib/secrets/agent.d.ts +48 -10
  140. package/dist/lib/secrets/agent.js +123 -15
  141. package/dist/lib/secrets/bundles.d.ts +26 -0
  142. package/dist/lib/secrets/bundles.js +59 -8
  143. package/dist/lib/secrets/mcp.js +4 -2
  144. package/dist/lib/secrets/remote.d.ts +17 -0
  145. package/dist/lib/secrets/remote.js +40 -0
  146. package/dist/lib/self-update.d.ts +20 -0
  147. package/dist/lib/self-update.js +54 -1
  148. package/dist/lib/serve/control.d.ts +95 -0
  149. package/dist/lib/serve/control.js +260 -0
  150. package/dist/lib/serve/server.d.ts +35 -1
  151. package/dist/lib/serve/server.js +106 -76
  152. package/dist/lib/serve/stream.d.ts +43 -0
  153. package/dist/lib/serve/stream.js +116 -0
  154. package/dist/lib/serve/token.d.ts +35 -0
  155. package/dist/lib/serve/token.js +85 -0
  156. package/dist/lib/session/bundle.d.ts +164 -0
  157. package/dist/lib/session/bundle.js +200 -0
  158. package/dist/lib/session/remote-bundle.d.ts +12 -0
  159. package/dist/lib/session/remote-bundle.js +61 -0
  160. package/dist/lib/session/remote-list.js +5 -1
  161. package/dist/lib/session/state.d.ts +7 -25
  162. package/dist/lib/session/state.js +16 -6
  163. package/dist/lib/session/sync/agents.d.ts +54 -6
  164. package/dist/lib/session/sync/agents.js +0 -0
  165. package/dist/lib/session/sync/config.js +8 -2
  166. package/dist/lib/session/sync/manifest.d.ts +14 -3
  167. package/dist/lib/session/sync/manifest.js +4 -0
  168. package/dist/lib/session/sync/sync.d.ts +23 -2
  169. package/dist/lib/session/sync/sync.js +177 -74
  170. package/dist/lib/session/types.d.ts +30 -0
  171. package/dist/lib/ssh-tunnel.d.ts +19 -1
  172. package/dist/lib/ssh-tunnel.js +99 -8
  173. package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
  174. package/dist/lib/staleness/detectors/subagents.js +5 -192
  175. package/dist/lib/staleness/writers/subagents.d.ts +10 -0
  176. package/dist/lib/staleness/writers/subagents.js +11 -102
  177. package/dist/lib/startup/command-registry.d.ts +4 -0
  178. package/dist/lib/startup/command-registry.js +9 -0
  179. package/dist/lib/state.d.ts +5 -0
  180. package/dist/lib/state.js +12 -0
  181. package/dist/lib/subagents-registry.d.ts +85 -0
  182. package/dist/lib/subagents-registry.js +393 -0
  183. package/dist/lib/subagents.d.ts +8 -8
  184. package/dist/lib/subagents.js +32 -663
  185. package/dist/lib/sync-umbrella.d.ts +1 -0
  186. package/dist/lib/sync-umbrella.js +14 -3
  187. package/dist/lib/tmux/session.d.ts +7 -0
  188. package/dist/lib/tmux/session.js +3 -1
  189. package/dist/lib/triggers/webhook.d.ts +18 -0
  190. package/dist/lib/triggers/webhook.js +105 -0
  191. package/dist/lib/types.d.ts +35 -1
  192. package/dist/lib/usage.d.ts +42 -3
  193. package/dist/lib/usage.js +163 -21
  194. package/dist/lib/versions.js +14 -11
  195. package/dist/lib/workflows.d.ts +20 -0
  196. package/dist/lib/workflows.js +24 -0
  197. package/package.json +2 -1
@@ -723,6 +723,46 @@ export function agentGetSync(name) {
723
723
  return null;
724
724
  }
725
725
  }
726
+ /** Inline node program for a synchronous liveness ping. Connects, sends one
727
+ * `{cmd:'ping'}`, exits 0 iff a valid ping response comes back, else 3. A stale
728
+ * socket file with no listener refuses the connection immediately, so this
729
+ * fast-fails without riding any cold-start logic. argv after -e: [execPath, <socket>]. */
730
+ const SYNC_PING_PROGRAM = `
731
+ const net = require('net');
732
+ const sock = process.argv[1];
733
+ const c = net.createConnection(sock);
734
+ let buf = '';
735
+ const dead = () => { try { c.destroy(); } catch (e) {} process.exit(3); };
736
+ const timer = setTimeout(dead, 700);
737
+ c.on('error', dead);
738
+ c.on('connect', () => c.write(JSON.stringify({ cmd: 'ping' }) + '\\n'));
739
+ c.setEncoding('utf-8');
740
+ c.on('data', (d) => {
741
+ buf += d;
742
+ const nl = buf.indexOf('\\n');
743
+ if (nl < 0) return;
744
+ clearTimeout(timer);
745
+ let r; try { r = JSON.parse(buf.slice(0, nl)); } catch (e) { return dead(); }
746
+ try { c.destroy(); } catch (e) {}
747
+ process.exit(r && r.ok && r.cmd === 'ping' ? 0 : 3);
748
+ });
749
+ `;
750
+ /**
751
+ * Synchronous liveness check: is a broker actually LISTENING and answering (not
752
+ * just a lingering socket file)? Used to decide whether the auto-cache may take
753
+ * the synchronous warm path — a dead broker whose socket outlived it (crash,
754
+ * OOM, version-skew teardown) must NOT drag a foreground read through the
755
+ * worker's 20s cold-start budget. A stale socket refuses instantly, so this is
756
+ * fast in both the alive and dead cases. macOS only.
757
+ */
758
+ export function agentReachableSync() {
759
+ if (!onDarwin())
760
+ return false;
761
+ if (!agentSocketExists())
762
+ return false;
763
+ const r = spawnSync(process.execPath, ['-e', SYNC_PING_PROGRAM, socketPath()], { timeout: 1500 });
764
+ return r.status === 0 && !r.error;
765
+ }
726
766
  /**
727
767
  * Inline node program for the synchronous evict path. Mirrors SYNC_GET_PROGRAM:
728
768
  * writeBundle is synchronous and called synchronously everywhere, so a stale
@@ -817,26 +857,81 @@ export function secretsAgentAutoEnabled() {
817
857
  return true;
818
858
  }
819
859
  }
860
+ /** Minimum / maximum bounds for the configurable hold window. A too-small value
861
+ * would defeat the broker (constant re-prompts); a too-large one pins secrets in
862
+ * memory far longer than intended. */
863
+ export const MIN_HOLD_MS = 60 * 1000; // 1m
864
+ export const MAX_HOLD_MS = 30 * 24 * 60 * 60 * 1000; // 30d
865
+ /**
866
+ * How long an unlocked / auto-cached bundle is held before the next read
867
+ * re-prompts. Defaults to DEFAULT_TTL_MS (7d); override with
868
+ * `secrets.agent.holdMs` (milliseconds) in agents.yaml — e.g. 86400000 for a 24h
869
+ * cap. Clamped to [MIN_HOLD_MS, MAX_HOLD_MS] so a typo can neither disable the
870
+ * hold nor pin a secret in memory indefinitely. Best-effort: an unreadable or
871
+ * non-numeric value falls back to the 7d default. Pure except for the meta read.
872
+ */
873
+ export function secretsHoldMs() {
874
+ try {
875
+ return clampHoldMs(readMeta().secrets?.agent?.holdMs);
876
+ }
877
+ catch {
878
+ return DEFAULT_TTL_MS;
879
+ }
880
+ }
881
+ /** Pure clamp for a configured `holdMs`: a positive finite number is bounded to
882
+ * [MIN_HOLD_MS, MAX_HOLD_MS]; anything else (absent, 0, negative, NaN, non-number)
883
+ * falls back to the 7d default. Exported for direct unit testing. */
884
+ export function clampHoldMs(v) {
885
+ if (typeof v === 'number' && Number.isFinite(v) && v > 0) {
886
+ return Math.min(Math.max(Math.floor(v), MIN_HOLD_MS), MAX_HOLD_MS);
887
+ }
888
+ return DEFAULT_TTL_MS;
889
+ }
820
890
  /**
821
891
  * Fire-and-forget: populate the broker with a freshly-resolved bundle so the
822
892
  * NEXT process reads it without a prompt. Used by the auto-cache path after a
823
- * real keychain read of a `daily`-policy bundle. Adds no latency to the caller
824
- * it spawns a detached `secrets _agent-load` worker (passing the resolved env
825
- * over stdin, never argv) and returns immediately.
893
+ * real keychain read of a `daily`-policy bundle, so the NEXT concurrent read is
894
+ * silent. Env travels over stdin, never argv.
826
895
  *
827
- * The worker reuses the robust `ensureAgentRunning` path (spawn-then-ping with a
828
- * generous budget) rather than a tight inline retry loop: under heavy load the
829
- * broker is itself a cold-starting full CLI and can take several seconds to bind
830
- * the socket, so a short fixed budget would give up before it's ready and the
831
- * cache would silently never populate. Best-effort; never throws. macOS only.
896
+ * Reliability (this is what makes `daily` actually "stick"): when a broker is
897
+ * ALREADY listening, warm it SYNCHRONOUSLY with a bounded wait so the bundle is
898
+ * held by the time this process exits. The old detached-only path lost the race
899
+ * under load a short-lived reader (`agents secrets export`, a release-script
900
+ * loop) exited before the unref'd worker connected, so the cache silently never
901
+ * populated and every read re-prompted despite the `daily` policy. Only when the
902
+ * broker must COLD-START (no socket yet) do we fall back to the detached worker,
903
+ * so a first-ever read never blocks on a multi-second broker boot.
904
+ *
905
+ * The worker reuses the robust `ensureAgentRunning` path (spawn-then-ping) rather
906
+ * than a tight inline retry loop. Best-effort; never throws. macOS only.
832
907
  */
833
908
  export function agentAutoLoadSync(name, bundle, env, ttlMs) {
834
909
  if (!onDarwin())
835
910
  return;
911
+ const payload = JSON.stringify({ name, bundle, env, ttlMs });
912
+ // Broker actually LISTENING → deterministic synchronous warm (bounded; the read
913
+ // already paid a Touch ID, so <1s here is invisible). We gate on a real liveness
914
+ // ping, NOT mere socket-file existence: a broker that died leaving its socket
915
+ // behind (crash, OOM, or the version-skew teardown in this file) would otherwise
916
+ // drag this FOREGROUND read through the worker's 20s cold-start budget on every
917
+ // read. A dead/stale socket fails the ping fast, so we drop straight to the
918
+ // detached path (which does the cold-start + stale-socket cleanup off the hot
919
+ // path) — restoring "a dead broker costs the foreground read nothing".
920
+ if (agentReachableSync()) {
921
+ try {
922
+ const { cmd, args } = cliSpawn(['secrets', '_agent-load']);
923
+ const r = spawnSync(cmd, args, { input: payload, timeout: 3000, stdio: ['pipe', 'ignore', 'ignore'] });
924
+ if (!r.error && r.status === 0)
925
+ return;
926
+ }
927
+ catch {
928
+ // fall through to the detached best-effort path
929
+ }
930
+ }
836
931
  try {
837
932
  const { cmd, args } = cliSpawn(['secrets', '_agent-load']);
838
933
  const worker = spawn(cmd, args, { stdio: ['pipe', 'ignore', 'ignore'], detached: true });
839
- worker.stdin?.write(JSON.stringify({ name, bundle, env, ttlMs }));
934
+ worker.stdin?.write(payload);
840
935
  worker.stdin?.end();
841
936
  worker.unref();
842
937
  }
@@ -847,8 +942,14 @@ export function agentAutoLoadSync(name, bundle, env, ttlMs) {
847
942
  /**
848
943
  * Body of the hidden `secrets _agent-load` worker. Reads one `{name, bundle,
849
944
  * env, ttlMs}` payload from stdin, ensures the broker is up (robust, generous
850
- * budget), and loads the bundle into it. Detached from the originating read, so
851
- * its latency is invisible — which is why it can afford a long ensure budget.
945
+ * budget), and loads the bundle into it.
946
+ *
947
+ * Exit code is load-truthful: 0 ONLY when the bundle was actually loaded into a
948
+ * reachable broker; non-zero on any failure (malformed payload, broker couldn't
949
+ * be brought up, or the load transport failed). The synchronous caller
950
+ * (agentAutoLoadSync) relies on this to decide whether to skip the detached
951
+ * fallback — a bare "process exited 0" would otherwise be a false-positive
952
+ * success that silently reintroduces the very re-prompt storm this path fixes.
852
953
  */
853
954
  export async function runAgentLoadFromStdin() {
854
955
  if (!onDarwin())
@@ -861,15 +962,22 @@ export async function runAgentLoadFromStdin() {
861
962
  payload = JSON.parse(Buffer.concat(chunks).toString('utf-8'));
862
963
  }
863
964
  catch {
864
- return; // malformed payload — nothing to load
965
+ process.exitCode = 1; // malformed payload — nothing loaded
966
+ return;
865
967
  }
866
- if (!payload || !payload.name || !payload.bundle || !payload.env)
968
+ if (!payload || !payload.name || !payload.bundle || !payload.env) {
969
+ process.exitCode = 1;
867
970
  return;
971
+ }
868
972
  // Generous budget: the broker is a cold-starting full CLI; under load it can
869
973
  // take several seconds to bind. We're detached, so waiting costs nothing.
870
- if (!(await ensureAgentRunning(20000)))
974
+ if (!(await ensureAgentRunning(20000))) {
975
+ process.exitCode = 1; // broker couldn't be brought up — did NOT load
871
976
  return;
872
- await agentLoad(payload.name, payload.bundle, payload.env, payload.ttlMs ?? DEFAULT_TTL_MS);
977
+ }
978
+ const loaded = await agentLoad(payload.name, payload.bundle, payload.env, payload.ttlMs ?? DEFAULT_TTL_MS);
979
+ if (!loaded)
980
+ process.exitCode = 1; // transport failed — did NOT load
873
981
  }
874
982
  /** Store a resolved bundle in the broker. Returns false on transport failure. */
875
983
  export async function agentLoad(name, bundle, env, ttlMs) {
@@ -221,6 +221,32 @@ export declare function assertRemoteBundleFlagsUnsupported(bundleName: string, h
221
221
  allowExpiredFlag: string;
222
222
  }): void;
223
223
  export declare function resolveBundleEnv(bundle: SecretsBundle, _opts?: ResolveBundleOptions): Record<string, string>;
224
+ /**
225
+ * True when the current process is a background / non-interactive context that
226
+ * must NEVER raise a Keychain biometry prompt on the interactive user's screen —
227
+ * a prompt nobody is watching. Two signals, either sufficient:
228
+ * - `AGENTS_RUNTIME` is `headless` or `teams` (set on the child env by
229
+ * `agents run --headless`, scheduled routines, and teammates — see
230
+ * exec.ts:resolveInteractive, runner.ts, teams/agents.ts).
231
+ * - neither stdin nor stdout is a TTY (a detached/backgrounded task whose
232
+ * stdio is redirected to a log — e.g. a release script run in the
233
+ * background as `( ... ) >log 2>&1 </dev/null`).
234
+ * `AGENTS_SECRETS_NO_PROMPT=1` forces headless-safe; `=0` force-allows a prompt
235
+ * even in a non-TTY context. An interactive `eval "$(agents secrets export X)"`
236
+ * keeps its terminal stdin, so it is NOT classified headless and still prompts.
237
+ *
238
+ * Only **macOS keychain** reads pop an interactive Touch ID sheet — the secrets
239
+ * broker itself is a no-op off darwin (see agent.ts), and libsecret (Linux) /
240
+ * the Windows credential store resolve without any prompt. So off-darwin this
241
+ * ALWAYS returns false: forcing broker-only there would break every headless
242
+ * Linux/Windows read (CI, `agents run --headless`, routines, the Linux-driven
243
+ * release flow) for no benefit — there is no prompt to suppress.
244
+ *
245
+ * A read in a macOS headless context resolves broker-only (agentOnly) and fails
246
+ * fast with an actionable error instead of hijacking Touch ID. This generalizes
247
+ * the per-caller pattern already used by the daemon (daemon.ts:readDaemonClaudeOAuthToken).
248
+ */
249
+ export declare function isHeadlessSecretsContext(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
224
250
  /**
225
251
  * Read a bundle's metadata AND resolve its env in a single Touch ID prompt.
226
252
  *
@@ -25,7 +25,7 @@ import { deleteKeychainToken, getKeychainToken, getKeychainTokens, hasKeychainTo
25
25
  import { fileStore } from './filestore.js';
26
26
  import { emit } from '../events.js';
27
27
  import { readMeta } from '../state.js';
28
- import { agentGetSync, agentAutoLoadSync, agentGetMetaSync, agentAutoLoadMetaSync, agentEvictSync, secretsAgentAutoEnabled, DEFAULT_TTL_MS } from './agent.js';
28
+ import { agentGetSync, agentAutoLoadSync, agentGetMetaSync, agentAutoLoadMetaSync, agentEvictSync, secretsAgentAutoEnabled, secretsHoldMs } from './agent.js';
29
29
  import { createHash } from 'node:crypto';
30
30
  const keychainStore = {
31
31
  has: hasKeychainToken,
@@ -492,8 +492,11 @@ export function listBundles() {
492
492
  for (const bundle of keychainBundles)
493
493
  out.push(bundle);
494
494
  // Populate the broker for the rest of the hold window (fire-and-forget).
495
+ // Same configurable cap as the value read-path — otherwise `secrets list`
496
+ // would keep serving a stale metadata snapshot for 7d even when the user
497
+ // capped the hold at 24h via secrets.agent.holdMs.
495
498
  if (useAgent && keychainBundles.length > 0) {
496
- agentAutoLoadMetaSync(nameSetHash, keychainBundles, DEFAULT_TTL_MS);
499
+ agentAutoLoadMetaSync(nameSetHash, keychainBundles, secretsHoldMs());
497
500
  }
498
501
  }
499
502
  }
@@ -705,6 +708,44 @@ export function resolveBundleEnv(bundle, _opts = {}) {
705
708
  void _opts.caller;
706
709
  return env;
707
710
  }
711
+ /**
712
+ * True when the current process is a background / non-interactive context that
713
+ * must NEVER raise a Keychain biometry prompt on the interactive user's screen —
714
+ * a prompt nobody is watching. Two signals, either sufficient:
715
+ * - `AGENTS_RUNTIME` is `headless` or `teams` (set on the child env by
716
+ * `agents run --headless`, scheduled routines, and teammates — see
717
+ * exec.ts:resolveInteractive, runner.ts, teams/agents.ts).
718
+ * - neither stdin nor stdout is a TTY (a detached/backgrounded task whose
719
+ * stdio is redirected to a log — e.g. a release script run in the
720
+ * background as `( ... ) >log 2>&1 </dev/null`).
721
+ * `AGENTS_SECRETS_NO_PROMPT=1` forces headless-safe; `=0` force-allows a prompt
722
+ * even in a non-TTY context. An interactive `eval "$(agents secrets export X)"`
723
+ * keeps its terminal stdin, so it is NOT classified headless and still prompts.
724
+ *
725
+ * Only **macOS keychain** reads pop an interactive Touch ID sheet — the secrets
726
+ * broker itself is a no-op off darwin (see agent.ts), and libsecret (Linux) /
727
+ * the Windows credential store resolve without any prompt. So off-darwin this
728
+ * ALWAYS returns false: forcing broker-only there would break every headless
729
+ * Linux/Windows read (CI, `agents run --headless`, routines, the Linux-driven
730
+ * release flow) for no benefit — there is no prompt to suppress.
731
+ *
732
+ * A read in a macOS headless context resolves broker-only (agentOnly) and fails
733
+ * fast with an actionable error instead of hijacking Touch ID. This generalizes
734
+ * the per-caller pattern already used by the daemon (daemon.ts:readDaemonClaudeOAuthToken).
735
+ */
736
+ export function isHeadlessSecretsContext(env = process.env, platform = process.platform) {
737
+ if (platform !== 'darwin')
738
+ return false; // no biometry prompt to suppress off-darwin
739
+ const override = env.AGENTS_SECRETS_NO_PROMPT;
740
+ if (override === '1')
741
+ return true;
742
+ if (override === '0')
743
+ return false;
744
+ const runtime = env.AGENTS_RUNTIME;
745
+ if (runtime === 'headless' || runtime === 'teams')
746
+ return true;
747
+ return !process.stdin.isTTY && !process.stdout.isTTY;
748
+ }
708
749
  /**
709
750
  * Read a bundle's metadata AND resolve its env in a single Touch ID prompt.
710
751
  *
@@ -747,8 +788,15 @@ export function readAndResolveBundleEnv(name, opts = {}) {
747
788
  return filtered;
748
789
  }
749
790
  }
750
- if (opts.agentOnly) {
751
- throw new Error(`Secrets bundle '${name}' is not unlocked in the secrets agent.`);
791
+ // Only keychain-backed bundles can pop a Touch ID prompt and are the only ones
792
+ // the broker ever holds. A file-backed bundle resolves via passphrase with no
793
+ // prompt, so agentOnly must never block it — the broker never holds file
794
+ // bundles, so the throw would fire unconditionally and break a legitimate read.
795
+ if (opts.agentOnly && backend === 'keychain') {
796
+ throw new Error(`Secrets bundle '${name}' is not unlocked in the secrets agent, and this is a ` +
797
+ `headless/background process that must not raise a Touch ID prompt on the ` +
798
+ `interactive user's screen. Run 'agents secrets unlock ${name}' in a terminal ` +
799
+ `first, or set AGENTS_SECRETS_NO_PROMPT=0 to force an interactive prompt.`);
752
800
  }
753
801
  if (backend === 'file')
754
802
  assertFileBackendUsable(name);
@@ -881,15 +929,18 @@ export function readAndResolveBundleEnv(name, opts = {}) {
881
929
  emitReadAudit('success');
882
930
  // Auto-cache: this was a real keychain read (the agent fast-path returned
883
931
  // earlier on a hit). If the bundle opts into the `daily` policy and the user
884
- // enabled `secrets.agent.auto`, populate the broker in the background so the
885
- // next concurrent run reads silently. Skipped when noAgent (e.g. `unlock`,
886
- // which loads the agent itself). Fire-and-forget never blocks this read.
932
+ // enabled `secrets.agent.auto`, populate the broker so the next concurrent
933
+ // run reads silently. Skipped when noAgent (e.g. `unlock`, which loads the
934
+ // agent itself). When a broker is already up this warms it synchronously
935
+ // (bounded ~3s) so `daily` reliably sticks; only a cold-start broker uses the
936
+ // detached fire-and-forget path (see agentAutoLoadSync). The costly Touch ID
937
+ // prompt already happened, so the bounded wait is invisible.
887
938
  if (backend === 'keychain' &&
888
939
  !opts.noAgent &&
889
940
  process.env.AGENTS_SECRETS_NO_AGENT !== '1' &&
890
941
  bundlePolicy(bundle) === 'daily' &&
891
942
  secretsAgentAutoEnabled()) {
892
- agentAutoLoadSync(name, bundle, env, DEFAULT_TTL_MS);
943
+ agentAutoLoadSync(name, bundle, env, secretsHoldMs());
893
944
  }
894
945
  return { bundle, env };
895
946
  }
@@ -24,7 +24,7 @@
24
24
  * unit-testable in-process.
25
25
  */
26
26
  import * as readline from 'readline';
27
- import { listBundles, readAndResolveBundleEnv, readBundle, validateBundleName, } from './bundles.js';
27
+ import { listBundles, readAndResolveBundleEnv, isHeadlessSecretsContext, readBundle, validateBundleName, } from './bundles.js';
28
28
  /** MCP protocol revision this server negotiates. */
29
29
  export const MCP_PROTOCOL_VERSION = '2024-11-05';
30
30
  /** The single tool this server exposes. */
@@ -68,7 +68,9 @@ export function resolveSecret(bundle, key) {
68
68
  throw new Error(`Key '${key}' not found in bundle '${bundle}'.` +
69
69
  (available.length ? ` Available keys: ${available.join(', ')}.` : ' Bundle has no keys.'));
70
70
  }
71
- const { env } = readAndResolveBundleEnv(bundle, { caller: 'secrets-mcp' });
71
+ // The MCP get_secret tool is typically served by a background/headless agent
72
+ // process; resolve broker-only there so it never raises an unwatched prompt.
73
+ const { env } = readAndResolveBundleEnv(bundle, { caller: 'secrets-mcp', agentOnly: isHeadlessSecretsContext() });
72
74
  const value = env[key];
73
75
  if (value === undefined) {
74
76
  throw new Error(`Key '${key}' in bundle '${bundle}' could not be resolved.`);
@@ -16,6 +16,23 @@
16
16
  * in argv / `ps` / remote shell history. Nothing is persisted locally.
17
17
  */
18
18
  import { type SshExecResult } from '../ssh-exec.js';
19
+ /**
20
+ * Trust boundary for a remote-resolved env map. A peer's `secrets export` output
21
+ * is untrusted input: a compromised or misconfigured host could return keys that
22
+ * silently reshape THIS process's behavior once merged into the agent env
23
+ * (bundles.ts:251 `sanitizeProcessEnv` only strips loader vars from process.env,
24
+ * never the remote bundle). Block the dangerous-override classes here — at the
25
+ * source — so every consumer (`run --secrets b@host`, `secrets exec --host`) is
26
+ * protected, not just one call site:
27
+ * - LD_* / DYLD_* / NODE_OPTIONS and the other loader/interpreter injections
28
+ * (reuses the canonical bundles.ts predicate);
29
+ * - GIT_* — GIT_SSH_COMMAND et al. hijack every git subprocess;
30
+ * - *_PROXY — HTTP(S)_PROXY / ALL_PROXY reroute outbound traffic (MITM);
31
+ * - *_BASE_URL — ANTHROPIC_BASE_URL / OPENAI_BASE_URL redirect the model API.
32
+ * These keys are already rejected on the ADD side (validateEnvKey for loaders),
33
+ * so a legitimate bundle never carries them — only a hostile peer would.
34
+ */
35
+ export declare function isDangerousRemoteEnvKey(name: string): boolean;
19
36
  /**
20
37
  * Resolve a `--host` value to an ssh target string. Tries the `agents hosts`
21
38
  * registry first (enrolled name → ssh-config alias / `user@host`); on a miss,
@@ -21,7 +21,36 @@ import { emit } from '../events.js';
21
21
  import { sshTargetFor } from '../hosts/types.js';
22
22
  import { buildRemoteAgentsInvocation } from '../hosts/remote-cmd.js';
23
23
  import { resolveRemoteOsSync } from '../hosts/remote-os.js';
24
+ import { isLoaderOrInterpreterEnv } from './bundles.js';
24
25
  const REMOTE_TIMEOUT_MS = 30_000;
26
+ /**
27
+ * Trust boundary for a remote-resolved env map. A peer's `secrets export` output
28
+ * is untrusted input: a compromised or misconfigured host could return keys that
29
+ * silently reshape THIS process's behavior once merged into the agent env
30
+ * (bundles.ts:251 `sanitizeProcessEnv` only strips loader vars from process.env,
31
+ * never the remote bundle). Block the dangerous-override classes here — at the
32
+ * source — so every consumer (`run --secrets b@host`, `secrets exec --host`) is
33
+ * protected, not just one call site:
34
+ * - LD_* / DYLD_* / NODE_OPTIONS and the other loader/interpreter injections
35
+ * (reuses the canonical bundles.ts predicate);
36
+ * - GIT_* — GIT_SSH_COMMAND et al. hijack every git subprocess;
37
+ * - *_PROXY — HTTP(S)_PROXY / ALL_PROXY reroute outbound traffic (MITM);
38
+ * - *_BASE_URL — ANTHROPIC_BASE_URL / OPENAI_BASE_URL redirect the model API.
39
+ * These keys are already rejected on the ADD side (validateEnvKey for loaders),
40
+ * so a legitimate bundle never carries them — only a hostile peer would.
41
+ */
42
+ export function isDangerousRemoteEnvKey(name) {
43
+ const upper = name.toUpperCase();
44
+ if (isLoaderOrInterpreterEnv(upper))
45
+ return true;
46
+ if (upper.startsWith('GIT_'))
47
+ return true;
48
+ if (upper.endsWith('_PROXY'))
49
+ return true;
50
+ if (upper.endsWith('_BASE_URL'))
51
+ return true;
52
+ return false;
53
+ }
25
54
  /** Remote OS for a host name or target string. Prefer the original host name
26
55
  * because enrolled inline hosts resolve to `user@address`, while the OS
27
56
  * registry is keyed by the host name. */
@@ -153,9 +182,20 @@ export async function remoteResolveEnv(target, bundle, opts = {}) {
153
182
  throw new Error(`Unexpected payload resolving '${bundle}' on ${target}.`);
154
183
  }
155
184
  const env = {};
185
+ const blocked = [];
156
186
  for (const [k, v] of Object.entries(parsed)) {
187
+ // Drop dangerous-override keys returned by the (untrusted) peer before they
188
+ // can reshape this process — see isDangerousRemoteEnvKey.
189
+ if (isDangerousRemoteEnvKey(k)) {
190
+ blocked.push(k);
191
+ continue;
192
+ }
157
193
  env[k] = typeof v === 'string' ? v : String(v);
158
194
  }
195
+ if (blocked.length > 0) {
196
+ process.stderr.write(`[secrets] Dropped ${blocked.length} dangerous key(s) from '${bundle}'@${target} ` +
197
+ `(remote override blocked): ${blocked.join(', ')}\n`);
198
+ }
159
199
  // The remote host audits its own `secrets export` read; this emit records the
160
200
  // event on the INITIATING host too (values were pulled into this process and
161
201
  // injected locally). Covers `secrets exec --host` and `run --secrets b@host`.
@@ -83,6 +83,26 @@ export declare function installPackageIntoPrefix(spec: string, prefix: string):
83
83
  * package's postinstall hook.
84
84
  */
85
85
  export declare function installPackageWithBun(spec: string): Promise<void>;
86
+ /**
87
+ * Verify a downloaded tarball's bytes against a Subresource Integrity (SRI)
88
+ * string of the form `sha512-<base64>` — npm's `dist.integrity`. Recomputes the
89
+ * digest over the actual bytes with the named algorithm and compares it, in
90
+ * constant time, to the decoded expected digest.
91
+ *
92
+ * Fails closed: a malformed SRI, an algorithm weaker than sha512, or any digest
93
+ * mismatch throws. This is the gate that makes self-update refuse a tampered or
94
+ * corrupted tarball *before* it is ever handed to a package manager to install.
95
+ */
96
+ export declare function verifyTarballIntegrity(tarball: Buffer, integrity: string): void;
97
+ /**
98
+ * Download the published tarball at `tarballUrl` and prove its bytes match
99
+ * `integrity` before returning a path to it on disk. The returned .tgz is safe
100
+ * to hand to `npm install`/`bun add` — it has been verified byte-for-byte
101
+ * against the registry attestation. Fails closed: a non-200, a download error,
102
+ * or a hash mismatch throws and no file path is returned, so the caller never
103
+ * installs an unverified artifact.
104
+ */
105
+ export declare function downloadVerifiedTarball(tarballUrl: string, integrity: string, timeoutMs?: number): Promise<string>;
86
106
  /** Read the version field of the package.json at `packageRoot`, fresh from disk. */
87
107
  export declare function readInstalledVersion(packageRoot: string): string;
88
108
  /**
@@ -14,6 +14,7 @@
14
14
  import * as fs from 'fs';
15
15
  import * as os from 'os';
16
16
  import * as path from 'path';
17
+ import { createHash, timingSafeEqual } from 'crypto';
17
18
  import { spawnSync } from 'child_process';
18
19
  import { compareVersions } from './versions.js';
19
20
  import { needsWindowsShell } from './platform/index.js';
@@ -164,7 +165,59 @@ export async function installPackageWithBun(spec) {
164
165
  const { promisify } = await import('util');
165
166
  const execFileAsync = promisify(execFile);
166
167
  // On Windows `bun` resolves to `bun.exe`/`bun.cmd`; force shell for the .cmd case.
167
- await execFileAsync('bun', ['add', '-g', spec], { shell: needsWindowsShell('bun') });
168
+ // --ignore-scripts: the tarball has already been integrity-verified, but its
169
+ // lifecycle scripts must not run at install time (the caller refreshes shims
170
+ // explicitly via refreshAliasShims()) — same fail-closed posture as the npm path.
171
+ await execFileAsync('bun', ['add', '-g', spec, '--ignore-scripts'], { shell: needsWindowsShell('bun') });
172
+ }
173
+ /**
174
+ * Verify a downloaded tarball's bytes against a Subresource Integrity (SRI)
175
+ * string of the form `sha512-<base64>` — npm's `dist.integrity`. Recomputes the
176
+ * digest over the actual bytes with the named algorithm and compares it, in
177
+ * constant time, to the decoded expected digest.
178
+ *
179
+ * Fails closed: a malformed SRI, an algorithm weaker than sha512, or any digest
180
+ * mismatch throws. This is the gate that makes self-update refuse a tampered or
181
+ * corrupted tarball *before* it is ever handed to a package manager to install.
182
+ */
183
+ export function verifyTarballIntegrity(tarball, integrity) {
184
+ const dash = integrity.indexOf('-');
185
+ if (dash <= 0) {
186
+ throw new Error(`malformed integrity string: ${JSON.stringify(integrity)}`);
187
+ }
188
+ const algorithm = integrity.slice(0, dash);
189
+ const expectedBase64 = integrity.slice(dash + 1);
190
+ // npm publishes sha512 SRI; refuse to verify against anything weaker rather
191
+ // than silently accepting a downgraded (e.g. sha1) attestation.
192
+ if (algorithm !== 'sha512') {
193
+ throw new Error(`unsupported integrity algorithm '${algorithm}' (expected sha512)`);
194
+ }
195
+ const expected = Buffer.from(expectedBase64, 'base64');
196
+ const actual = createHash('sha512').update(tarball).digest();
197
+ if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
198
+ throw new Error(`integrity check failed: tarball hash sha512-${actual.toString('base64')} ` +
199
+ `does not match expected ${integrity}`);
200
+ }
201
+ }
202
+ /**
203
+ * Download the published tarball at `tarballUrl` and prove its bytes match
204
+ * `integrity` before returning a path to it on disk. The returned .tgz is safe
205
+ * to hand to `npm install`/`bun add` — it has been verified byte-for-byte
206
+ * against the registry attestation. Fails closed: a non-200, a download error,
207
+ * or a hash mismatch throws and no file path is returned, so the caller never
208
+ * installs an unverified artifact.
209
+ */
210
+ export async function downloadVerifiedTarball(tarballUrl, integrity, timeoutMs = 60_000) {
211
+ const response = await fetch(tarballUrl, { signal: AbortSignal.timeout(timeoutMs) });
212
+ if (!response.ok) {
213
+ throw new Error(`could not download tarball from ${tarballUrl} (HTTP ${response.status})`);
214
+ }
215
+ const tarball = Buffer.from(await response.arrayBuffer());
216
+ verifyTarballIntegrity(tarball, integrity);
217
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'agents-cli-upgrade-'));
218
+ const file = path.join(dir, path.basename(new URL(tarballUrl).pathname) || 'package.tgz');
219
+ fs.writeFileSync(file, tarball);
220
+ return file;
168
221
  }
169
222
  /** Read the version field of the package.json at `packageRoot`, fresh from disk. */
170
223
  export function readInstalledVersion(packageRoot) {
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Authenticated `agents serve --control` server — the "anchor" the iOS/iPadOS
3
+ * cockpit talks to (RUSH-1731).
4
+ *
5
+ * It is the read-only {@link handleServeGet} surface plus a small mutation API,
6
+ * gated by a bearer token ({@link verifyControlToken}) on EVERY request because,
7
+ * unlike plain `agents serve`, this variant may bind beyond loopback (a tailnet
8
+ * address) so the phone can reach it. It adds no new execution machinery: a run
9
+ * request re-invokes the same `agents run` the CLI uses (inheriting host
10
+ * offload, secrets, detached dispatch), and a message re-invokes `agents
11
+ * message`. The server generates the run's session id so the run is immediately
12
+ * addressable for streaming (Phase 2) and steering.
13
+ *
14
+ * Both side-effecting operations are behind DI seams ({@link ControlOptions})
15
+ * so tests drive them without spawning real agents — mirroring the `snapshot`
16
+ * seam on the read-only server.
17
+ */
18
+ import type http from 'http';
19
+ import type { StdioOptions } from 'child_process';
20
+ import { type ServeOptions } from './server.js';
21
+ /** A request to start an agent run on this anchor (local or offloaded). */
22
+ export interface RunRequest {
23
+ /** Agent to run: 'claude' | 'codex' | 'gemini' | … */
24
+ agent: string;
25
+ /** Headless prompt. */
26
+ prompt: string;
27
+ /** Permission mode: plan | edit | auto | skip. Defaults to the CLI default. */
28
+ mode?: string;
29
+ /** Offload onto a registered device / host (the executor). Omit → run on the anchor. */
30
+ host?: string;
31
+ /** Working directory (on the host when `host` is set). */
32
+ cwd?: string;
33
+ }
34
+ /** Identifiers by which the started run can later be streamed / messaged. */
35
+ export interface RunResult {
36
+ /** Addressable session id (a UUID for claude; otherwise equals `name`). */
37
+ sessionId: string;
38
+ /** Durable run name seeded into the session label. */
39
+ name: string;
40
+ }
41
+ /** Starts a run and returns its addressable ids. */
42
+ export type RunDispatcher = (req: RunRequest) => Promise<RunResult>;
43
+ /** Delivers a message to a running/parked agent by id. */
44
+ export type Messenger = (id: string, text: string, from?: string) => Promise<void>;
45
+ export interface ControlOptions extends ServeOptions {
46
+ /** Bearer verifier. Defaults to {@link verifyControlToken}. */
47
+ verifyToken?: (presented: string | undefined) => boolean;
48
+ /** Run starter. Defaults to spawning `agents run …` detached. */
49
+ runner?: RunDispatcher;
50
+ /** Message sender. Defaults to spawning `agents message …`. */
51
+ messenger?: Messenger;
52
+ /** Poll cadence (ms) for the session event stream. Defaults to 300. */
53
+ streamPollMs?: number;
54
+ /** Resolve a session id to its NDJSON capture file. Defaults to {@link streamLogPath}. */
55
+ streamLogPathFor?: (sessionId: string) => string;
56
+ }
57
+ /**
58
+ * SSE stream of a session's normalized NDJSON events, offset-tailed from its
59
+ * capture file. Resumes from `?offset=<bytes>` or the `Last-Event-ID` header
60
+ * (each event's `id:` is its exact byte offset, so resume neither loses nor
61
+ * duplicates). Closes when a terminal (`result`/`error`) event is seen or the
62
+ * client disconnects. A not-yet-created file simply yields nothing until the
63
+ * run starts writing — the phone can open the stream the instant it dispatches.
64
+ */
65
+ export declare function startSessionStream(file: string, req: http.IncomingMessage, res: http.ServerResponse, pollMs: number): void;
66
+ /**
67
+ * Spawn a detached child and settle once it has either successfully spawned or
68
+ * failed to. Attaching an `'error'` listener is mandatory: a `ChildProcess` is
69
+ * an `EventEmitter`, so an `'error'` event (e.g. ENOENT from a stale/missing
70
+ * `agents` binary — a real condition `validateDaemonBinary` guards against)
71
+ * with no listener throws and would take down the whole anchor process, not
72
+ * just the one request. Mirrors the `once('spawn') / once('error')` pattern in
73
+ * `teams/agents.ts`. The `'error'` listener registered by `once` survives after
74
+ * a successful spawn, so a later error is absorbed rather than crashing.
75
+ */
76
+ export declare function spawnDetached(command: string, args: string[], stdio?: StdioOptions): Promise<void>;
77
+ /**
78
+ * Default run dispatcher: re-invoke `agents run` with a server-minted id so the
79
+ * run is addressable, detached so it outlives this request (and the anchor
80
+ * process). Local runs detach here; `--host` runs detach on the executor via
81
+ * the existing dispatch path.
82
+ */
83
+ export declare const defaultRunner: RunDispatcher;
84
+ /** Default messenger: re-invoke `agents message <id> <text>` and await its exit. */
85
+ export declare const defaultMessenger: Messenger;
86
+ /**
87
+ * Create (but do not start) the authenticated control server. Caller invokes
88
+ * `.listen(port, bind)`.
89
+ */
90
+ export declare function createControlServer(opts?: ControlOptions): http.Server;
91
+ /** Start the control server on `port`, bound to `bind`. Resolves the bound port. */
92
+ export declare function startControlServer(port: number, bind: string, opts?: ControlOptions): Promise<{
93
+ server: http.Server;
94
+ port: number;
95
+ }>;