@phnx-labs/agents-cli 1.22.46 → 1.22.47

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 (167) hide show
  1. package/CHANGELOG.md +279 -0
  2. package/LICENSE +102 -182
  3. package/README.md +31 -18
  4. package/dist/cli/command-registry.d.ts +1 -1
  5. package/dist/cli/command-registry.js +4 -2
  6. package/dist/commands/accounts.js +56 -12
  7. package/dist/commands/auth.js +7 -5
  8. package/dist/commands/browser-sessions-picker.js +9 -7
  9. package/dist/commands/browser.js +266 -15
  10. package/dist/commands/config.js +26 -4
  11. package/dist/commands/cost.d.ts +1 -1
  12. package/dist/commands/detach.d.ts +13 -0
  13. package/dist/commands/detach.js +11 -8
  14. package/dist/commands/exec.js +55 -38
  15. package/dist/commands/feed-watch.d.ts +2 -0
  16. package/dist/commands/feed-watch.js +28 -0
  17. package/dist/commands/feed.js +27 -0
  18. package/dist/commands/feedback.js +3 -19
  19. package/dist/commands/insights.d.ts +1 -1
  20. package/dist/commands/insights.js +2 -2
  21. package/dist/commands/lease.js +3 -3
  22. package/dist/commands/routines.js +1 -1
  23. package/dist/commands/sessions-picker-factory.d.ts +2 -1
  24. package/dist/commands/sessions-picker-factory.js +1 -1
  25. package/dist/commands/sessions-stop.d.ts +24 -0
  26. package/dist/commands/sessions-stop.js +66 -0
  27. package/dist/commands/sessions-trace.d.ts +82 -0
  28. package/dist/commands/sessions-trace.js +387 -0
  29. package/dist/commands/sessions.js +4 -0
  30. package/dist/commands/ssh.d.ts +15 -0
  31. package/dist/commands/ssh.js +194 -16
  32. package/dist/commands/teams.js +34 -2
  33. package/dist/commands/view.d.ts +2 -0
  34. package/dist/commands/view.js +18 -31
  35. package/dist/commands/watchdog.js +9 -14
  36. package/dist/lib/account-capabilities.d.ts +4 -0
  37. package/dist/lib/account-capabilities.js +13 -9
  38. package/dist/lib/account-registry.d.ts +2 -0
  39. package/dist/lib/account-registry.js +30 -2
  40. package/dist/lib/accounting/rotate.d.ts +9 -0
  41. package/dist/lib/accounting/rotate.js +12 -2
  42. package/dist/lib/accounting/usage.d.ts +70 -3
  43. package/dist/lib/accounting/usage.js +235 -63
  44. package/dist/lib/agent-modes.js +29 -1
  45. package/dist/lib/agent-spec/agents.d.ts +4 -3
  46. package/dist/lib/agent-spec/agents.js +14 -6
  47. package/dist/lib/analytics/mix-commands.d.ts +1 -1
  48. package/dist/lib/analytics/mix-commands.js +2 -2
  49. package/dist/lib/auth-health.d.ts +13 -3
  50. package/dist/lib/auth-health.js +50 -8
  51. package/dist/lib/browser/devices.d.ts +19 -0
  52. package/dist/lib/browser/devices.js +23 -0
  53. package/dist/lib/browser/ipc.d.ts +1 -1
  54. package/dist/lib/browser/ipc.js +33 -7
  55. package/dist/lib/browser/profiles.d.ts +106 -1
  56. package/dist/lib/browser/profiles.js +300 -17
  57. package/dist/lib/browser/remote-control.d.ts +26 -2
  58. package/dist/lib/browser/remote-control.js +33 -3
  59. package/dist/lib/browser/runtime-state.d.ts +9 -0
  60. package/dist/lib/browser/runtime-state.js +24 -8
  61. package/dist/lib/browser/service.d.ts +37 -0
  62. package/dist/lib/browser/service.js +78 -20
  63. package/dist/lib/browser/sessions-list.d.ts +1 -3
  64. package/dist/lib/browser/sessions-list.js +3 -16
  65. package/dist/lib/browser/types.d.ts +12 -1
  66. package/dist/lib/codex-policy.d.ts +11 -1
  67. package/dist/lib/codex-policy.js +20 -5
  68. package/dist/lib/config-keys.d.ts +4 -3
  69. package/dist/lib/config-keys.js +17 -7
  70. package/dist/lib/daemon/runner.js +15 -2
  71. package/dist/lib/daemon-ticks.js +2 -2
  72. package/dist/lib/device-config.d.ts +25 -8
  73. package/dist/lib/device-config.js +90 -17
  74. package/dist/lib/devices/config-migration.d.ts +7 -0
  75. package/dist/lib/devices/config-migration.js +67 -3
  76. package/dist/lib/devices/health.d.ts +27 -2
  77. package/dist/lib/devices/health.js +39 -9
  78. package/dist/lib/devices/interactive-host.d.ts +15 -0
  79. package/dist/lib/devices/interactive-host.js +53 -0
  80. package/dist/lib/devices/registry.d.ts +61 -4
  81. package/dist/lib/devices/registry.js +94 -38
  82. package/dist/lib/devices/stats-cache.d.ts +6 -0
  83. package/dist/lib/devices/stats-cache.js +10 -2
  84. package/dist/lib/exec.d.ts +5 -3
  85. package/dist/lib/exec.js +32 -16
  86. package/dist/lib/feed/answer.d.ts +29 -0
  87. package/dist/lib/feed/answer.js +145 -0
  88. package/dist/lib/feed/attention.d.ts +83 -0
  89. package/dist/lib/feed/attention.js +0 -0
  90. package/dist/lib/feed/feed.d.ts +109 -0
  91. package/dist/lib/feed/feed.js +154 -3
  92. package/dist/lib/feed/pr-status.d.ts +14 -0
  93. package/dist/lib/feed/pr-status.js +47 -0
  94. package/dist/lib/feed/watch.d.ts +59 -0
  95. package/dist/lib/feed/watch.js +180 -0
  96. package/dist/lib/fleet/capture.js +6 -0
  97. package/dist/lib/fleet/remote-login.js +8 -3
  98. package/dist/lib/fleet/types.d.ts +17 -0
  99. package/dist/lib/harness/adapter.d.ts +11 -0
  100. package/dist/lib/harness/adapters/claude.js +38 -24
  101. package/dist/lib/harness/adapters/codex.js +2 -4
  102. package/dist/lib/hooks/cache.js +8 -0
  103. package/dist/lib/hooks/match.d.ts +3 -0
  104. package/dist/lib/hooks/match.js +25 -0
  105. package/dist/lib/hosts/passthrough.js +16 -0
  106. package/dist/lib/hosts/registry.js +7 -0
  107. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  108. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  109. package/dist/lib/menubar/install-menubar.d.ts +13 -0
  110. package/dist/lib/menubar/install-menubar.js +26 -2
  111. package/dist/lib/open-url.d.ts +60 -2
  112. package/dist/lib/open-url.js +167 -12
  113. package/dist/lib/refresh-coordinator.d.ts +1 -1
  114. package/dist/lib/refresh-coordinator.js +1 -1
  115. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  116. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
  117. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  118. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  119. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
  120. package/dist/lib/session/active.d.ts +67 -0
  121. package/dist/lib/session/active.js +69 -1
  122. package/dist/lib/session/db.js +4 -2
  123. package/dist/lib/session/discover.d.ts +2 -0
  124. package/dist/lib/session/discover.js +12 -6
  125. package/dist/lib/session/prompt.d.ts +35 -0
  126. package/dist/lib/session/prompt.js +56 -0
  127. package/dist/lib/session/remote/watch.d.ts +1 -1
  128. package/dist/lib/session/remote/watch.js +6 -1
  129. package/dist/lib/session/render.js +7 -3
  130. package/dist/lib/session/session-cache.js +5 -1
  131. package/dist/lib/session/trajectory-compare.d.ts +47 -0
  132. package/dist/lib/session/trajectory-compare.js +116 -0
  133. package/dist/lib/session/trajectory-html.d.ts +15 -0
  134. package/dist/lib/session/trajectory-html.js +750 -0
  135. package/dist/lib/session/trajectory-lineage.d.ts +92 -0
  136. package/dist/lib/session/trajectory-lineage.js +195 -0
  137. package/dist/lib/session/trajectory-text.d.ts +37 -0
  138. package/dist/lib/session/trajectory-text.js +277 -0
  139. package/dist/lib/session/trajectory.d.ts +102 -0
  140. package/dist/lib/session/trajectory.js +335 -0
  141. package/dist/lib/startup/command-registry.d.ts +3 -1
  142. package/dist/lib/startup/command-registry.js +5 -2
  143. package/dist/lib/state.d.ts +1 -1
  144. package/dist/lib/state.js +4 -2
  145. package/dist/lib/teams/agents.js +9 -0
  146. package/dist/lib/terminal/inject.d.ts +19 -0
  147. package/dist/lib/terminal/inject.js +19 -9
  148. package/dist/lib/tmux/session.d.ts +46 -13
  149. package/dist/lib/tmux/session.js +173 -27
  150. package/dist/lib/types.d.ts +16 -0
  151. package/dist/lib/usage-backoff.d.ts +2 -1
  152. package/dist/lib/usage-backoff.js +60 -19
  153. package/dist/lib/usage-refresh.d.ts +17 -3
  154. package/dist/lib/usage-refresh.js +57 -11
  155. package/dist/lib/watchdog/log.d.ts +1 -1
  156. package/dist/lib/watchdog/log.js +1 -1
  157. package/dist/lib/watchdog/runner.d.ts +36 -45
  158. package/dist/lib/watchdog/runner.js +150 -119
  159. package/dist/lib/watchdog/service.d.ts +0 -1
  160. package/dist/lib/watchdog/service.js +0 -1
  161. package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
  162. package/dist/lib/watchdog/watchdog-agent.js +59 -0
  163. package/dist/lib/watchdog/watchdog.d.ts +11 -2
  164. package/dist/lib/watchdog/watchdog.js +37 -104
  165. package/package.json +2 -2
  166. package/dist/commands/usage.d.ts +0 -27
  167. package/dist/commands/usage.js +0 -96
@@ -4,6 +4,7 @@ import * as path from 'path';
4
4
  import { BrowserCdpConnectionError, CDPClient, discoverBrowserWsUrl, verifyBrowserIdentity, } from './cdp.js';
5
5
  import { getProfile, getProfileRuntimeDir, getProfileDownloadsDir, getProfileSessionsDir, getBrowserRuntimeDir, listProfiles, extractConfiguredPort, resolveEndpoint, parseEndpointUrl, } from './profiles.js';
6
6
  import { killChrome, getRunningChromeInfo, launchBrowser, allocatePort } from './chrome.js';
7
+ import { assertRemoteControlAllowedForRequest } from './remote-control.js';
7
8
  import { connectLocal } from './drivers/local.js';
8
9
  import { connectSSH, shellQuote } from './drivers/ssh.js';
9
10
  import { clearProfileRuntime, listProfileCacheDirs, readProfileRuntimeMeta, isProcessAlive } from './runtime-state.js';
@@ -312,6 +313,11 @@ export class BrowserService {
312
313
  /** Coalescing window for the `touchTask` write. See {@link touchTask}. */
313
314
  static TOUCH_PERSIST_INTERVAL_MS = 60_000;
314
315
  async start(profileName, opts = {}) {
316
+ // Consent gate, before anything is resolved or launched. This is the
317
+ // authoritative one: gating only the `browser start` COMMAND left the ~18
318
+ // page verbs that create a browser implicitly (navigate, click, screenshot,
319
+ // …) able to open a browser on a machine whose owner never opted in.
320
+ assertRemoteControlAllowedForRequest(opts.fleetRemote, { actor: opts.actor });
315
321
  const profile = await getProfile(profileName);
316
322
  if (!profile) {
317
323
  throw new Error(`Profile "${profileName}" not found`);
@@ -345,22 +351,8 @@ export class BrowserService {
345
351
  taskName = taskId;
346
352
  }
347
353
  const taskLabel = deriveTaskLabel({ title: opts.title, url: opts.url });
348
- let conn = this.connections.get(composite);
354
+ let conn = await this.reuseHealthyConnection(composite);
349
355
  let effectiveKey = composite;
350
- // If we have a cached connection, confirm it's still usable before any
351
- // caller relies on it. A browser killed externally (Cmd-Q, crash, or a
352
- // user clearing the profile by hand) leaves a closed WebSocket here.
353
- // Without this check the next `cdp.send` throws "CDP connection not
354
- // open" and the user has no way to recover short of killing the daemon.
355
- if (conn && !(await isConnHealthy(conn))) {
356
- try {
357
- conn.cdp.close();
358
- }
359
- catch { /* already closed */ }
360
- conn.cleanup?.();
361
- this.connections.delete(composite);
362
- conn = undefined;
363
- }
364
356
  if (conn && conn.electron && conn.tasks.size > 0) {
365
357
  if (this.forkingProfiles.has(composite)) {
366
358
  while (this.forkingProfiles.has(composite)) {
@@ -388,11 +380,7 @@ export class BrowserService {
388
380
  }
389
381
  }
390
382
  else if (!conn) {
391
- conn = await this.connectProfile(effectiveProfile, resolved.target, composite);
392
- conn.key = composite;
393
- conn.profile = profileName;
394
- this.connections.set(composite, conn);
395
- await this.applyDefaultDownloadBehavior(conn, composite);
383
+ conn = await this.openConnection(effectiveProfile, resolved.target, composite, profileName);
396
384
  }
397
385
  if (conn && !conn.profile)
398
386
  conn.profile = profileName;
@@ -2310,6 +2298,10 @@ export class BrowserService {
2310
2298
  // Zero matches for this caller.
2311
2299
  if (!opts.createIfMissing)
2312
2300
  return null;
2301
+ // Refuse BEFORE ensureDefaultBrowserProfile() below, which probes local
2302
+ // browser installs and may CREATE and persist an `auto-chrome` profile — a
2303
+ // refused request must not leave state behind on the target machine.
2304
+ assertRemoteControlAllowedForRequest(opts.fleetRemote, { actor: opts.actor });
2313
2305
  // Implicit start on the default / named profile.
2314
2306
  let profileName = opts.profile;
2315
2307
  if (!profileName) {
@@ -2318,6 +2310,7 @@ export class BrowserService {
2318
2310
  profileName = detected.name;
2319
2311
  }
2320
2312
  const started = await this.start(profileName, {
2313
+ fleetRemote: opts.fleetRemote,
2321
2314
  url: opts.url,
2322
2315
  actor: opts.actor,
2323
2316
  launchId: opts.launchId,
@@ -2835,6 +2828,71 @@ export class BrowserService {
2835
2828
  history = history.slice(0, 50);
2836
2829
  await fs.promises.writeFile(historyFile, JSON.stringify(history, null, 2));
2837
2830
  }
2831
+ /**
2832
+ * The cached connection for `key`, or undefined — evicting it first if the
2833
+ * browser died underneath us.
2834
+ *
2835
+ * A browser killed externally (Cmd-Q, crash, a user clearing the profile by
2836
+ * hand) leaves a closed WebSocket in the map. Without this the next
2837
+ * `cdp.send` throws "CDP connection not open" and there is no recovery short
2838
+ * of killing the daemon.
2839
+ */
2840
+ async reuseHealthyConnection(key) {
2841
+ const conn = this.connections.get(key);
2842
+ if (!conn)
2843
+ return undefined;
2844
+ if (await isConnHealthy(conn))
2845
+ return conn;
2846
+ try {
2847
+ conn.cdp.close();
2848
+ }
2849
+ catch { /* already closed */ }
2850
+ conn.cleanup?.();
2851
+ this.connections.delete(key);
2852
+ return undefined;
2853
+ }
2854
+ /** Connect a profile at `target`, register it under `key`, and arm downloads. */
2855
+ async openConnection(profile, target, key, profileName) {
2856
+ const conn = await this.connectProfile(profile, target, key);
2857
+ conn.key = key;
2858
+ conn.profile = profileName;
2859
+ this.connections.set(key, conn);
2860
+ await this.applyDefaultDownloadBehavior(conn, key);
2861
+ return conn;
2862
+ }
2863
+ /**
2864
+ * Open a tab for a HUMAN to read, bound to no task.
2865
+ *
2866
+ * That is the entire difference from {@link start}, and it is the point. The
2867
+ * abandoned-task reaper closes a task's tabs when the calling session dies or
2868
+ * after the idle window, but it "deliberately never touches a tab that is not
2869
+ * in `task.tabs`" (hygiene.ts) — so a viewer tab must not be in one, or the
2870
+ * artifact the user is reading vanishes when the agent that rendered it exits.
2871
+ *
2872
+ * Consequently `stop`/`done` do not close it either, which is correct: it is
2873
+ * the user's tab now.
2874
+ */
2875
+ async showUrl(profileName, url, opts = {}) {
2876
+ // Same consent gate as start(): this opens a browser, so a fleet-remote
2877
+ // caller needs the target machine's opt-in.
2878
+ assertRemoteControlAllowedForRequest(opts.fleetRemote, { actor: opts.actor });
2879
+ const profile = await getProfile(profileName);
2880
+ if (!profile)
2881
+ throw new Error(`Profile "${profileName}" not found`);
2882
+ const resolved = resolveEndpoint(profile, opts.endpointName);
2883
+ const key = connectionKey(profileName, resolved.name);
2884
+ const effectiveProfile = {
2885
+ ...profile,
2886
+ binary: resolved.binary,
2887
+ targetFilter: resolved.targetFilter,
2888
+ };
2889
+ const conn = (await this.reuseHealthyConnection(key)) ??
2890
+ (await this.openConnection(effectiveProfile, resolved.target, key, profileName));
2891
+ // createPageTarget refuses Arc with an actionable error rather than crashing it.
2892
+ const created = await this.createPageTarget(conn, { url });
2893
+ this.invalidateTargetCache(conn);
2894
+ return { profile: profileName, key, tabId: created.targetId };
2895
+ }
2838
2896
  async getHistory(limit = 10) {
2839
2897
  const historyFile = path.join(getBrowserRuntimeDir(), 'history.json');
2840
2898
  if (!fs.existsSync(historyFile))
@@ -32,8 +32,6 @@ export declare function renderBrowserSessions(groups: ProfileArtifacts[]): strin
32
32
  * substring match. Returns the absolute path, or null if nothing matched.
33
33
  */
34
34
  export declare function resolveArtifact(groups: ProfileArtifacts[], selector: string): string | null;
35
- /** Open a file in the OS default app. Returns true on success. */
36
- export declare function openArtifact(filePath: string): boolean;
37
35
  /** The identity fields this module cares about for one task. */
38
36
  export interface TaskIdentity {
39
37
  owner?: string;
@@ -139,4 +137,4 @@ export declare function runBrowserSessions(opts: {
139
137
  profile?: string;
140
138
  open?: string | boolean;
141
139
  json?: boolean;
142
- }): void;
140
+ }): Promise<void>;
@@ -14,12 +14,12 @@
14
14
  * problem (pid registry, the SessionStart hook index, the activity log; see
15
15
  * `feed-post.ts` `resolvePostIdentity`), never a second parser.
16
16
  */
17
+ import { showFile } from '../open-url.js';
17
18
  import * as fs from 'fs';
18
19
  import { formatBytes } from '../format.js';
19
20
  // Re-exported for the picker, which lists artifacts from this module's rows.
20
21
  export { formatBytes };
21
22
  import * as path from 'path';
22
- import { spawnSync } from 'child_process';
23
23
  import { getBrowserRuntimeDir, getProfileRuntimeDir } from './profiles.js';
24
24
  import { formatRelativeTime } from '../session/relative-time.js';
25
25
  import { getSessionById, listBrowserSessionRecords, pruneToolSessions } from '../session/db.js';
@@ -158,19 +158,6 @@ export function resolveArtifact(groups, selector) {
158
158
  const hit = all.find((a) => a.name === selector) ?? all.find((a) => a.name.includes(selector));
159
159
  return hit ? hit.path : null;
160
160
  }
161
- /** Open a file in the OS default app. Returns true on success. */
162
- export function openArtifact(filePath) {
163
- const openers = process.platform === 'darwin'
164
- ? [['open', [filePath]]]
165
- : process.platform === 'win32'
166
- ? [['cmd', ['/c', 'start', '""', filePath]]]
167
- : [['xdg-open', [filePath]], ['gnome-open', [filePath]]];
168
- for (const [cmd, args] of openers) {
169
- if (spawnSync(cmd, args, { stdio: 'ignore' }).status === 0)
170
- return true;
171
- }
172
- return false;
173
- }
174
161
  /**
175
162
  * Read a profile's `tasks.json` for the identity of its CURRENTLY LIVE tasks,
176
163
  * keyed by task name. A task's entry is deleted on `agents browser stop` (see
@@ -380,7 +367,7 @@ export function matchesBrowserSessionRow(row, query) {
380
367
  * `open` is the Commander value for `--open [selector]`: undefined when the flag
381
368
  * is absent, `true` when passed bare (defaults to 'latest'), or the selector string.
382
369
  */
383
- export function runBrowserSessions(opts) {
370
+ export async function runBrowserSessions(opts) {
384
371
  const groups = listBrowserSessions(opts.profile);
385
372
  if (opts.open !== undefined && opts.open !== false) {
386
373
  const selector = opts.open === true ? 'latest' : opts.open;
@@ -390,7 +377,7 @@ export function runBrowserSessions(opts) {
390
377
  process.exit(1);
391
378
  }
392
379
  console.log(target);
393
- if (!openArtifact(target)) {
380
+ if ((await showFile(target)).via === 'none') {
394
381
  console.error(`Could not open ${target}`);
395
382
  process.exit(1);
396
383
  }
@@ -287,7 +287,7 @@ export interface ReapResult {
287
287
  /** Tasks left alone this pass: still live, still inside the idle window, or recording. */
288
288
  skipped: number;
289
289
  }
290
- export type IPCAction = 'start' | 'gc' | 'record-start' | 'record-stop' | 'done' | 'stop' | 'status' | 'history' | 'navigate' | 'tab-add' | 'tab-focus' | 'tab-close' | 'tab-list' | 'evaluate' | 'screenshot' | 'pdf' | 'refs' | 'click' | 'type' | 'press' | 'hover' | 'scroll' | 'set-viewport' | 'set-device' | 'console' | 'errors' | 'requests' | 'response-body' | 'wait' | 'set-download-path' | 'wait-download' | 'upload' | 'getAppLogs' | 'version';
290
+ export type IPCAction = 'start' | 'show' | 'gc' | 'record-start' | 'record-stop' | 'done' | 'stop' | 'status' | 'history' | 'navigate' | 'tab-add' | 'tab-focus' | 'tab-close' | 'tab-list' | 'evaluate' | 'screenshot' | 'pdf' | 'refs' | 'click' | 'type' | 'press' | 'hover' | 'scroll' | 'set-viewport' | 'set-device' | 'console' | 'errors' | 'requests' | 'response-body' | 'wait' | 'set-download-path' | 'wait-download' | 'upload' | 'getAppLogs' | 'version';
291
291
  export interface IPCRequest {
292
292
  action: IPCAction;
293
293
  task?: string;
@@ -366,6 +366,17 @@ export interface IPCRequest {
366
366
  * for `computer.action` events, so both tool surfaces key on one signal.
367
367
  */
368
368
  sessionId?: string;
369
+ /**
370
+ * True when the CLI process that issued this request was itself dispatched to
371
+ * this machine by a fleet `--device` hop.
372
+ *
373
+ * Stamped client-side for the same reason actor/launchId are — and
374
+ * additionally because the daemon may have been auto-started BY a fleet-remote
375
+ * CLI and inherited AGENTS_FLEET_REMOTE for its whole life (startDetached
376
+ * passes `env: opts.env ?? process.env`). The daemon's own env is therefore
377
+ * not a truthful signal about the CURRENT caller; only the request is.
378
+ */
379
+ fleetRemote?: boolean;
369
380
  }
370
381
  /** Subset of IPCResponse describing a recording start result. */
371
382
  export interface RecordStartFields {
@@ -1,6 +1,7 @@
1
- export type CodexPolicyMode = 'plan' | 'edit' | 'skip';
1
+ export type CodexPolicyMode = 'plan' | 'edit' | 'auto' | 'skip';
2
2
  export declare const CODEX_PLAN_PROFILE = "agents-plan";
3
3
  export declare const CODEX_EDIT_PROFILE = "agents-edit";
4
+ export declare const CODEX_AUTO_PROFILE = "agents-auto";
4
5
  /**
5
6
  * Writable roots for Codex's `edit` profile: the managed user `.agents` dir, the
6
7
  * baseline toolchain caches, and — when `cwd` is inside a repo — that repo's
@@ -17,6 +18,15 @@ export declare function codexPermissionProfileConfig(mode: Exclude<CodexPolicyMo
17
18
  * Config overrides are deliberately used instead of the legacy `--sandbox`
18
19
  * flags: named permission profiles are the only Codex surface that can keep a
19
20
  * plan run filesystem-read-only while independently enabling network access.
21
+ *
22
+ * `auto` and `edit` share one sandbox (`:workspace` plus the writable roots) and
23
+ * differ only in `approval_policy`. `edit` is `on-request`: a command the sandbox
24
+ * denies comes back as an approval prompt, which is right when someone is sitting
25
+ * at the terminal. `auto` is `never`: nothing prompts, and a denied command
26
+ * surfaces to the model as a plain command failure — the only behavior that works
27
+ * for an unattended run, where a prompt nobody answers is an agent that has
28
+ * stopped. Autonomy is the approval axis only; neither widens the sandbox, and
29
+ * `skip` remains the sole mode that removes it.
20
30
  */
21
31
  export declare function codexPolicyArgs(mode: CodexPolicyMode, writableRoots?: string[]): string[];
22
32
  /** Preserve whether --mode was omitted when a run is re-dispatched remotely. */
@@ -4,6 +4,7 @@ import { codexDefaultWritableRoots } from './permissions.js';
4
4
  import { repoAgentsDirForCwd } from './project-key.js';
5
5
  export const CODEX_PLAN_PROFILE = 'agents-plan';
6
6
  export const CODEX_EDIT_PROFILE = 'agents-edit';
7
+ export const CODEX_AUTO_PROFILE = 'agents-auto';
7
8
  function unique(values) {
8
9
  return [...new Set(values)];
9
10
  }
@@ -28,25 +29,39 @@ function inlineWorkspaceRoots(roots) {
28
29
  }
29
30
  export function codexPermissionProfileConfig(mode, writableRoots = codexEditWritableRoots()) {
30
31
  const parent = mode === 'plan' ? ':read-only' : ':workspace';
31
- const roots = mode === 'edit'
32
- ? `, workspace_roots = { ${inlineWorkspaceRoots(unique(writableRoots))} }`
33
- : '';
32
+ const roots = mode === 'plan'
33
+ ? ''
34
+ : `, workspace_roots = { ${inlineWorkspaceRoots(unique(writableRoots))} }`;
34
35
  return `{ extends = ${JSON.stringify(parent)}${roots}, network = { enabled = true, allow_local_binding = true } }`;
35
36
  }
37
+ const CODEX_PROFILES = {
38
+ plan: CODEX_PLAN_PROFILE,
39
+ edit: CODEX_EDIT_PROFILE,
40
+ auto: CODEX_AUTO_PROFILE,
41
+ };
36
42
  /**
37
43
  * Canonical Codex safety policy used by every native launch path.
38
44
  *
39
45
  * Config overrides are deliberately used instead of the legacy `--sandbox`
40
46
  * flags: named permission profiles are the only Codex surface that can keep a
41
47
  * plan run filesystem-read-only while independently enabling network access.
48
+ *
49
+ * `auto` and `edit` share one sandbox (`:workspace` plus the writable roots) and
50
+ * differ only in `approval_policy`. `edit` is `on-request`: a command the sandbox
51
+ * denies comes back as an approval prompt, which is right when someone is sitting
52
+ * at the terminal. `auto` is `never`: nothing prompts, and a denied command
53
+ * surfaces to the model as a plain command failure — the only behavior that works
54
+ * for an unattended run, where a prompt nobody answers is an agent that has
55
+ * stopped. Autonomy is the approval axis only; neither widens the sandbox, and
56
+ * `skip` remains the sole mode that removes it.
42
57
  */
43
58
  export function codexPolicyArgs(mode, writableRoots = codexEditWritableRoots()) {
44
59
  if (mode === 'skip')
45
60
  return ['--dangerously-bypass-approvals-and-sandbox'];
46
- const profile = mode === 'plan' ? CODEX_PLAN_PROFILE : CODEX_EDIT_PROFILE;
61
+ const profile = CODEX_PROFILES[mode];
47
62
  return [
48
63
  '-c',
49
- 'approval_policy="on-request"',
64
+ `approval_policy=${mode === 'auto' ? '"never"' : '"on-request"'}`,
50
65
  '-c',
51
66
  `default_permissions=${JSON.stringify(profile)}`,
52
67
  '-c',
@@ -33,10 +33,11 @@ export interface ParsedAutoConfigKey {
33
33
  scope: 'auto';
34
34
  property: 'pool';
35
35
  }
36
- /** The default browser profile (device-scope, self or peer). */
36
+ /** A device-scope browser key (self or peer): the profile agents drive, or the
37
+ * browser that shows the user a page. */
37
38
  export interface ParsedBrowserConfigKey {
38
39
  scope: 'browser';
39
- property: 'profile';
40
+ property: 'profile' | 'viewer';
40
41
  device?: string;
41
42
  }
42
43
  export interface ParsedProjectConfigKey {
@@ -50,7 +51,7 @@ export interface ParsedDeviceConfigKey {
50
51
  property: DeviceConfigProperty;
51
52
  }
52
53
  export type ParsedConfigKey = ParsedRunConfigKey | ParsedInteractiveConfigKey | ParsedUsageConfigKey | ParsedAutoConfigKey | ParsedBrowserConfigKey | ParsedProjectConfigKey | ParsedDeviceConfigKey;
53
- export type DeviceConfigProperty = 'role' | 'max-agents' | 'scheduler' | 'daemon' | 'watchdog' | 'tmux' | 'browser.remote-control' | 'browser.task-idle-minutes' | 'notes' | 'browser.profile';
54
+ export type DeviceConfigProperty = 'role' | 'max-agents' | 'scheduler' | 'daemon' | 'watchdog' | 'tmux' | 'browser.remote-control' | 'browser.task-idle-minutes' | 'notes' | 'browser.profile' | 'browser.viewer';
54
55
  /** Normalize agent@version to use `@` consistently. */
55
56
  export declare function formatAgentVersion(agent: AgentId, version: string): string;
56
57
  /**
@@ -10,6 +10,7 @@ import { AGENTS } from './agents.js';
10
10
  import { MODEL_TIERS } from './model-tiers.js';
11
11
  import { VERSION_RE } from './run-defaults.js';
12
12
  const DEVICE_CONFIG_PROPERTIES = [
13
+ 'browser.viewer',
13
14
  'role',
14
15
  'max-agents',
15
16
  'scheduler',
@@ -88,10 +89,13 @@ export function parseConfigKey(key) {
88
89
  if (raw === 'browser.profile') {
89
90
  return { scope: 'browser', property: 'profile' };
90
91
  }
92
+ if (raw === 'browser.viewer') {
93
+ return { scope: 'browser', property: 'viewer' };
94
+ }
91
95
  if (raw === 'project.root') {
92
96
  return { scope: 'project', property: 'root' };
93
97
  }
94
- const deviceMatch = raw.match(/^devices\.(.+)\.(role|max-agents|scheduler|daemon|watchdog|tmux|notes|browser\.remote-control|browser\.task-idle-minutes|browser\.profile)$/);
98
+ const deviceMatch = raw.match(/^devices\.(.+)\.(role|max-agents|scheduler|daemon|watchdog|tmux|notes|browser\.remote-control|browser\.task-idle-minutes|browser\.profile|browser\.viewer)$/);
95
99
  if (deviceMatch) {
96
100
  return {
97
101
  scope: 'device',
@@ -113,7 +117,7 @@ export function parseConfigKey(key) {
113
117
  throw new Error(`Invalid auto config key '${key}'. Use auto.pool.`);
114
118
  }
115
119
  if (raw.startsWith('browser.')) {
116
- throw new Error(`Invalid browser config key '${key}'. Use browser.profile.`);
120
+ throw new Error(`Invalid browser config key '${key}'. Use browser.profile or browser.viewer.`);
117
121
  }
118
122
  if (raw.startsWith('project.')) {
119
123
  throw new Error(`Invalid project config key '${key}'. Use project.root.`);
@@ -138,7 +142,9 @@ export function formatConfigKey(parsed) {
138
142
  case 'auto':
139
143
  return 'auto.pool';
140
144
  case 'browser':
141
- return parsed.device ? `devices.${parsed.device}.browser.profile` : 'browser.profile';
145
+ return parsed.device
146
+ ? `devices.${parsed.device}.browser.${parsed.property}`
147
+ : `browser.${parsed.property}`;
142
148
  case 'project':
143
149
  return 'project.root';
144
150
  case 'device':
@@ -152,7 +158,7 @@ export function listKnownConfigKeys() {
152
158
  for (const tier of MODEL_TIERS) {
153
159
  keys.push(`run.<agent@version>.tier.${tier}`);
154
160
  }
155
- keys.push('interactive.host', 'usage.primary-host', 'auto.pool', 'browser.profile', 'project.root');
161
+ keys.push('interactive.host', 'usage.primary-host', 'auto.pool', 'browser.profile', 'browser.viewer', 'project.root');
156
162
  for (const prop of DEVICE_CONFIG_PROPERTIES) {
157
163
  keys.push(`devices.<name>.${prop}`);
158
164
  }
@@ -185,6 +191,8 @@ export function devicePropertyToConfigName(property) {
185
191
  return 'notes';
186
192
  case 'browser.profile':
187
193
  return 'browser.profile';
194
+ case 'browser.viewer':
195
+ return 'browser.viewer';
188
196
  }
189
197
  }
190
198
  /**
@@ -204,10 +212,12 @@ export function configKeyStorageHint(parsed) {
204
212
  return 'config.usagePrimaryHost';
205
213
  case 'auto':
206
214
  return 'config.autoPool';
207
- case 'browser':
215
+ case 'browser': {
216
+ const yamlKey = parsed.property === 'viewer' ? 'browserViewer' : 'defaultBrowserProfile';
208
217
  return parsed.device
209
- ? `devices/${parsed.device}/agents.yaml config.defaultBrowserProfile`
210
- : 'devices/<self>/agents.yaml config.defaultBrowserProfile';
218
+ ? `devices/${parsed.device}/agents.yaml config.${yamlKey}`
219
+ : `devices/<self>/agents.yaml config.${yamlKey}`;
220
+ }
211
221
  case 'project':
212
222
  return 'devices.<self>.projectRoot';
213
223
  case 'device':
@@ -39,6 +39,7 @@ import { resolveClaudeSetupToken } from '../claude-account-token.js';
39
39
  import { getConfiguredRunStrategy, resolveRunVersion, resolveAccountVersion, rotationFailoverChain, readinessFromCandidate, formatNoHealthyAccountError, } from '../accounting/rotate.js';
40
40
  import { readAuthHealth, isDeadVerdict } from '../auth-health.js';
41
41
  import { machineId } from '../machine-id.js';
42
+ import { selfConfiguredDeviceRole } from '../device-config.js';
42
43
  import { isSelfUpdatingAgent, ROUTINE_AGENT_IDS, isAgentHardDeprecated, hardDeprecationError } from '../agents.js';
43
44
  import { isCustomHarnessName, readProfile } from '../profiles.js';
44
45
  export class RoutineAlreadyRunningError extends Error {
@@ -1120,10 +1121,22 @@ export function buildRoutineSpawnEnv(baseEnv, agent, version, timezone, overlayH
1120
1121
  const setupToken = agent === 'claude' && version
1121
1122
  ? resolveClaudeSetupToken(getVersionHomePath('claude', version))
1122
1123
  : null;
1123
- if (setupToken)
1124
+ // A `personal` device (the user's own interactive box) uses the per-version
1125
+ // login for EVERY run, routines included — the setup-token is a worker-only
1126
+ // credential (RUSH-2395, mirrors the claude adapter's personal-device branch).
1127
+ // On a personal box, only strip an inherited copy of the account's OWN
1128
+ // setup-token by value so a leaked ambient value can't override the login; a
1129
+ // token the user set deliberately is a different string and survives.
1130
+ if (selfConfiguredDeviceRole() === 'personal') {
1131
+ if (setupToken && out.CLAUDE_CODE_OAUTH_TOKEN === setupToken)
1132
+ delete out.CLAUDE_CODE_OAUTH_TOKEN;
1133
+ }
1134
+ else if (setupToken) {
1124
1135
  out.CLAUDE_CODE_OAUTH_TOKEN = setupToken;
1125
- else
1136
+ }
1137
+ else {
1126
1138
  delete out.CLAUDE_CODE_OAUTH_TOKEN;
1139
+ }
1127
1140
  if (agent === 'cursor' && overlayHome) {
1128
1141
  // prepareJobHome links this host's Cursor auth file here. Pin XDG_CONFIG_HOME
1129
1142
  // to the overlay so an ambient value cannot bypass the routine sandbox.
@@ -89,7 +89,7 @@ export async function refreshLocalFleetAuthState(opts) {
89
89
  // Fleet status publishes every tick regardless — it does not ride that endpoint.
90
90
  const cached = readFleetAuthRows(self);
91
91
  const reuse = shouldReuseCachedAuthProbe(force, cached, requestedAt);
92
- const authRows = reuse ? cached : await probeLocalFleetAuth({ cliVersion: getCliVersion() });
92
+ const authRows = reuse ? cached : await probeLocalFleetAuth({ cliVersion: getCliVersion(), forceLive: force });
93
93
  if (!reuse)
94
94
  writeFleetAuthRows(self, authRows);
95
95
  const row = await publishLocalFleetStatus(self);
@@ -129,7 +129,7 @@ export async function runUsageRefreshTick() {
129
129
  const r = await runUsageRefresh({
130
130
  listAccounts: buildLocalUsageAccounts,
131
131
  writeUsageCache: writeClaudeUsageCache,
132
- backoffUntil: usageRateLimitedUntil,
132
+ backoffUntil: (agentId, usageKey) => usageRateLimitedUntil(agentId, Date.now(), usageKey),
133
133
  });
134
134
  const { listProfiles } = await import('./profiles.js');
135
135
  const { refreshDueByokUsage } = await import('./byok-usage.js');
@@ -147,8 +147,32 @@ export type AutoPoolMode = (typeof AUTO_POOL_MODES)[number];
147
147
  * explicit `worker` mark (see {@link listConfiguredDeviceRoles}).
148
148
  */
149
149
  export declare function configuredDeviceRole(name: string): ConfiguredDeviceRole | undefined;
150
+ /**
151
+ * The role marked on THIS machine — the one running the CLI — or undefined when
152
+ * it was never marked. Keyed off {@link machineId} (overridable via
153
+ * AGENTS_SYNC_MACHINE_ID), so it matches the device's own config-folder key.
154
+ *
155
+ * The auth strategy reads this: a `personal` device (the user's own interactive
156
+ * box) holds a real per-version login and MUST authenticate from it for EVERY
157
+ * run, interactive or headless; only a `worker` uses the file-based setup-token
158
+ * (RUSH-2395). `undefined` is treated as non-personal (worker-equivalent) by
159
+ * that gate — an unmarked box has no login to defer to.
160
+ */
161
+ export declare function selfConfiguredDeviceRole(): ConfiguredDeviceRole | undefined;
150
162
  /** Mark a device's role fleet-wide; `undefined` clears the mark. */
151
163
  export declare function setConfiguredDeviceRole(name: string, role: ConfiguredDeviceRole | undefined): void;
164
+ /**
165
+ * Devices whose OWN config pins one of the browser profile keys to `profile`.
166
+ *
167
+ * Used by `profiles rename` to warn rather than silently leave a peer pointing at
168
+ * a name that no longer exists. Deliberately read-only: rewriting another
169
+ * machine's device doc from here would be a cross-machine mutation nobody asked
170
+ * for, and the pin may well be correct there (a local profile of the same name).
171
+ */
172
+ export declare function devicesPinningBrowserProfile(profile: string): Array<{
173
+ device: string;
174
+ key: 'browser.profile' | 'browser.viewer';
175
+ }>;
152
176
  /**
153
177
  * Every device with an effective role, keyed by device name. Layers like every
154
178
  * other device-scope key: the fleet default (central fleet.defaults.config)
@@ -194,14 +218,7 @@ export declare function isSchedulerEnabled(): boolean;
194
218
  * scheduler init) refuses with.
195
219
  */
196
220
  export declare function assertSchedulerEnabled(): void;
197
- /**
198
- * True unless this machine's config turns off the managed tmux wrap for
199
- * interactive `agents run` launches (`tmux.enabled=false`).
200
- *
201
- * Read as one of the guards in `shouldWrapInTmux` (lib/exec.ts) — the durable,
202
- * per-machine form of `--no-tmux` / `AGENTS_NO_TMUX=1`, for a box whose tmux is
203
- * broken or unwanted. Unset means today's behavior: wrap.
204
- */
221
+ /** True only when this machine explicitly enables the managed tmux wrap. */
205
222
  export declare function isTmuxEnabled(): boolean;
206
223
  /** True unless this machine's config disables the daemon outright (top-level kill switch). */
207
224
  export declare function isDaemonEnabled(): boolean;