@phnx-labs/agents-cli 1.22.52 → 1.22.54

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 (180) hide show
  1. package/CHANGELOG.md +336 -0
  2. package/README.md +42 -9
  3. package/dist/bootstrap.js +55 -154
  4. package/dist/cli/command-registry.d.ts +5 -0
  5. package/dist/cli/command-registry.js +8 -1
  6. package/dist/commands/accounts.js +220 -174
  7. package/dist/commands/apply.js +6 -3
  8. package/dist/commands/auth-mint.d.ts +8 -0
  9. package/dist/commands/auth-mint.js +96 -0
  10. package/dist/commands/auth.js +5 -1
  11. package/dist/commands/browser.js +1 -1
  12. package/dist/commands/cost.js +8 -2
  13. package/dist/commands/daemon.js +2 -2
  14. package/dist/commands/doctor.js +6 -1
  15. package/dist/commands/exec.js +26 -17
  16. package/dist/commands/fleet-capture.js +7 -0
  17. package/dist/commands/focus.d.ts +1 -0
  18. package/dist/commands/focus.js +4 -2
  19. package/dist/commands/go.d.ts +5 -4
  20. package/dist/commands/go.js +8 -7
  21. package/dist/commands/insights.js +9 -0
  22. package/dist/commands/monitors.js +85 -30
  23. package/dist/commands/output.js +8 -2
  24. package/dist/commands/repo.js +18 -0
  25. package/dist/commands/secrets.js +33 -14
  26. package/dist/commands/sessions-inject.js +8 -3
  27. package/dist/commands/sessions-picker.js +2 -1
  28. package/dist/commands/sessions.d.ts +20 -12
  29. package/dist/commands/sessions.js +94 -40
  30. package/dist/commands/setup-accounts.d.ts +8 -0
  31. package/dist/commands/setup-accounts.js +47 -0
  32. package/dist/commands/setup.d.ts +1 -1
  33. package/dist/commands/setup.js +11 -2
  34. package/dist/commands/share.d.ts +52 -3
  35. package/dist/commands/share.js +262 -18
  36. package/dist/commands/ssh.d.ts +7 -0
  37. package/dist/commands/ssh.js +53 -14
  38. package/dist/commands/status.js +14 -0
  39. package/dist/commands/sync.js +44 -0
  40. package/dist/commands/view.d.ts +3 -1
  41. package/dist/commands/view.js +5 -4
  42. package/dist/lib/account-registry.d.ts +15 -5
  43. package/dist/lib/account-registry.js +165 -53
  44. package/dist/lib/accounting/rotate.d.ts +20 -6
  45. package/dist/lib/accounting/rotate.js +38 -7
  46. package/dist/lib/accounting/usage.d.ts +37 -1
  47. package/dist/lib/accounting/usage.js +71 -6
  48. package/dist/lib/agent-spec/agents.d.ts +5 -2
  49. package/dist/lib/agent-spec/agents.js +25 -7
  50. package/dist/lib/analytics/mix-commands.js +12 -6
  51. package/dist/lib/answer-router.js +2 -1
  52. package/dist/lib/auth-mint.d.ts +150 -0
  53. package/dist/lib/auth-mint.js +434 -0
  54. package/dist/lib/browser/profiles.d.ts +18 -0
  55. package/dist/lib/browser/profiles.js +26 -1
  56. package/dist/lib/browser/registry.d.ts +44 -14
  57. package/dist/lib/browser/registry.js +141 -45
  58. package/dist/lib/browser/remote-control.d.ts +9 -7
  59. package/dist/lib/browser/remote-control.js +9 -7
  60. package/dist/lib/claude-account-token.d.ts +10 -0
  61. package/dist/lib/claude-account-token.js +14 -4
  62. package/dist/lib/config-drift.d.ts +37 -0
  63. package/dist/lib/config-drift.js +72 -0
  64. package/dist/lib/daemon/auth-sync-service.d.ts +19 -0
  65. package/dist/lib/daemon/auth-sync-service.js +34 -0
  66. package/dist/lib/daemon/daemon.js +30 -4
  67. package/dist/lib/daemon/runner.js +10 -2
  68. package/dist/lib/daemon-services.d.ts +1 -1
  69. package/dist/lib/daemon-services.js +5 -0
  70. package/dist/lib/device-config.d.ts +3 -3
  71. package/dist/lib/device-config.js +8 -7
  72. package/dist/lib/devices/config-migration.js +147 -1
  73. package/dist/lib/devices/connect.d.ts +26 -0
  74. package/dist/lib/devices/connect.js +48 -1
  75. package/dist/lib/devices/device-docs.d.ts +35 -0
  76. package/dist/lib/devices/device-docs.js +163 -0
  77. package/dist/lib/devices/discovery-policy.d.ts +14 -2
  78. package/dist/lib/devices/discovery-policy.js +31 -21
  79. package/dist/lib/devices/doctor-findings.d.ts +5 -1
  80. package/dist/lib/devices/doctor-findings.js +19 -1
  81. package/dist/lib/devices/registry.d.ts +11 -5
  82. package/dist/lib/devices/registry.js +46 -18
  83. package/dist/lib/exec.d.ts +88 -30
  84. package/dist/lib/exec.js +138 -34
  85. package/dist/lib/feed/feed.d.ts +10 -2
  86. package/dist/lib/feed/feed.js +35 -2
  87. package/dist/lib/feed-broadcast.js +1 -1
  88. package/dist/lib/fleet/apply.d.ts +11 -0
  89. package/dist/lib/fleet/apply.js +23 -3
  90. package/dist/lib/fleet/auth-sync.js +5 -3
  91. package/dist/lib/help.d.ts +9 -0
  92. package/dist/lib/help.js +29 -1
  93. package/dist/lib/hosts/dispatch.d.ts +4 -3
  94. package/dist/lib/hosts/dispatch.js +12 -8
  95. package/dist/lib/hosts/passthrough.d.ts +1 -10
  96. package/dist/lib/hosts/passthrough.js +1 -13
  97. package/dist/lib/hosts/providers/local.d.ts +9 -3
  98. package/dist/lib/hosts/providers/local.js +23 -12
  99. package/dist/lib/hosts/reconnect.d.ts +7 -4
  100. package/dist/lib/hosts/reconnect.js +29 -25
  101. package/dist/lib/hosts/registry.js +4 -1
  102. package/dist/lib/hosts/remote-os.js +3 -1
  103. package/dist/lib/installations/versions.js +9 -1
  104. package/dist/lib/linux-userns.d.ts +58 -0
  105. package/dist/lib/linux-userns.js +116 -0
  106. package/dist/lib/memory.d.ts +26 -0
  107. package/dist/lib/memory.js +80 -1
  108. package/dist/lib/monitors/config.d.ts +11 -0
  109. package/dist/lib/monitors/config.js +8 -0
  110. package/dist/lib/monitors/engine.js +8 -1
  111. package/dist/lib/monitors/state.d.ts +37 -1
  112. package/dist/lib/monitors/state.js +79 -4
  113. package/dist/lib/permissions-registry.d.ts +2 -0
  114. package/dist/lib/permissions-registry.js +116 -14
  115. package/dist/lib/permissions.d.ts +5 -3
  116. package/dist/lib/permissions.js +25 -27
  117. package/dist/lib/profiles.d.ts +8 -7
  118. package/dist/lib/profiles.js +12 -0
  119. package/dist/lib/project-key.d.ts +9 -0
  120. package/dist/lib/project-key.js +11 -0
  121. package/dist/lib/secrets/bundles.d.ts +35 -0
  122. package/dist/lib/secrets/bundles.js +78 -1
  123. package/dist/lib/secrets/push.d.ts +3 -8
  124. package/dist/lib/secrets/push.js +18 -14
  125. package/dist/lib/secrets/remote.d.ts +9 -18
  126. package/dist/lib/secrets/remote.js +11 -26
  127. package/dist/lib/secrets/reserved-sync.d.ts +65 -0
  128. package/dist/lib/secrets/reserved-sync.js +129 -0
  129. package/dist/lib/self-heal/checks/hook-manifest.d.ts +2 -0
  130. package/dist/lib/self-heal/checks/hook-manifest.js +56 -0
  131. package/dist/lib/self-heal/registry.js +4 -0
  132. package/dist/lib/self-heal/types.d.ts +1 -1
  133. package/dist/lib/session/active.d.ts +10 -1
  134. package/dist/lib/session/active.js +8 -5
  135. package/dist/lib/session/actor-sidecar.d.ts +7 -0
  136. package/dist/lib/session/actor-sidecar.js +2 -0
  137. package/dist/lib/session/db.d.ts +39 -4
  138. package/dist/lib/session/db.js +168 -31
  139. package/dist/lib/session/discover.d.ts +32 -4
  140. package/dist/lib/session/discover.js +126 -37
  141. package/dist/lib/session/insights.d.ts +14 -0
  142. package/dist/lib/session/insights.js +25 -2
  143. package/dist/lib/session/linear.js +1 -1
  144. package/dist/lib/session/live-metadata.js +1 -0
  145. package/dist/lib/session/pid-registry.d.ts +7 -0
  146. package/dist/lib/session/prompt.d.ts +15 -0
  147. package/dist/lib/session/prompt.js +21 -0
  148. package/dist/lib/session/shell-programs.d.ts +17 -0
  149. package/dist/lib/session/shell-programs.js +21 -0
  150. package/dist/lib/session/state.js +2 -1
  151. package/dist/lib/session/stream-render.js +2 -1
  152. package/dist/lib/session/tool-calls.js +2 -5
  153. package/dist/lib/session/trajectory-html.js +2 -1
  154. package/dist/lib/session/trajectory.js +3 -12
  155. package/dist/lib/session/types.d.ts +25 -0
  156. package/dist/lib/session/types.js +10 -0
  157. package/dist/lib/share/publish.d.ts +53 -5
  158. package/dist/lib/share/publish.js +99 -17
  159. package/dist/lib/share/worker-template.js +594 -64
  160. package/dist/lib/startup/root-command.js +2 -1
  161. package/dist/lib/state.d.ts +24 -0
  162. package/dist/lib/state.js +318 -54
  163. package/dist/lib/sync-status.d.ts +4 -0
  164. package/dist/lib/sync-status.js +3 -0
  165. package/dist/lib/terminal/resolve.d.ts +7 -0
  166. package/dist/lib/terminal/resolve.js +41 -2
  167. package/dist/lib/traces/classify.js +24 -19
  168. package/dist/lib/traces/insights.d.ts +67 -0
  169. package/dist/lib/traces/insights.js +178 -0
  170. package/dist/lib/traces/phenotype.d.ts +67 -0
  171. package/dist/lib/traces/phenotype.js +437 -0
  172. package/dist/lib/traces/segments.d.ts +133 -0
  173. package/dist/lib/traces/segments.js +301 -0
  174. package/dist/lib/traces/sync.d.ts +33 -0
  175. package/dist/lib/traces/sync.js +11 -2
  176. package/dist/lib/types.d.ts +47 -1
  177. package/dist/lib/usage-refresh.js +2 -1
  178. package/dist/lib/view-types.d.ts +2 -0
  179. package/dist/lib/watchdog/runner.js +18 -4
  180. package/package.json +2 -1
@@ -13,7 +13,7 @@ export type FindingSeverity = 'critical' | 'warning';
13
13
  * the JSON consumer group by kind. */
14
14
  /** Every finding class. Severity is NOT annotated here — {@link FINDING_SEVERITY}
15
15
  * below owns it, and a second copy in these comments is a fourth place to drift. */
16
- export declare const ALL_FINDING_KINDS: readonly ["logged-out", "logout-unprovable", "missing-hook", "missing-plugin", "unwired-hook", "hook-runtime-broken", "hook-runtime-visibility-unavailable", "cli-missing", "missing-resource", "content-drift", "never-synced", "stale", "repo-behind", "repo-drift", "fleet-resource-gap", "host-cli-missing", "host-cli-invalid", "version-skew", "orphan", "duplicate-hook", "duplicate-hook-drift", "rc-secret-export", "env-secret-export", "exec-policy", "ssh-key-enrollment", "stale-cli", "binary-shadow", "owner-sink-unreachable"];
16
+ export declare const ALL_FINDING_KINDS: readonly ["logged-out", "logout-unprovable", "missing-hook", "missing-plugin", "unwired-hook", "hook-runtime-broken", "hook-runtime-visibility-unavailable", "cli-missing", "missing-resource", "content-drift", "never-synced", "stale", "repo-behind", "repo-drift", "fleet-resource-gap", "host-cli-missing", "host-cli-invalid", "version-skew", "orphan", "duplicate-hook", "duplicate-hook-drift", "rc-secret-export", "env-secret-export", "auth-bundle-wrong-backend", "exec-policy", "ssh-key-enrollment", "stale-cli", "binary-shadow", "owner-sink-unreachable"];
17
17
  /**
18
18
  * The severity each kind is emitted with - the SINGLE source of truth, read by
19
19
  * the builders below and asserted against both prose rubrics by
@@ -100,6 +100,10 @@ export interface LocalFindingInputs {
100
100
  * scan reports clean while the leak is still in flight (RUSH-1968). Never the
101
101
  * value — only whether it is set. */
102
102
  masterPassphraseInEnv?: boolean;
103
+ /** True when the reserved `auth` bundle exists on a non-file backend
104
+ * (SEC-GAP-3): usage/probe ignores the setup-tokens and falls through to
105
+ * Touch ID. Collected by `inspectReservedAuthBundle`. */
106
+ authBundleWrongBackend?: boolean;
103
107
  /** The effective PowerShell execution policy and the platform it was read on.
104
108
  * Only `win32` yields a finding — the `agents.ps1` launcher is Windows-only. */
105
109
  execPolicy?: {
@@ -26,7 +26,7 @@
26
26
  * never-synced · stale · repo-behind · repo-drift · version-skew ·
27
27
  * fleet-resource-gap · hook-runtime-visibility-unavailable · orphan · duplicate-hook ·
28
28
  * duplicate-hook-drift · host-cli-missing · host-cli-invalid ·
29
- * rc-secret-export · env-secret-export · exec-policy · stale-cli ·
29
+ * rc-secret-export · env-secret-export · auth-bundle-wrong-backend · exec-policy · stale-cli ·
30
30
  * binary-shadow.
31
31
  * (RUSH-2162 moved never-synced and duplicate-hook-drift to WARNING: both are
32
32
  * stale-sync states one `agents sync` resolves, not "needs you now".)
@@ -102,6 +102,7 @@ export const ALL_FINDING_KINDS = [
102
102
  'duplicate-hook-drift', // …with differing content, so a stale copy can disagree
103
103
  'rc-secret-export', // credential-shaped export in a shell rc file
104
104
  'env-secret-export', // the file-store master key live in THIS process's env
105
+ 'auth-bundle-wrong-backend', // reserved `auth` bundle exists but is not file-backed
105
106
  'exec-policy', // Windows execution policy blocks agents.ps1
106
107
  'ssh-key-enrollment', // Windows OpenSSH public-key path/content/ACL is invalid
107
108
  'stale-cli',
@@ -150,6 +151,7 @@ export const FINDING_SEVERITY = {
150
151
  'host-cli-invalid': 'warning',
151
152
  'rc-secret-export': 'warning',
152
153
  'env-secret-export': 'warning',
154
+ 'auth-bundle-wrong-backend': 'warning',
153
155
  'exec-policy': 'warning',
154
156
  'ssh-key-enrollment': 'critical',
155
157
  'stale-cli': 'warning',
@@ -269,6 +271,8 @@ export function remediationFor(finding) {
269
271
  // that inherited it — an editor, a tmux server, the agents daemon — and
270
272
  // each keeps handing it to new children until IT restarts.
271
273
  return 'unset at the source, then restart every process that inherited it (shells, editor, tmux, agents daemon)';
274
+ case 'auth-bundle-wrong-backend':
275
+ return 'agents secrets delete auth --yes && agents secrets create auth --backend file';
272
276
  case 'exec-policy':
273
277
  return 'Set-ExecutionPolicy -Scope CurrentUser RemoteSigned';
274
278
  case 'ssh-key-enrollment':
@@ -519,6 +523,9 @@ export function buildLocalFindings(input) {
519
523
  const envFinding = envSecretFinding(device, input.masterPassphraseInEnv ?? false);
520
524
  if (envFinding)
521
525
  out.push(envFinding);
526
+ const authFinding = authBundleWrongBackendFinding(device, input.authBundleWrongBackend ?? false);
527
+ if (authFinding)
528
+ out.push(authFinding);
522
529
  // Windows execution policy blocking the generated agents.ps1 launcher.
523
530
  const policyFinding = execPolicyFinding(device, input.execPolicy);
524
531
  if (policyFinding)
@@ -688,6 +695,15 @@ function envSecretFinding(device, present) {
688
695
  + '(Expected inside a release sign context, which sets it deliberately.)',
689
696
  });
690
697
  }
698
+ function authBundleWrongBackendFinding(device, present) {
699
+ if (!present)
700
+ return null;
701
+ return finding({
702
+ severity: FINDING_SEVERITY['auth-bundle-wrong-backend'], kind: 'auth-bundle-wrong-backend', device,
703
+ message: "reserved secrets bundle 'auth' exists but is not file-backed — "
704
+ + 'usage/probe ignores the setup-tokens and falls through to the interactive login',
705
+ });
706
+ }
691
707
  function execPolicyFinding(device, execPolicy) {
692
708
  if (!execPolicy || execPolicy.platform !== 'win32')
693
709
  return null;
@@ -1038,6 +1054,8 @@ function warningSubject(f) {
1038
1054
  return 'shell rc';
1039
1055
  if (f.kind === 'env-secret-export')
1040
1056
  return 'environment';
1057
+ if (f.kind === 'auth-bundle-wrong-backend')
1058
+ return 'auth bundle';
1041
1059
  if (f.kind === 'exec-policy')
1042
1060
  return 'PowerShell';
1043
1061
  if (f.kind === 'fleet-resource-gap')
@@ -176,10 +176,12 @@ export declare function removeDevice(name: string): Promise<boolean>;
176
176
  */
177
177
  export type { IgnoredDeviceEntry } from '../fleet/types.js';
178
178
  /**
179
- * The full ignore-list entries — who dismissed a node, when, and on which box —
180
- * the typed read side for `agents devices ignored`. Absent `fleet.ignored` =>
181
- * []. A malformed block is a hard error for the same reason the registry is:
182
- * silently returning [] would let the next write wipe the user's dismissals.
179
+ * The EFFECTIVE ignore-list: the union of every box's device doc
180
+ * `fleet.ignored` (deduped by node name, newest `ignoredAt` winning) plus any
181
+ * lingering central-legacy `fleet.ignored` block (drained by the migration).
182
+ * Deterministic and order-independent. A malformed block is a hard error for the
183
+ * same reason the registry is: silently returning [] would let the next write
184
+ * wipe the user's dismissals.
183
185
  */
184
186
  export declare function loadIgnoredEntries(meta?: Meta): IgnoredDeviceEntry[];
185
187
  /** Load the set of ignored node names. Same corruption contract as
@@ -195,7 +197,11 @@ export declare function isIgnored(name: string): Promise<boolean>;
195
197
  * lib/devices/config-migration.ts.
196
198
  */
197
199
  export declare function withIgnoredAdded(meta: Meta, names: string[], ignoredAt: string): Meta;
198
- /** Add a node name to the ignore-list. Idempotent. Returns the resulting set. */
200
+ /** Add a node name to THIS box's ignore-list (device doc). Idempotent. Returns
201
+ * the resulting cross-box union of dismissed names. Reads only the device docs
202
+ * for the return value — a corrupt central-legacy block surfaces loudly on the
203
+ * effective read path ({@link loadIgnoredEntries}), never blocks a per-box
204
+ * write that does not touch central at all. */
199
205
  export declare function addIgnored(name: string): Promise<Set<string>>;
200
206
  /** Remove a node name from the ignore-list (un-ignore). Returns false if it was
201
207
  * not ignored. */
@@ -20,6 +20,7 @@ import lockfile from 'proper-lockfile';
20
20
  import { getDevicesRegistryPath, readMeta, updateMeta } from '../state.js';
21
21
  import { atomicWriteJsonSync } from '../fs-atomic.js';
22
22
  import { machineId } from '../machine-id.js';
23
+ import { addIgnoredEntry, unionDeviceIgnored } from './device-docs.js';
23
24
  /**
24
25
  * Whether a fan-out should dial this device, honouring the preference stated on
25
26
  * {@link DeviceProfile.reachability}: the live SSH probe wins over the cached
@@ -287,19 +288,48 @@ export async function removeDevice(name) {
287
288
  * []. A malformed block is a hard error for the same reason the registry is:
288
289
  * silently returning [] would let the next write wipe the user's dismissals.
289
290
  */
290
- export function loadIgnoredEntries(meta = readMeta()) {
291
- const raw = meta.fleet?.ignored;
292
- if (raw === undefined)
293
- return [];
291
+ /** Validate a raw ignore-list block, or throw with `where` naming the file. */
292
+ function assertIgnoredShape(raw, where) {
294
293
  if (!Array.isArray(raw) ||
295
294
  raw.some((e) => !e ||
296
295
  typeof e.name !== 'string' ||
297
296
  typeof e.ignoredAt !== 'string' ||
298
297
  typeof e.ignoredOn !== 'string')) {
299
- throw new Error(`Device ignore-list corrupted in agents.yaml (fleet.ignored): expected a list of { name, ignoredAt, ignoredOn } entries. Inspect and repair ~/.agents/agents.yaml.`);
298
+ throw new Error(`Device ignore-list corrupted in ${where}: expected a list of { name, ignoredAt, ignoredOn } entries. Inspect and repair it.`);
300
299
  }
300
+ }
301
+ /**
302
+ * THIS box's OWN dismissals — the writable slice in `meta.deviceFleet.ignored`
303
+ * (the device doc). `withIgnoredAdded`/`removeIgnored` operate on this so a box
304
+ * only ever edits its own folder (PHNX-3315). Absent => [].
305
+ */
306
+ function loadOwnIgnoredEntries(meta) {
307
+ const raw = meta.deviceFleet?.ignored;
308
+ if (raw === undefined)
309
+ return [];
310
+ assertIgnoredShape(raw, `devices/<machine>/agents.yaml (fleet.ignored)`);
301
311
  return raw;
302
312
  }
313
+ /**
314
+ * The EFFECTIVE ignore-list: the union of every box's device doc
315
+ * `fleet.ignored` (deduped by node name, newest `ignoredAt` winning) plus any
316
+ * lingering central-legacy `fleet.ignored` block (drained by the migration).
317
+ * Deterministic and order-independent. A malformed block is a hard error for the
318
+ * same reason the registry is: silently returning [] would let the next write
319
+ * wipe the user's dismissals.
320
+ */
321
+ export function loadIgnoredEntries(meta = readMeta()) {
322
+ const byName = new Map();
323
+ const central = meta.fleet?.ignored;
324
+ if (central !== undefined) {
325
+ assertIgnoredShape(central, `agents.yaml (fleet.ignored)`);
326
+ for (const e of central)
327
+ addIgnoredEntry(byName, e);
328
+ }
329
+ for (const e of unionDeviceIgnored())
330
+ addIgnoredEntry(byName, e);
331
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
332
+ }
303
333
  /** Load the set of ignored node names. Same corruption contract as
304
334
  * {@link loadIgnoredEntries}. */
305
335
  export async function loadIgnored() {
@@ -317,40 +347,38 @@ export async function isIgnored(name) {
317
347
  * lib/devices/config-migration.ts.
318
348
  */
319
349
  export function withIgnoredAdded(meta, names, ignoredAt) {
320
- const entries = loadIgnoredEntries(meta); // throws on a corrupted block — never wipe it
350
+ const entries = loadOwnIgnoredEntries(meta); // throws on a corrupted block — never wipe it
321
351
  const have = new Set(entries.map((e) => e.name));
322
352
  const fresh = names.filter((n) => !have.has(n));
323
353
  if (fresh.length === 0)
324
354
  return meta;
325
- const fleet = (meta.fleet ?? { devices: {} });
326
355
  const ignored = [
327
356
  ...entries,
328
357
  ...fresh.map((name) => ({ name, ignoredAt, ignoredOn: machineId() })),
329
358
  ].sort((a, b) => a.name.localeCompare(b.name));
330
- const nextFleet = { ...fleet, ignored };
331
- return { ...meta, fleet: nextFleet };
359
+ return { ...meta, deviceFleet: { ...meta.deviceFleet, ignored } };
332
360
  }
333
- /** Add a node name to the ignore-list. Idempotent. Returns the resulting set. */
361
+ /** Add a node name to THIS box's ignore-list (device doc). Idempotent. Returns
362
+ * the resulting cross-box union of dismissed names. Reads only the device docs
363
+ * for the return value — a corrupt central-legacy block surfaces loudly on the
364
+ * effective read path ({@link loadIgnoredEntries}), never blocks a per-box
365
+ * write that does not touch central at all. */
334
366
  export async function addIgnored(name) {
335
367
  assertValidDeviceName(name);
336
- const meta = updateMeta((m) => withIgnoredAdded(m, [name], new Date().toISOString()));
337
- return new Set(loadIgnoredEntries(meta).map((e) => e.name));
368
+ updateMeta((m) => withIgnoredAdded(m, [name], new Date().toISOString()));
369
+ return new Set(unionDeviceIgnored().map((e) => e.name));
338
370
  }
339
371
  /** Remove a node name from the ignore-list (un-ignore). Returns false if it was
340
372
  * not ignored. */
341
373
  export async function removeIgnored(name) {
342
374
  let removed = false;
343
375
  updateMeta((m) => {
344
- const fleet = m.fleet;
345
- if (!fleet?.ignored)
346
- return m;
347
- const entries = loadIgnoredEntries(m);
376
+ const entries = loadOwnIgnoredEntries(m); // only this box's own dismissals are ours to drop
348
377
  const next = entries.filter((e) => e.name !== name);
349
378
  if (next.length === entries.length)
350
379
  return m;
351
380
  removed = true;
352
- const nextFleet = { ...fleet, ignored: next };
353
- return { ...m, fleet: nextFleet };
381
+ return { ...m, deviceFleet: { ...m.deviceFleet, ignored: next } };
354
382
  });
355
383
  return removed;
356
384
  }
@@ -1,4 +1,5 @@
1
1
  import type { AgentId, Mode } from './types.js';
2
+ import { type UsernsStatus } from './linux-userns.js';
2
3
  /**
3
4
  * Agent execution modes. Canonical name `skip` (dangerously skip permissions);
4
5
  * `full` is accepted as a permanent silent alias via normalizeMode().
@@ -88,11 +89,46 @@ export interface ModeWarningState {
88
89
  export declare function defaultModeFor(agent: AgentId): Mode;
89
90
  /** Safe mode used when the user did not provide --mode or a configured default. */
90
91
  export declare function implicitModeFor(agent: AgentId): ExecMode;
92
+ /**
93
+ * Preflight for Codex's Linux sandbox. Codex ≥0.146 sandboxes `read-only` and
94
+ * `workspace-write` runs with a bundled bubblewrap that needs an unprivileged
95
+ * user namespace; on a box that restricts it (Ubuntu 24.04
96
+ * `apparmor_restrict_unprivileged_userns=1`) bwrap dies with "setting up uid map:
97
+ * Permission denied" and a HEADLESS codex run lands zero tools — no file writes,
98
+ * no shell — while still reporting a completed turn. That silent under-delivery
99
+ * is what breaks `agents teams` codex teammates (always headless + workspace-write)
100
+ * and headless `agents run codex` alike on the fleet. Returns a loud, actionable
101
+ * message to fail the launch with instead of spawning that doomed run; returns
102
+ * null when the run is fine to proceed.
103
+ *
104
+ * Deliberately scoped: only `codex`, only Linux, only a HEADLESS run (an
105
+ * interactive TUI surfaces the bwrap error to the operator itself), and only a
106
+ * SANDBOXED mode — `skip` is codex `--dangerously-bypass-approvals-and-sandbox`,
107
+ * which uses no bwrap and is unaffected. The intended auto=workspace-write config
108
+ * is never weakened here; the run fails loud rather than silently downgrading.
109
+ * PHNX-3285.
110
+ */
111
+ export declare function codexSandboxPreflight(args: {
112
+ agent: AgentId;
113
+ platform: NodeJS.Platform;
114
+ interactive: boolean;
115
+ mode: Mode;
116
+ userns: UsernsStatus;
117
+ machine: string;
118
+ }): string | null;
91
119
  /** Reasoning effort levels passed to agents that support them. 'auto' defers to the agent's default. */
92
120
  export type ExecEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'auto';
93
121
  /** Options for spawning an agent process. Omitting `prompt` launches the CLI interactively. */
94
122
  export interface ExecOptions {
95
123
  agent: AgentId;
124
+ /**
125
+ * Custom harness / profile name when this run was launched via
126
+ * `agents run <profile>` (e.g. `deepseek`). `agent` stays the HOST CLI
127
+ * that actually executes. Stamped onto `AGENTS_AGENT_NAME`, the pid
128
+ * registry, and the session-actor sidecar so listings can tell the
129
+ * profile apart from a native host run (PHNX-2935).
130
+ */
131
+ harnessName?: string;
96
132
  version?: string;
97
133
  /** Version home whose native auth/config is overlaid onto this run's binary. */
98
134
  configVersion?: string;
@@ -177,6 +213,18 @@ export interface ExecOptions {
177
213
  */
178
214
  raw?: boolean;
179
215
  }
216
+ /**
217
+ * Identity a custom-harness run stamps on env / pid-registry / sidecars.
218
+ * `agent` is the host CLI; `harnessName` is the profile the user launched.
219
+ * Empty/whitespace harness names fall back to the host so a blank stamp
220
+ * never hides a real agent.
221
+ */
222
+ export declare function stampedAgentName(options: Pick<ExecOptions, 'agent' | 'harnessName'>): string;
223
+ /**
224
+ * Profile name when it differs from the host agent. Undefined for a native
225
+ * run, so pid-registry / sidecar records stay sparse.
226
+ */
227
+ export declare function customHarnessName(options: Pick<ExecOptions, 'agent' | 'harnessName'>): string | undefined;
180
228
  /**
181
229
  * Resolve interactive vs headless. Explicit flags are definitive and win over
182
230
  * inference: `--interactive` forces interactive, `--headless` forces headless.
@@ -406,15 +454,18 @@ export interface TmuxWrapContext {
406
454
  * True when this run was dispatched onto this box over SSH by `--device`
407
455
  * (the launcher exports {@link REMOTE_INTERACTIVE_ENV}).
408
456
  *
409
- * A remote interactive agent is a child of the sshd session and holds its
410
- * controlling TTY, so without the wrap a dropped link SIGHUPs it and the work
411
- * in flight is gone. Durability is therefore NOT a preference the way
412
- * `configEnabled` is — `tmux.enabled` is about whether this box's operator
413
- * likes tmux's mouse/clipboard/scrollback at their own keyboard, which says
414
- * nothing about whether a run arriving over the network must outlive it.
415
- * Conflating the two is what left every `--device` agent unsurvivable
416
- * (RUSH-3125), while lib/hosts/reconnect.ts reconnected on the premise that
417
- * they were detached.
457
+ * Since PHNX-3316 this flag no longer forces the wrap: `tmux.enabled` gates
458
+ * local and remote runs alike, because the operator reading "tmux disabled"
459
+ * expects NO tmux anywhere. A followed remote run left bare is protected by
460
+ * reconnect-and-resume (lib/hosts/reconnect.ts): a dropped link costs the
461
+ * in-flight turn, and the harness session resumes from disk.
462
+ *
463
+ * The flag still matters for one case: a followed remote run whose LAUNCHER
464
+ * has no TTY (CI, a script, another agent driving the CLI — `sshStream`
465
+ * allocates the peer's TTY from `process.stdin.isTTY`, dispatch.ts) gets no
466
+ * TTY on the peer either, so there is nothing to attach to and the detached
467
+ * pane is the run's only interface — it wraps regardless of `configEnabled`
468
+ * (and is refused as `undurable` when tmux is missing).
418
469
  */
419
470
  remoteDispatch: boolean;
420
471
  /** Whether a tmux binary is on PATH. */
@@ -423,16 +474,19 @@ export interface TmuxWrapContext {
423
474
  * True when this process has a real TTY to attach (`stdout.isTTY`).
424
475
  * A piped `agents run --interactive` (session-tracker tests, CI) has none:
425
476
  * wrapping then treating the failed attach as Ctrl-b d leaked live panes
426
- * for a week on yosemite-s0 (PHNX-3293). Remote dispatch still wraps
427
- * without a TTY — `--device --no-follow` *wants* a detached pane.
477
+ * for a week on yosemite-s0 (PHNX-3293). A remote dispatch still wraps
478
+ * without a TTY — a launcher that has none (CI, scripts, another agent)
479
+ * gives the peer nothing to attach to, so the detached pane is the run's
480
+ * only interface.
428
481
  */
429
482
  hasTty: boolean;
430
483
  }
431
484
  /**
432
- * What to do with an interactive spawn. Three outcomes, not two: a run that
433
- * MUST be durable and cannot be is neither "wrap" nor "spawn bare" — it is a
434
- * launch that should not happen, because a bare remote spawn looks fine right
435
- * up until the link blinks and the agent dies with it.
485
+ * What to do with an interactive spawn. Three outcomes, not two: a remote run
486
+ * that WOULD wrap (the operator opted in, or a `--no-follow` run whose only
487
+ * interface is the detached pane) on a box with no tmux is neither "wrap" nor
488
+ * "spawn bare" — it is a launch that should not happen, because a bare remote
489
+ * spawn looks fine right up until the link blinks and the agent dies with it.
436
490
  */
437
491
  export type TmuxWrapDecision =
438
492
  /** Run the agent in a detached tmux session and attach this TTY. */
@@ -443,7 +497,7 @@ export type TmuxWrapDecision =
443
497
  | {
444
498
  kind: 'bare';
445
499
  }
446
- /** Remote-dispatched and tmux is missing on this box: refuse, don't pretend. */
500
+ /** Remote-dispatched, wants the wrap, and tmux is missing: refuse, don't pretend. */
447
501
  | {
448
502
  kind: 'undurable';
449
503
  };
@@ -451,20 +505,24 @@ export type TmuxWrapDecision =
451
505
  * Decide whether to run an interactive agent INSIDE a detached tmux session on
452
506
  * the shared socket (then attach the current TTY) instead of a bare spawn.
453
507
  *
454
- * Wrapping serves two independent purposes, and they are gated differently:
455
- *
456
- * - **Addressability** (`configEnabled`): a unique `%pane` handle so
457
- * `agents sessions --active` can tell co-located agents apart and `agents
458
- * focus` re-attaches without forking. That is a local preference — the
459
- * operator turns it on once tmux's mouse/clipboard/scrollback suits them.
460
- * - **Durability** (`remoteDispatch`): a run that arrived over SSH must outlive
461
- * the SSH client, because a bare remote spawn dies of SIGHUP the moment the
462
- * link blinks. That is not a preference, so it does not consult
463
- * `configEnabled` (RUSH-3125 — see {@link TmuxWrapContext.remoteDispatch}).
464
- *
465
- * The per-run opt-outs bind BOTH: `--raw` / `--no-tmux` / `AGENTS_NO_TMUX=1` are
466
- * explicit "I want the bare process" requests, and honouring them over the
467
- * durability rule keeps one escape hatch that always works.
508
+ * The wrap is opt-in via this device's `tmux.enabled` (`configEnabled`): a
509
+ * unique `%pane` handle so `agents sessions --active` can tell co-located
510
+ * agents apart and `agents focus` re-attaches without forking, plus scrollback
511
+ * and mouse at the operator's keyboard. Off means OFF, for local and remote
512
+ * runs alike (PHNX-3316) — a followed `--device` run left bare is protected by
513
+ * reconnect-and-resume (lib/hosts/reconnect.ts), which rejoins the live pane
514
+ * when one exists and resumes the harness session from disk when it does not.
515
+ * The RUSH-3125 forced remote wrap conflated durability with that preference
516
+ * and surprised every operator who had explicitly left tmux off.
517
+ *
518
+ * One case still wraps regardless: a followed remote run whose launcher has
519
+ * no TTY (CI, scripts, another agent) gives the peer nothing to attach to —
520
+ * the detached pane is the run's only interface, not an ergonomics choice.
521
+ *
522
+ * The per-run opt-outs bind everything: `--raw` / `--no-tmux` /
523
+ * `AGENTS_NO_TMUX=1` are explicit "I want the bare process" requests, and an
524
+ * escape hatch that silently stopped applying over `--device` would be worse
525
+ * than the bare run the user asked for.
468
526
  *
469
527
  * Pure, so the gate is unit-tested independently of the (side-effecting) spawn.
470
528
  */