@phnx-labs/agents-cli 1.20.65 → 1.20.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +119 -1
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/exec.d.ts +48 -0
  5. package/dist/commands/exec.js +65 -0
  6. package/dist/commands/monitors.js +8 -0
  7. package/dist/commands/plugins.js +28 -7
  8. package/dist/commands/sessions-browser.d.ts +82 -0
  9. package/dist/commands/sessions-browser.js +320 -0
  10. package/dist/commands/sessions.d.ts +1 -0
  11. package/dist/commands/sessions.js +121 -4
  12. package/dist/commands/share.d.ts +2 -0
  13. package/dist/commands/share.js +150 -0
  14. package/dist/commands/ssh.js +9 -1
  15. package/dist/index.js +2 -1
  16. package/dist/lib/agents.d.ts +28 -0
  17. package/dist/lib/agents.js +68 -0
  18. package/dist/lib/devices/connect.d.ts +18 -1
  19. package/dist/lib/devices/connect.js +10 -2
  20. package/dist/lib/devices/known-hosts.d.ts +62 -0
  21. package/dist/lib/devices/known-hosts.js +137 -0
  22. package/dist/lib/hosts/dispatch.js +20 -2
  23. package/dist/lib/hosts/remote-cmd.js +8 -4
  24. package/dist/lib/monitors/engine.js +4 -0
  25. package/dist/lib/monitors/sources/device.js +13 -3
  26. package/dist/lib/picker.d.ts +53 -0
  27. package/dist/lib/picker.js +214 -1
  28. package/dist/lib/plugins.d.ts +31 -1
  29. package/dist/lib/plugins.js +74 -13
  30. package/dist/lib/secrets/agent.d.ts +35 -0
  31. package/dist/lib/secrets/agent.js +114 -55
  32. package/dist/lib/secrets/filestore.d.ts +9 -0
  33. package/dist/lib/secrets/filestore.js +21 -8
  34. package/dist/lib/secrets/index.d.ts +7 -0
  35. package/dist/lib/secrets/index.js +26 -6
  36. package/dist/lib/share/capture.d.ts +29 -0
  37. package/dist/lib/share/capture.js +140 -0
  38. package/dist/lib/share/config.d.ts +35 -0
  39. package/dist/lib/share/config.js +100 -0
  40. package/dist/lib/share/og.d.ts +25 -0
  41. package/dist/lib/share/og.js +84 -0
  42. package/dist/lib/share/provision.d.ts +10 -0
  43. package/dist/lib/share/provision.js +91 -0
  44. package/dist/lib/share/publish.d.ts +57 -0
  45. package/dist/lib/share/publish.js +145 -0
  46. package/dist/lib/share/worker-template.d.ts +2 -0
  47. package/dist/lib/share/worker-template.js +82 -0
  48. package/dist/lib/shims.d.ts +13 -0
  49. package/dist/lib/shims.js +42 -2
  50. package/dist/lib/ssh-exec.d.ts +24 -0
  51. package/dist/lib/ssh-exec.js +15 -3
  52. package/dist/lib/startup/command-registry.d.ts +1 -0
  53. package/dist/lib/startup/command-registry.js +2 -0
  54. package/dist/lib/types.d.ts +10 -0
  55. package/package.json +1 -1
@@ -12,7 +12,7 @@
12
12
  * display, and keyboard navigation. Used by sessions, teams, and other
13
13
  * interactive pickers throughout the CLI.
14
14
  */
15
- import { createPrompt, useState, useKeypress, useEffect, useMemo, usePagination, usePrefix, makeTheme, isEnterKey, isUpKey, isDownKey, isSpaceKey, Separator, } from '@inquirer/core';
15
+ import { createPrompt, useState, useKeypress, useEffect, useMemo, useRef, usePagination, usePrefix, makeTheme, isEnterKey, isUpKey, isDownKey, isSpaceKey, isBackspaceKey, Separator, } from '@inquirer/core';
16
16
  import chalk from 'chalk';
17
17
  import { stripVTControlCharacters } from 'node:util';
18
18
  const DEFAULT_TERMINAL_ROWS = 24;
@@ -324,3 +324,216 @@ export function multiItemPicker(config) {
324
324
  });
325
325
  return prompt(config);
326
326
  }
327
+ /**
328
+ * Async-refetch variant of {@link itemPicker}. Holds a `filter` object in state and
329
+ * re-runs `load(filter)` whenever a keybinding mutates it (with a loading placeholder
330
+ * while the fetch — e.g. an SSH fleet fan-out — is in flight). A separate `S` search
331
+ * mode filters the loaded pool client-side. `enter` returns the active row + the live
332
+ * filter; `esc` cancels (from search mode, `esc` first exits search).
333
+ *
334
+ * Same render/pagination/preview machinery as the static pickers — only the data
335
+ * source and keymap are dynamic.
336
+ */
337
+ export function dynamicPicker(config) {
338
+ const prompt = createPrompt((cfg, done) => {
339
+ const theme = makeTheme({});
340
+ const [status, setStatus] = useState('idle');
341
+ const [filter, setFilter] = useState(() => cfg.initialFilter);
342
+ const [items, setItems] = useState([]);
343
+ const [loading, setLoading] = useState(true);
344
+ const [query, setQuery] = useState('');
345
+ const [mode, setMode] = useState('nav');
346
+ const [previewOpen, setPreviewOpen] = useState(false);
347
+ const [active, setActive] = useState(0);
348
+ const [flash, setFlash] = useState('');
349
+ const prefix = usePrefix({ status, theme });
350
+ // Guards against a slow load resolving after a newer filter superseded it.
351
+ const gen = useRef(0);
352
+ useEffect(() => {
353
+ const my = ++gen.current;
354
+ setLoading(true);
355
+ Promise.resolve(cfg.load(filter))
356
+ .then((rows) => {
357
+ if (my !== gen.current)
358
+ return;
359
+ setItems(rows);
360
+ setLoading(false);
361
+ setActive(0);
362
+ })
363
+ .catch(() => {
364
+ if (my !== gen.current)
365
+ return;
366
+ setItems([]);
367
+ setLoading(false);
368
+ });
369
+ }, [filter]);
370
+ const results = useMemo(() => {
371
+ const q = query.trim();
372
+ const pool = q && cfg.matches ? items.filter((it) => cfg.matches(it, q)) : items;
373
+ return pool.slice(0, 200).map((item) => ({
374
+ value: item,
375
+ label: cfg.labelFor(item, q),
376
+ }));
377
+ }, [items, query]);
378
+ useEffect(() => {
379
+ if (active >= results.length)
380
+ setActive(0);
381
+ }, [results]);
382
+ const selected = results[active];
383
+ const finish = () => {
384
+ if (!selected)
385
+ return;
386
+ setStatus('done');
387
+ done({ item: selected.value, filter });
388
+ };
389
+ useKeypress((key, rl) => {
390
+ if (isEnterKey(key)) {
391
+ finish();
392
+ return;
393
+ }
394
+ // Search mode: we own the query buffer (readline's line doesn't survive
395
+ // across renders here, so we build it from key events). Clear readline
396
+ // every keystroke so nothing leaks, then append the typed character.
397
+ if (mode === 'search') {
398
+ if (key.name === 'escape') {
399
+ // Exit search but KEEP the query as an active filter, so hotkeys (and
400
+ // the y copy-cmd) operate on the searched view. A second esc in nav
401
+ // clears it. Enter also confirms the highlighted row directly.
402
+ rl.clearLine(0);
403
+ setMode('nav');
404
+ return;
405
+ }
406
+ if (isUpKey(key)) {
407
+ rl.clearLine(0);
408
+ if (results.length > 0)
409
+ setActive((active - 1 + results.length) % results.length);
410
+ return;
411
+ }
412
+ if (isDownKey(key)) {
413
+ rl.clearLine(0);
414
+ if (results.length > 0)
415
+ setActive((active + 1) % results.length);
416
+ return;
417
+ }
418
+ if (isBackspaceKey(key)) {
419
+ rl.clearLine(0);
420
+ setQuery(query.slice(0, -1));
421
+ return;
422
+ }
423
+ const seq = key.sequence;
424
+ rl.clearLine(0);
425
+ if (seq && seq.length === 1 && seq >= ' ' && !key.ctrl) {
426
+ setQuery(query + seq);
427
+ }
428
+ return;
429
+ }
430
+ // Nav mode: single keys are hotkeys — clear the readline buffer so a hotkey
431
+ // letter (r/b/c/…) never accumulates as stray input.
432
+ rl.clearLine(0);
433
+ if (key.name === 'escape') {
434
+ // First esc clears an active search filter; a second (no filter) cancels.
435
+ if (query) {
436
+ setQuery('');
437
+ return;
438
+ }
439
+ done(null);
440
+ return;
441
+ }
442
+ if (isUpKey(key)) {
443
+ if (results.length > 0)
444
+ setActive((active - 1 + results.length) % results.length);
445
+ return;
446
+ }
447
+ if (isDownKey(key)) {
448
+ if (results.length > 0)
449
+ setActive((active + 1) % results.length);
450
+ return;
451
+ }
452
+ if (flash)
453
+ setFlash('');
454
+ if (key.name === (cfg.searchKey ?? 's')) {
455
+ setMode('search');
456
+ return;
457
+ }
458
+ if (cfg.buildPreview && key.name === (cfg.previewKey ?? 'tab')) {
459
+ setPreviewOpen(!previewOpen);
460
+ return;
461
+ }
462
+ const binding = cfg.keyBindings?.[key.name ?? ''];
463
+ if (binding) {
464
+ const next = binding(filter);
465
+ if (!Object.is(next, filter))
466
+ setFilter(next);
467
+ return;
468
+ }
469
+ if (cfg.onKey) {
470
+ const msg = cfg.onKey(key.name ?? '', filter, selected?.value, query);
471
+ if (msg)
472
+ setFlash(msg);
473
+ }
474
+ });
475
+ const message = theme.style.message(cfg.message, status);
476
+ if (status === 'done') {
477
+ return `${prefix} ${message}`;
478
+ }
479
+ const headerBits = [prefix, message];
480
+ if (cfg.headerFor)
481
+ headerBits.push(chalk.gray(cfg.headerFor(filter)));
482
+ if (mode === 'search') {
483
+ headerBits.push(query ? chalk.cyan('/' + query) : chalk.gray('/ (type to filter)'));
484
+ }
485
+ else if (query) {
486
+ headerBits.push(chalk.cyan('/' + query));
487
+ }
488
+ const header = headerBits.filter(Boolean).join(' ');
489
+ const page = usePagination({
490
+ items: results,
491
+ active,
492
+ renderItem({ item, isActive }) {
493
+ if (Separator.isSeparator(item))
494
+ return ` ${item.separator}`;
495
+ const cursor = isActive ? chalk.cyan('>') : ' ';
496
+ const row = isActive ? chalk.bold(item.label) : item.label;
497
+ return `${cursor} ${row}`;
498
+ },
499
+ pageSize: cfg.pageSize ?? 12,
500
+ loop: false,
501
+ });
502
+ const help = chalk.gray(cfg.helpFor
503
+ ? cfg.helpFor(filter, mode)
504
+ : mode === 'search'
505
+ ? '↑↓ navigate · esc exit search · ⏎ ' + (cfg.enterHint ?? 'select')
506
+ : 's search · ↑↓ navigate · ⏎ ' + (cfg.enterHint ?? 'select') + ' · esc cancel');
507
+ const parts = [header];
508
+ if (loading) {
509
+ parts.push(chalk.gray(` ${cfg.loadingMessage ?? 'Loading…'}`));
510
+ }
511
+ else {
512
+ parts.push(page);
513
+ if (results.length === 0) {
514
+ parts.push(chalk.gray(` ${cfg.emptyMessage ?? 'No matches.'}`));
515
+ }
516
+ }
517
+ if (previewOpen && selected && cfg.buildPreview && !loading) {
518
+ const width = terminalWidth();
519
+ const separator = chalk.gray('─'.repeat(Math.min(width, 80)));
520
+ const flashRows = flash ? renderedRows(flash, width) : 0;
521
+ const fixedRows = renderedRows(header, width) +
522
+ renderedRows(parts.slice(1).join('\n'), width) +
523
+ renderedRows(separator, width) +
524
+ renderedRows(help, width) +
525
+ flashRows;
526
+ const availablePreviewRows = terminalRows() - fixedRows;
527
+ const preview = limitPreviewHeight(cfg.buildPreview(selected.value), availablePreviewRows, width);
528
+ if (preview) {
529
+ parts.push(separator);
530
+ parts.push(preview);
531
+ }
532
+ }
533
+ if (flash)
534
+ parts.push(chalk.green(flash));
535
+ parts.push(help);
536
+ return [header, parts.slice(1).join('\n')];
537
+ });
538
+ return prompt(config);
539
+ }
@@ -316,7 +316,37 @@ export declare function getUpstreamManifestVersion(info: PluginSourceInfo): stri
316
316
  * Update an installed plugin by re-pulling from its original source.
317
317
  * Returns true if the update succeeded.
318
318
  */
319
- export declare function updatePlugin(name: string): Promise<{
319
+ /**
320
+ * Labels of exec surfaces present in `after` that were NOT present in `before`.
321
+ * Used by updatePlugin to distinguish a newly-appearing execution surface
322
+ * (upstream compromise → renewed consent required) from one the user already
323
+ * trusted (leave enablement alone).
324
+ */
325
+ export declare function newExecSurfaceLabels(before: PluginCapabilities, after: PluginCapabilities): string[];
326
+ /**
327
+ * Re-fetch a plugin from its recorded source and apply the update to disk.
328
+ *
329
+ * Security (RUSH-1757): a plugin's upstream is mutable. `updatePlugin` never
330
+ * mutates the live plugin tree before it has inspected the incoming content —
331
+ * the new revision is fetched into a **quarantine** dir first, its capabilities
332
+ * are diffed against the current on-disk baseline, and the update is applied to
333
+ * `plugin.root` only after the trust decision. If the update introduces a NEW
334
+ * executable surface (hooks/, .mcp.json, bin/, scripts/, settings.json,
335
+ * permissions/) that the current revision did not carry, the update is refused
336
+ * unless `options.allowExecSurfaces` is set — the last-good content is kept in
337
+ * place, so a benign-then-compromised upstream can never execute on the next
338
+ * update without renewed consent. A surface the user already trusted is not a
339
+ * "new" surface and does not re-trigger the gate.
340
+ */
341
+ export declare function updatePlugin(name: string, options?: {
342
+ allowExecSurfaces?: boolean;
343
+ }): Promise<{
320
344
  success: boolean;
321
345
  error?: string;
346
+ /** True when the update was refused because it introduced new exec surfaces. */
347
+ blockedByExecSurfaces?: boolean;
348
+ /** Labels of exec surfaces newly introduced by this update, if any. */
349
+ newExecSurfaces?: string[];
350
+ /** Whether the applied revision carries executable surfaces (for the caller's re-sync). */
351
+ hasExecSurfaces?: boolean;
322
352
  }>;
@@ -1739,7 +1739,33 @@ export function getUpstreamManifestVersion(info) {
1739
1739
  * Update an installed plugin by re-pulling from its original source.
1740
1740
  * Returns true if the update succeeded.
1741
1741
  */
1742
- export async function updatePlugin(name) {
1742
+ /**
1743
+ * Labels of exec surfaces present in `after` that were NOT present in `before`.
1744
+ * Used by updatePlugin to distinguish a newly-appearing execution surface
1745
+ * (upstream compromise → renewed consent required) from one the user already
1746
+ * trusted (leave enablement alone).
1747
+ */
1748
+ export function newExecSurfaceLabels(before, after) {
1749
+ return Object.keys(PLUGIN_EXEC_SURFACE_LABELS)
1750
+ .filter((key) => after[key] && !before[key])
1751
+ .map((key) => PLUGIN_EXEC_SURFACE_LABELS[key]);
1752
+ }
1753
+ /**
1754
+ * Re-fetch a plugin from its recorded source and apply the update to disk.
1755
+ *
1756
+ * Security (RUSH-1757): a plugin's upstream is mutable. `updatePlugin` never
1757
+ * mutates the live plugin tree before it has inspected the incoming content —
1758
+ * the new revision is fetched into a **quarantine** dir first, its capabilities
1759
+ * are diffed against the current on-disk baseline, and the update is applied to
1760
+ * `plugin.root` only after the trust decision. If the update introduces a NEW
1761
+ * executable surface (hooks/, .mcp.json, bin/, scripts/, settings.json,
1762
+ * permissions/) that the current revision did not carry, the update is refused
1763
+ * unless `options.allowExecSurfaces` is set — the last-good content is kept in
1764
+ * place, so a benign-then-compromised upstream can never execute on the next
1765
+ * update without renewed consent. A surface the user already trusted is not a
1766
+ * "new" surface and does not re-trigger the gate.
1767
+ */
1768
+ export async function updatePlugin(name, options = {}) {
1743
1769
  const plugin = getPlugin(name);
1744
1770
  if (!plugin) {
1745
1771
  return { success: false, error: `Plugin '${name}' not found` };
@@ -1755,33 +1781,68 @@ export async function updatePlugin(name) {
1755
1781
  catch {
1756
1782
  return { success: false, error: `Could not read source info for '${name}'` };
1757
1783
  }
1784
+ // Baseline: what the live (already-trusted) revision ships today.
1785
+ const before = inspectPluginCapabilities(plugin.root);
1786
+ // Quarantine dir lives beside plugin.root (same filesystem) so the final
1787
+ // apply can be a rename. The dot-prefix keeps it out of plugin discovery.
1788
+ const quarantine = path.join(path.dirname(plugin.root), `.${path.basename(plugin.root)}.update-quarantine`);
1789
+ const cleanupQuarantine = () => {
1790
+ try {
1791
+ fs.rmSync(quarantine, { recursive: true, force: true });
1792
+ }
1793
+ catch { /* best effort */ }
1794
+ };
1795
+ cleanupQuarantine();
1758
1796
  try {
1797
+ // 1. Fetch the incoming revision into quarantine — never touch plugin.root yet.
1759
1798
  if (sourceInfo.isGit) {
1760
- execFileSync('git', ['-C', plugin.root, 'pull', '--ff-only'], { stdio: 'pipe' });
1799
+ // Copy the working checkout (with its .git) and fast-forward the copy, so a
1800
+ // hostile upstream diff lands in the quarantine, not the live tree.
1801
+ fs.cpSync(plugin.root, quarantine, { recursive: true });
1802
+ execFileSync('git', ['-C', quarantine, 'pull', '--ff-only'], { stdio: 'pipe' });
1761
1803
  }
1762
1804
  else {
1763
1805
  const resolvedSource = sourceInfo.source.replace(/^~/, homeDir());
1764
1806
  if (!fs.existsSync(resolvedSource)) {
1807
+ cleanupQuarantine();
1765
1808
  return { success: false, error: `Source path no longer exists: ${resolvedSource}` };
1766
1809
  }
1767
- // Preserve .user-config.json and .source during re-copy
1768
- const userConfigPath = path.join(plugin.root, USER_CONFIG_FILE);
1769
- const userConfigBackup = fs.existsSync(userConfigPath)
1770
- ? fs.readFileSync(userConfigPath, 'utf-8')
1771
- : null;
1772
- fs.rmSync(plugin.root, { recursive: true, force: true });
1773
- fs.cpSync(resolvedSource, plugin.root, { recursive: true });
1774
- if (userConfigBackup !== null) {
1775
- fs.writeFileSync(userConfigPath, userConfigBackup, 'utf-8');
1776
- }
1810
+ fs.cpSync(resolvedSource, quarantine, { recursive: true });
1811
+ }
1812
+ // 2. Diff capabilities of the incoming revision against the baseline.
1813
+ const after = inspectPluginCapabilities(quarantine);
1814
+ const newSurfaces = newExecSurfaceLabels(before, after);
1815
+ // 3. Refuse a surface-introducing update without renewed consent. The
1816
+ // last-good content stays in place untouched.
1817
+ if (newSurfaces.length > 0 && options.allowExecSurfaces !== true) {
1818
+ cleanupQuarantine();
1819
+ return {
1820
+ success: false,
1821
+ blockedByExecSurfaces: true,
1822
+ newExecSurfaces: newSurfaces,
1823
+ error: `Update refused: '${name}' introduces new executable surfaces (${newSurfaces.join(', ')}). ` +
1824
+ `Re-run with --allow-exec-surfaces if you trust the source.`,
1825
+ };
1826
+ }
1827
+ // 4. Apply: swap the quarantined revision into plugin.root, preserving the
1828
+ // user config and re-stamping .source.
1829
+ const userConfigPath = path.join(plugin.root, USER_CONFIG_FILE);
1830
+ const userConfigBackup = fs.existsSync(userConfigPath)
1831
+ ? fs.readFileSync(userConfigPath, 'utf-8')
1832
+ : null;
1833
+ fs.rmSync(plugin.root, { recursive: true, force: true });
1834
+ fs.renameSync(quarantine, plugin.root);
1835
+ if (userConfigBackup !== null) {
1836
+ fs.writeFileSync(userConfigPath, userConfigBackup, 'utf-8');
1777
1837
  }
1778
1838
  // Re-stamp .source with the freshly pulled manifest version so the baseline
1779
1839
  // tracks what's now on disk (keeps the heal "unmodified?" check honest).
1780
1840
  const freshVersion = loadPluginManifest(plugin.root)?.version;
1781
1841
  fs.writeFileSync(path.join(plugin.root, SOURCE_FILE), JSON.stringify({ ...sourceInfo, version: freshVersion }), 'utf-8');
1842
+ return { success: true, newExecSurfaces: newSurfaces, hasExecSurfaces: hasPluginExecSurfaces(after) };
1782
1843
  }
1783
1844
  catch (err) {
1845
+ cleanupQuarantine();
1784
1846
  return { success: false, error: err.message };
1785
1847
  }
1786
- return { success: true };
1787
1848
  }
@@ -23,6 +23,7 @@
23
23
  * macOS only: Linux libsecret has no biometry prompt, so there's nothing to
24
24
  * deduplicate — every entry point here no-ops off darwin.
25
25
  */
26
+ import * as net from 'net';
26
27
  import type { SecretsBundle } from './bundles.js';
27
28
  /** Default lifetime of an unlocked bundle when `--ttl` is not given. */
28
29
  export declare const DEFAULT_TTL_MS: number;
@@ -74,6 +75,13 @@ export interface AgentStatusEntry {
74
75
  expiresAt: number;
75
76
  keyCount: number;
76
77
  }
78
+ /**
79
+ * Read the current broker capability token, or null if none is present. Clients
80
+ * read it fresh per request and attach it to every non-ping command; a broker
81
+ * restart mints a new token, so a stale read simply fails authorization and the
82
+ * caller falls back to a direct keychain read (soft, never a hard error).
83
+ */
84
+ export declare function readAgentToken(): string | null;
77
85
  /** True if a legacy standalone-broker launchd plist is still installed. */
78
86
  export declare function secretsAgentServiceInstalled(): boolean;
79
87
  /**
@@ -97,17 +105,21 @@ export type Request = {
97
105
  } | {
98
106
  cmd: 'get';
99
107
  name: string;
108
+ token?: string;
100
109
  } | {
101
110
  cmd: 'load';
102
111
  name: string;
103
112
  bundle: SecretsBundle;
104
113
  env: Record<string, string>;
105
114
  ttlMs: number;
115
+ token?: string;
106
116
  } | {
107
117
  cmd: 'lock';
108
118
  name?: string;
119
+ token?: string;
109
120
  } | {
110
121
  cmd: 'status';
122
+ token?: string;
111
123
  };
112
124
  export type Response = {
113
125
  ok: true;
@@ -164,6 +176,28 @@ export declare function handleAgentRequest(store: Map<string, StoredBundle>, req
164
176
  * has direct regression coverage (the inline stdout handler isn't unit-testable).
165
177
  */
166
178
  export declare function shouldWipeOnWatchEvent(chunk: string): boolean;
179
+ /**
180
+ * Authorization gate applied to every request BEFORE handleAgentRequest touches
181
+ * the store (RUSH-1760). A bare same-UID socket connection is no longer trusted
182
+ * to load/get arbitrary bundle env: each command except the liveness `ping` must
183
+ * carry the per-broker capability token, which lives in a 0600 file inside the
184
+ * 0700 agent dir and so is readable only by the UID that owns the broker.
185
+ *
186
+ * Fail closed: a missing/empty expected token (no token file) rejects every
187
+ * command but ping. `ping` stays unauthenticated — it exposes only the protocol
188
+ * and cli version, and clients need it to detect a reachable broker before they
189
+ * have any reason to read the token. Pure + exported for direct unit testing.
190
+ */
191
+ export declare function isRequestAuthorized(req: Request, expectedToken: string | null): boolean;
192
+ type BrokerConnectionHandler = (conn: net.Socket) => void;
193
+ /**
194
+ * Build the socket `connection` handler shared by both brokers (standalone and
195
+ * daemon-hosted): newline-framed JSON in, one response line out, with the
196
+ * authorization gate (isRequestAuthorized) applied before `handle`. `token`
197
+ * resolves the currently-expected capability token per request so a token
198
+ * rotation on broker restart is picked up without rebuilding the handler.
199
+ */
200
+ export declare function makeConnectionHandler(handle: (req: Request) => Response, token: () => string | null): BrokerConnectionHandler;
167
201
  /**
168
202
  * Run the broker in the foreground. Spawned detached by ensureAgentRunning via
169
203
  * `agents secrets _agent-run`. Holds the store in memory, serves the socket,
@@ -325,3 +359,4 @@ export declare function agentPing(): Promise<{
325
359
  * gets starved under heavy load, so it's last.
326
360
  */
327
361
  export declare function ensureAgentRunning(timeoutMs?: number): Promise<boolean>;
362
+ export {};