@phnx-labs/agents-cli 1.22.47 → 1.22.49

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 (249) hide show
  1. package/CHANGELOG.md +401 -17
  2. package/README.md +66 -32
  3. package/dist/bootstrap.js +4 -2
  4. package/dist/cli/command-registry.d.ts +1 -0
  5. package/dist/cli/command-registry.js +2 -0
  6. package/dist/commands/accounts.d.ts +58 -0
  7. package/dist/commands/accounts.js +206 -32
  8. package/dist/commands/artifacts.js +7 -6
  9. package/dist/commands/attach.js +1 -1
  10. package/dist/commands/browser.js +501 -103
  11. package/dist/commands/config.d.ts +0 -1
  12. package/dist/commands/config.js +0 -20
  13. package/dist/commands/daemon.d.ts +21 -0
  14. package/dist/commands/daemon.js +147 -12
  15. package/dist/commands/exec.js +52 -15
  16. package/dist/commands/focus.d.ts +14 -0
  17. package/dist/commands/focus.js +48 -5
  18. package/dist/commands/go.d.ts +3 -0
  19. package/dist/commands/go.js +16 -3
  20. package/dist/commands/inspect.js +2 -2
  21. package/dist/commands/lease.d.ts +1 -1
  22. package/dist/commands/lease.js +4 -3
  23. package/dist/commands/mailboxes.js +4 -3
  24. package/dist/commands/menubar.js +11 -4
  25. package/dist/commands/projects.d.ts +16 -1
  26. package/dist/commands/projects.js +58 -64
  27. package/dist/commands/resume.js +1 -1
  28. package/dist/commands/route.js +4 -4
  29. package/dist/commands/routines.js +3 -2
  30. package/dist/commands/run-broadcast.d.ts +1 -1
  31. package/dist/commands/run-broadcast.js +1 -1
  32. package/dist/commands/sessions-picker.d.ts +22 -0
  33. package/dist/commands/sessions-picker.js +48 -9
  34. package/dist/commands/sessions-trace.js +1 -1
  35. package/dist/commands/sessions.js +1 -0
  36. package/dist/commands/setup-secrets.js +23 -0
  37. package/dist/commands/share.d.ts +20 -5
  38. package/dist/commands/share.js +152 -52
  39. package/dist/commands/ssh.js +54 -7
  40. package/dist/commands/traces.d.ts +2 -0
  41. package/dist/commands/traces.js +184 -0
  42. package/dist/commands/view.d.ts +1 -1
  43. package/dist/commands/view.js +17 -34
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.js +5 -0
  46. package/dist/lib/accounting/usage.d.ts +14 -3
  47. package/dist/lib/accounting/usage.js +64 -18
  48. package/dist/lib/agent-spec/agents.js +11 -9
  49. package/dist/lib/browser/drivers/ssh.d.ts +16 -2
  50. package/dist/lib/browser/drivers/ssh.js +36 -9
  51. package/dist/lib/browser/ipc.js +8 -3
  52. package/dist/lib/browser/profiles.d.ts +35 -101
  53. package/dist/lib/browser/profiles.js +130 -301
  54. package/dist/lib/browser/registry.d.ts +39 -0
  55. package/dist/lib/browser/registry.js +149 -0
  56. package/dist/lib/browser/remote-control.d.ts +8 -6
  57. package/dist/lib/browser/remote-control.js +8 -6
  58. package/dist/lib/browser/resolve-target.d.ts +68 -0
  59. package/dist/lib/browser/resolve-target.js +250 -0
  60. package/dist/lib/browser/runtime-state.d.ts +40 -16
  61. package/dist/lib/browser/runtime-state.js +92 -25
  62. package/dist/lib/browser/service.d.ts +28 -9
  63. package/dist/lib/browser/service.js +139 -62
  64. package/dist/lib/browser/task-index.d.ts +70 -0
  65. package/dist/lib/browser/task-index.js +180 -0
  66. package/dist/lib/byok-usage.js +2 -2
  67. package/dist/lib/claude-statusline.d.ts +29 -0
  68. package/dist/lib/claude-statusline.js +139 -0
  69. package/dist/lib/cloud/host.d.ts +1 -1
  70. package/dist/lib/cloud/host.js +1 -1
  71. package/dist/lib/cloud/rush.js +1 -1
  72. package/dist/lib/computer/computer-rpc.js +2 -2
  73. package/dist/lib/computer/download.d.ts +15 -15
  74. package/dist/lib/computer/download.js +35 -98
  75. package/dist/lib/computer/ssh-tunnel.d.ts +0 -3
  76. package/dist/lib/computer/ssh-tunnel.js +12 -22
  77. package/dist/lib/config-keys.d.ts +2 -8
  78. package/dist/lib/config-keys.js +2 -13
  79. package/dist/lib/crabbox/cli.d.ts +1 -1
  80. package/dist/lib/crabbox/cli.js +2 -2
  81. package/dist/lib/crabbox/lease.d.ts +9 -0
  82. package/dist/lib/crabbox/lease.js +70 -15
  83. package/dist/lib/crabbox/runtimes.js +1 -1
  84. package/dist/lib/daemon/account-state-daemon-service.d.ts +16 -0
  85. package/dist/lib/daemon/account-state-daemon-service.js +26 -0
  86. package/dist/lib/daemon/browser-ipc-service.d.ts +20 -0
  87. package/dist/lib/daemon/browser-ipc-service.js +46 -0
  88. package/dist/lib/daemon/daemon.d.ts +3 -0
  89. package/dist/lib/daemon/daemon.js +212 -444
  90. package/dist/lib/daemon/device-probe-service.d.ts +26 -0
  91. package/dist/lib/daemon/device-probe-service.js +47 -0
  92. package/dist/lib/daemon/keychain-reap-service.d.ts +17 -0
  93. package/dist/lib/daemon/keychain-reap-service.js +32 -0
  94. package/dist/lib/daemon/monitor-engine-service.d.ts +22 -0
  95. package/dist/lib/daemon/monitor-engine-service.js +29 -0
  96. package/dist/lib/daemon/secrets-broker-service.d.ts +20 -0
  97. package/dist/lib/daemon/secrets-broker-service.js +66 -0
  98. package/dist/lib/daemon/self-heal-service.d.ts +27 -0
  99. package/dist/lib/daemon/self-heal-service.js +48 -0
  100. package/dist/lib/daemon/service.d.ts +102 -0
  101. package/dist/lib/daemon/service.js +98 -0
  102. package/dist/lib/daemon/session-index-service.d.ts +22 -0
  103. package/dist/lib/daemon/session-index-service.js +40 -0
  104. package/dist/lib/daemon/state-dir-check-service.d.ts +43 -0
  105. package/dist/lib/daemon/state-dir-check-service.js +63 -0
  106. package/dist/lib/daemon/supervisor.d.ts +87 -0
  107. package/dist/lib/daemon/supervisor.js +298 -0
  108. package/dist/lib/daemon/watchdog-service.d.ts +19 -0
  109. package/dist/lib/daemon/watchdog-service.js +40 -0
  110. package/dist/lib/daemon-health.d.ts +15 -0
  111. package/dist/lib/daemon-health.js +29 -5
  112. package/dist/lib/daemon-services.d.ts +8 -1
  113. package/dist/lib/daemon-services.js +55 -0
  114. package/dist/lib/daemon-ticks.d.ts +13 -2
  115. package/dist/lib/daemon-ticks.js +20 -18
  116. package/dist/lib/device-config.d.ts +0 -2
  117. package/dist/lib/device-config.js +1 -23
  118. package/dist/lib/devices/registry.d.ts +1 -1
  119. package/dist/lib/devices/registry.js +1 -1
  120. package/dist/lib/devices/stats-cache.d.ts +20 -0
  121. package/dist/lib/devices/stats-cache.js +49 -5
  122. package/dist/lib/devices/worker-pick.d.ts +48 -0
  123. package/dist/lib/devices/worker-pick.js +113 -0
  124. package/dist/lib/exec.d.ts +67 -13
  125. package/dist/lib/exec.js +85 -29
  126. package/dist/lib/harness/adapters/cursor.js +8 -11
  127. package/dist/lib/helper-download.d.ts +108 -0
  128. package/dist/lib/helper-download.js +194 -0
  129. package/dist/lib/helper-versions.d.ts +52 -0
  130. package/dist/lib/helper-versions.js +58 -0
  131. package/dist/lib/hooks/cache.js +11 -0
  132. package/dist/lib/hooks/install.js +2 -2
  133. package/dist/lib/hosts/credentials.d.ts +1 -1
  134. package/dist/lib/hosts/credentials.js +1 -1
  135. package/dist/lib/hosts/dispatch.d.ts +7 -1
  136. package/dist/lib/hosts/dispatch.js +24 -5
  137. package/dist/lib/hosts/passthrough.d.ts +8 -15
  138. package/dist/lib/hosts/passthrough.js +19 -16
  139. package/dist/lib/hosts/providers/devices.d.ts +1 -1
  140. package/dist/lib/hosts/providers/devices.js +1 -1
  141. package/dist/lib/hosts/reconnect.d.ts +179 -21
  142. package/dist/lib/hosts/reconnect.js +266 -45
  143. package/dist/lib/installations/migrate.d.ts +4 -5
  144. package/dist/lib/installations/migrate.js +6 -7
  145. package/dist/lib/installations/shims.d.ts +1 -1
  146. package/dist/lib/installations/shims.js +23 -13
  147. package/dist/lib/installations/versions.js +25 -2
  148. package/dist/lib/linear-autoclose.d.ts +1 -1
  149. package/dist/lib/linear-autoclose.js +1 -1
  150. package/dist/lib/loop.d.ts +2 -2
  151. package/dist/lib/loop.js +1 -1
  152. package/dist/lib/menubar/download-menubar.d.ts +47 -0
  153. package/dist/lib/menubar/download-menubar.js +60 -0
  154. package/dist/lib/menubar/install-menubar.d.ts +31 -7
  155. package/dist/lib/menubar/install-menubar.js +79 -21
  156. package/dist/lib/menubar/notify-desktop.d.ts +3 -3
  157. package/dist/lib/menubar/notify-desktop.js +6 -6
  158. package/dist/lib/model-tiers.d.ts +1 -1
  159. package/dist/lib/project-probe.d.ts +1 -1
  160. package/dist/lib/project-probe.js +1 -1
  161. package/dist/lib/project-pull.d.ts +1 -1
  162. package/dist/lib/project-pull.js +2 -2
  163. package/dist/lib/projects.d.ts +1 -1
  164. package/dist/lib/projects.js +1 -1
  165. package/dist/lib/sandbox.js +6 -7
  166. package/dist/lib/secrets/download-keychain.d.ts +47 -0
  167. package/dist/lib/secrets/download-keychain.js +70 -0
  168. package/dist/lib/secrets/install-helper.d.ts +20 -1
  169. package/dist/lib/secrets/install-helper.js +44 -3
  170. package/dist/lib/session/active.js +17 -1
  171. package/dist/lib/session/db.d.ts +11 -1
  172. package/dist/lib/session/db.js +138 -6
  173. package/dist/lib/session/discover.d.ts +21 -1
  174. package/dist/lib/session/discover.js +33 -5
  175. package/dist/lib/session/highlights.d.ts +49 -1
  176. package/dist/lib/session/highlights.js +95 -0
  177. package/dist/lib/session/host-link.d.ts +16 -2
  178. package/dist/lib/session/host-link.js +7 -1
  179. package/dist/lib/session/parse.js +33 -1
  180. package/dist/lib/session/remote/remote-list.d.ts +9 -0
  181. package/dist/lib/session/remote/remote-list.js +19 -2
  182. package/dist/lib/session/remote/watch.js +11 -2
  183. package/dist/lib/session/session-cache.d.ts +53 -0
  184. package/dist/lib/session/session-cache.js +88 -0
  185. package/dist/lib/session/state.js +1 -1
  186. package/dist/lib/session/tool-calls.d.ts +2 -0
  187. package/dist/lib/session/tool-calls.js +1 -1
  188. package/dist/lib/session/trajectory-html.d.ts +5 -1
  189. package/dist/lib/session/trajectory-html.js +42 -22
  190. package/dist/lib/session/trajectory-text.js +9 -2
  191. package/dist/lib/session/trajectory.js +53 -4
  192. package/dist/lib/session/types.d.ts +15 -0
  193. package/dist/lib/sha256-asset.d.ts +26 -0
  194. package/dist/lib/sha256-asset.js +41 -0
  195. package/dist/lib/share/backend.d.ts +95 -0
  196. package/dist/lib/share/backend.js +139 -0
  197. package/dist/lib/share/delete.d.ts +6 -2
  198. package/dist/lib/share/delete.js +8 -9
  199. package/dist/lib/share/html.d.ts +22 -0
  200. package/dist/lib/share/html.js +88 -0
  201. package/dist/lib/share/provision.d.ts +18 -4
  202. package/dist/lib/share/provision.js +38 -4
  203. package/dist/lib/share/publish.d.ts +39 -15
  204. package/dist/lib/share/publish.js +55 -26
  205. package/dist/lib/share/worker-template.js +171 -11
  206. package/dist/lib/sqlite.js +16 -3
  207. package/dist/lib/ssh-exec.d.ts +65 -0
  208. package/dist/lib/ssh-exec.js +120 -4
  209. package/dist/lib/startup/command-registry.js +1 -1
  210. package/dist/lib/startup/dev-build.js +3 -3
  211. package/dist/lib/state.d.ts +1 -1
  212. package/dist/lib/state.js +7 -3
  213. package/dist/lib/subagents-registry.js +1 -1
  214. package/dist/lib/terminal/index.d.ts +1 -1
  215. package/dist/lib/terminal/types.d.ts +1 -1
  216. package/dist/lib/terminal/types.js +1 -1
  217. package/dist/lib/tmux/orphan-reap.js +1 -1
  218. package/dist/lib/tmux/session.js +31 -0
  219. package/dist/lib/traces/backend.d.ts +29 -0
  220. package/dist/lib/traces/backend.js +46 -0
  221. package/dist/lib/traces/classify.d.ts +52 -0
  222. package/dist/lib/traces/classify.js +72 -0
  223. package/dist/lib/traces/config.d.ts +10 -0
  224. package/dist/lib/traces/config.js +2 -0
  225. package/dist/lib/traces/provision.d.ts +19 -0
  226. package/dist/lib/traces/provision.js +30 -0
  227. package/dist/lib/traces/sync.d.ts +150 -0
  228. package/dist/lib/traces/sync.js +447 -0
  229. package/dist/lib/traces/worker-template.d.ts +4 -0
  230. package/dist/lib/traces/worker-template.js +159 -0
  231. package/dist/lib/types.d.ts +18 -19
  232. package/dist/lib/types.js +14 -0
  233. package/dist/lib/usage-refresh.d.ts +1 -1
  234. package/dist/lib/usage-refresh.js +11 -8
  235. package/dist/lib/workflows.d.ts +1 -1
  236. package/package.json +4 -6
  237. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  238. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +0 -24
  239. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  240. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  241. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +0 -128
  242. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  243. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -26
  244. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  245. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  246. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -135
  247. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  248. package/dist/lib/usage-fleet.d.ts +0 -32
  249. package/dist/lib/usage-fleet.js +0 -125
package/dist/lib/exec.js CHANGED
@@ -8,7 +8,7 @@ import { spawn } from 'child_process';
8
8
  import { randomUUID } from 'crypto';
9
9
  import * as fs from 'fs';
10
10
  import * as path from 'path';
11
- import { ALL_MODES } from './types.js';
11
+ import { ALL_MODES, REMOTE_INTERACTIVE_ENV } from './types.js';
12
12
  import { AGENTS, findInPath } from './agents.js';
13
13
  import { parseTimeout } from './scheduling/routines.js';
14
14
  import { compareVersions, getBinaryPath, getVersionHomePath, isVersionInstalled, listInstalledVersions, resolveVersion } from './installations/versions.js';
@@ -1176,36 +1176,47 @@ export function isPaneKnownAliveFromQueryResult(code, stdout) {
1176
1176
  * Decide whether to run an interactive agent INSIDE a detached tmux session on
1177
1177
  * the shared socket (then attach the current TTY) instead of a bare spawn.
1178
1178
  *
1179
- * Opting into tmux wrapping gives every interactive agent an exact, unique `%pane` handle so
1180
- * `agents sessions --active` can tell co-located agents apart, and lets `agents
1181
- * focus` re-attach a live session without forking it. Pure so the gate is unit-
1182
- * tested independently of the (side-effecting) spawn.
1179
+ * Wrapping serves two independent purposes, and they are gated differently:
1183
1180
  *
1184
- * All seven guards must pass:
1185
- * - interactive — a headless `-p` run has no TTY to attach; keep bare spawn.
1186
- * - not Windows — no tmux path on win32.
1187
- * - not already in tmux — nesting tmux-in-tmux is pointless and confusing.
1188
- * - not --raw — explicit opt-out.
1189
- * - not AGENTS_NO_TMUX=1 env opt-out (CI, scripts, the shim passthrough path).
1190
- * - tmux.enabled — this device explicitly opted into addressable tmux panes.
1191
- * - tmux installed otherwise there is nothing to wrap with.
1181
+ * - **Addressability** (`configEnabled`): a unique `%pane` handle so
1182
+ * `agents sessions --active` can tell co-located agents apart and `agents
1183
+ * focus` re-attaches without forking. That is a local preference — the
1184
+ * operator turns it on once tmux's mouse/clipboard/scrollback suits them.
1185
+ * - **Durability** (`remoteDispatch`): a run that arrived over SSH must outlive
1186
+ * the SSH client, because a bare remote spawn dies of SIGHUP the moment the
1187
+ * link blinks. That is not a preference, so it does not consult
1188
+ * `configEnabled` (RUSH-3125see {@link TmuxWrapContext.remoteDispatch}).
1189
+ *
1190
+ * The per-run opt-outs bind BOTH: `--raw` / `--no-tmux` / `AGENTS_NO_TMUX=1` are
1191
+ * explicit "I want the bare process" requests, and honouring them over the
1192
+ * durability rule keeps one escape hatch that always works.
1193
+ *
1194
+ * Pure, so the gate is unit-tested independently of the (side-effecting) spawn.
1192
1195
  */
1193
- export function shouldWrapInTmux(ctx) {
1196
+ export function resolveTmuxWrap(ctx) {
1197
+ // A headless `-p` run has no TTY to attach, Windows has no tmux path, and
1198
+ // nesting tmux-in-tmux is pointless — none of these can wrap, and none of them
1199
+ // is a remote interactive agent whose life depends on it.
1194
1200
  if (!ctx.interactive)
1195
- return false;
1201
+ return { kind: 'bare' };
1196
1202
  if (ctx.platform === 'win32')
1197
- return false;
1203
+ return { kind: 'bare' };
1198
1204
  if (ctx.inTmux)
1199
- return false;
1205
+ return { kind: 'bare' };
1206
+ // Explicit opt-outs win over the durability rule: `--raw` exists precisely to
1207
+ // get the unwrapped process, and a flag that silently stopped working on a
1208
+ // remote box would be worse than an undurable run the user asked for.
1200
1209
  if (ctx.raw)
1201
- return false;
1210
+ return { kind: 'bare' };
1202
1211
  if (ctx.noTmuxEnv)
1203
- return false;
1204
- if (!ctx.configEnabled)
1205
- return false;
1212
+ return { kind: 'bare' };
1213
+ if (!ctx.configEnabled && !ctx.remoteDispatch)
1214
+ return { kind: 'bare' };
1215
+ // Fail loud rather than launch a remote agent that a blink would kill: the
1216
+ // caller refuses the run instead of starting work that cannot be recovered.
1206
1217
  if (!ctx.tmuxAvailable)
1207
- return false;
1208
- return true;
1218
+ return ctx.remoteDispatch ? { kind: 'undurable' } : { kind: 'bare' };
1219
+ return { kind: 'wrap' };
1209
1220
  }
1210
1221
  /**
1211
1222
  * Build the shell command that runs an agent inside a tmux pane with the exact
@@ -1225,6 +1236,29 @@ export function shouldWrapInTmux(ctx) {
1225
1236
  * into SessionMeta.cmd on disk (tmux/session.ts). The launched command uses the
1226
1237
  * real values; the stored/informational copy uses the redacted form (RUSH-1758).
1227
1238
  */
1239
+ /**
1240
+ * True when `options.sessionId` is an id the HARNESS actually received, and so a
1241
+ * real, resumable handle — rather than one the launcher generated for its own
1242
+ * bookkeeping and the agent never saw.
1243
+ *
1244
+ * Mirrors exactly the two branches in buildExecCommand that put the id on the
1245
+ * command line: a native resume (any harness with a `resume` spec) and claude's
1246
+ * create-with-`--session-id`. Everything else — `agents run codex --session-id X`
1247
+ * without `--resume`, or a fresh non-claude launch whose real id only shows up
1248
+ * later via the SessionStart hook — leaves the agent with an id of its own
1249
+ * choosing, so `options.sessionId` must not be published as if it were that id.
1250
+ *
1251
+ * Keep in lockstep with buildExecCommand's resume/create branches: a harness
1252
+ * that gains a forced-session-id flag must be reflected here in the SAME change,
1253
+ * or this reports a genuine id as fabricated.
1254
+ */
1255
+ export function isHarnessKnownSessionId(agent, sessionId, resume) {
1256
+ if (!sessionId)
1257
+ return false;
1258
+ if (resume && AGENT_COMMANDS[agent]?.resume)
1259
+ return true;
1260
+ return agent === 'claude';
1261
+ }
1228
1262
  export function buildTmuxAgentCommand(executable, args, env, opts = {}) {
1229
1263
  const agentCmd = [executable, ...args].map(shellQuote).join(' ');
1230
1264
  // envFile: source the values instead of inlining them, so no VALUE ever lands
@@ -1430,8 +1464,13 @@ async function runInTmux(options, executable, args) {
1430
1464
  const cmd = buildTmuxAgentCommand(executable, args, execEnv, { envFile });
1431
1465
  const metaCmd = buildTmuxAgentCommand(executable, args, execEnv, { redactEnvValues: true });
1432
1466
  const labels = { agent: options.agent };
1433
- if (options.sessionId)
1467
+ // Only publish an id the harness actually received. createSession turns this
1468
+ // label into the `@ag_session_id` tmux option that status bars read, and a
1469
+ // launcher-internal id displayed there looks exactly like a resumable one
1470
+ // while `ag focus <id>` rejects it.
1471
+ if (isHarnessKnownSessionId(options.agent, options.sessionId, options.resume)) {
1434
1472
  labels.sessionId = options.sessionId;
1473
+ }
1435
1474
  // Only a launched pane sources-and-unlinks the env file. If createSession
1436
1475
  // throws, the pane never runs, so the resolved secrets (incl. the master
1437
1476
  // passphrase) would linger on disk — remove it on that failure path
@@ -1616,19 +1655,36 @@ async function spawnAgent(options) {
1616
1655
  command: executable,
1617
1656
  args: redactArgs(args.slice(0, 10)),
1618
1657
  });
1619
- // Interactive spawn-wrap: when this device opts in on macOS/Linux, run the
1620
- // agent INSIDE a shared-socket tmux session (then attach this TTY) so it gets
1621
- // a unique, addressable %pane. Every failed guard keeps the bare spawn below.
1622
- const wrapInTmux = shouldWrapInTmux({
1658
+ // Interactive spawn-wrap: run the agent INSIDE a shared-socket tmux session
1659
+ // (then attach this TTY) so it gets a unique, addressable %pane — and, when
1660
+ // the run arrived over `--device`, so it survives the SSH link that carries
1661
+ // it. Every failed guard keeps the bare spawn below.
1662
+ const tmuxWrap = resolveTmuxWrap({
1623
1663
  interactive,
1624
1664
  platform: process.platform,
1625
1665
  inTmux: !!process.env.TMUX,
1626
1666
  raw: options.raw === true,
1627
1667
  noTmuxEnv: process.env.AGENTS_NO_TMUX === '1',
1628
1668
  configEnabled: isTmuxEnabled(),
1669
+ remoteDispatch: process.env[REMOTE_INTERACTIVE_ENV] === '1',
1629
1670
  tmuxAvailable: isTmuxInstalled(),
1630
1671
  });
1631
- if (wrapInTmux) {
1672
+ if (tmuxWrap.kind === 'undurable') {
1673
+ // Refuse rather than start work a blink would destroy. This is the ONLY
1674
+ // check — `ensureHostReady` gates a `--device` dispatch on the peer being
1675
+ // reachable and able to run the pinned agent, not on it having tmux, so
1676
+ // there is no launcher-side probe ahead of this one. Failing here still
1677
+ // fails before the agent starts, which is what matters; a pre-dispatch
1678
+ // probe would only move the message earlier, at the cost of a round trip on
1679
+ // every launch.
1680
+ const msg = `agents: ${machineId()} has no tmux, so a --device run here could not survive a dropped connection.\n`
1681
+ + ` install it: (apt|dnf|brew) install tmux\n`
1682
+ + ` or accept the risk: agents run … --device ${machineId()} --raw\n`;
1683
+ process.stderr.write(`\x1b[31m${msg}\x1b[0m`);
1684
+ timer.end({ exitCode: 1, status: 'failed', error: 'remote interactive run has no tmux for durability' });
1685
+ return { exitCode: 1, stdout: '', stderr: msg };
1686
+ }
1687
+ if (tmuxWrap.kind === 'wrap') {
1632
1688
  timer.mark('startup');
1633
1689
  try {
1634
1690
  const result = await runInTmux(options, executable, args);
@@ -1,19 +1,16 @@
1
- import * as path from 'path';
2
1
  import { stripForeignConfigDir } from '../adapter.js';
3
2
  export const cursorAdapter = {
4
3
  id: 'cursor',
5
- // Cursor has no config-dir env var (only CURSOR_API_KEY / CURSOR_API_ENDPOINT).
6
- // Its OAuth token the login gate lives at $XDG_CONFIG_HOME/cursor/auth.json
7
- // (verified empirically: relocating XDG_CONFIG_HOME relocates the login;
8
- // ~/.cursor/cli-config.json holds only account metadata, not the token). Pin
9
- // XDG_CONFIG_HOME into the version home so each installed Cursor account
10
- // authenticates from its own token, isolated per run — no global ~/.cursor
11
- // symlink swap, so concurrent runs on different accounts never clobber one
12
- // another. cli-config.json (HOME-relative) has no override and stays on the
13
- // shared home; only the token is per-account, which is what gates the login.
4
+ // Cursor defaults to one machine-global OS-keychain login on macOS, which
5
+ // ignores XDG_CONFIG_HOME. Select the file credential store; Cursor writes
6
+ // that store to HOME-relative ~/.cursor/auth.json, and buildExecEnv already
7
+ // swaps HOME to the selected version home. Existing keychain credentials
8
+ // remain untouched.
14
9
  applyExecConfigEnv(result, ctx) {
15
10
  if (ctx.versionHome) {
16
- result.XDG_CONFIG_HOME = path.join(ctx.versionHome, '.config');
11
+ result.AGENTS_REAL_HOME ||= result.HOME;
12
+ result.HOME = ctx.versionHome;
13
+ result.AGENT_CLI_CREDENTIAL_STORE = 'file';
17
14
  }
18
15
  stripForeignConfigDir(result);
19
16
  },
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Shared on-demand download + verification for the macOS native helper `.app`
3
+ * bundles that ship as signed + notarized GitHub release assets on each helper's
4
+ * OWN tag (`ComputerHelper.app`, `MenubarHelper.app`, `Agents CLI.app`) — see
5
+ * `helper-versions.ts` for why they are no longer keyed to the CLI's tag.
6
+ *
7
+ * A `.app` is a directory, so each asset is a zip (`ditto -c -k --keepParent`);
8
+ * a fresh `npm i -g` machine with no bundled copy fetches the asset for the
9
+ * pinned helper version, verifies its sha256 against the published `.sha256`,
10
+ * then verifies the code signature (Developer ID Team + notarization — and, for
11
+ * the menu-bar helper, its designated requirement) before it is ever installed.
12
+ *
13
+ * `computer/download.ts` (ComputerHelper) and `menubar/download-menubar.ts`
14
+ * (MenubarHelper) are thin per-helper wrappers over the primitives here — one
15
+ * download+verify machinery, two specs — so a fix to the verify/download logic
16
+ * lands for both helpers at once.
17
+ */
18
+ import { type HelperName } from './helper-versions.js';
19
+ /** GitHub repo whose `v<version>` releases carry the helper assets. */
20
+ export declare const HELPER_RELEASE_REPO = "phnx-labs/agents-cli";
21
+ /** Apple Developer ID Team every helper must be signed by ("Developer ID
22
+ * Application: Muqit Nawaz"). Defense in depth on top of `spctl` notarization. */
23
+ export declare const EXPECTED_TEAM_ID = "2HTP252L87";
24
+ /** One helper's identity + verification policy — everything that differs between
25
+ * the computer helper and the menu-bar helper. */
26
+ export interface HelperSpec {
27
+ /**
28
+ * Which helper this is — the key into the independent release train
29
+ * (`helper-versions.ts`). This is what makes the download URL a function of
30
+ * the HELPER's version rather than the CLI's.
31
+ */
32
+ helper: HelperName;
33
+ /** The zipped `.app` release asset name, e.g. `ComputerHelper.app.zip`. */
34
+ assetName: string;
35
+ /** The bundle directory name once extracted, e.g. `ComputerHelper.app`. */
36
+ appName: string;
37
+ /** Cache dir components under `getCacheDir()`, e.g. `['computer', 'mac-helper']`. */
38
+ cacheSubdir: string[];
39
+ /** Developer ID Team the bundle must be signed by. */
40
+ expectedTeamId: string;
41
+ /**
42
+ * When set, the bundle's designated requirement MUST pin this
43
+ * CFBundleIdentifier in addition to `expectedTeamId`. Used by the menu-bar
44
+ * helper: macOS keys the Accessibility (TCC) grant to this requirement, so a
45
+ * substituted bundle whose DR does not pin (id, team) would silently revoke
46
+ * the user's grant on the next paste. Left undefined for the computer helper,
47
+ * which has no such upgrade-stable grant to protect (matching its behavior
48
+ * before this shared primitive existed).
49
+ */
50
+ expectedBundleId?: string;
51
+ /** Local-build command named in the "asset missing" error, for a repo checkout. */
52
+ localBuildHint: string;
53
+ }
54
+ /** Cache dir for a downloaded helper, one subdir per release tag. */
55
+ export declare function helperCacheDir(spec: HelperSpec, version: string): string;
56
+ /**
57
+ * Release-asset URLs for one helper build, at that HELPER's own tag.
58
+ *
59
+ * The tag is `<helper>/v<version>` (e.g. `menubar/v1.0.0`), never the CLI's
60
+ * `v<cliVersion>`. Keying these to the CLI tag is what forced every CLI release
61
+ * to re-stage every helper asset, and simultaneously made a helper fix
62
+ * unreachable without a CLI release — see `helper-versions.ts` for the full why.
63
+ *
64
+ * `version` is a HELPER version. Passing a CLI version here yields a tag that
65
+ * does not exist; callers default it from {@link helperFloor}.
66
+ */
67
+ export declare function helperAssetUrls(spec: HelperSpec, version: string): {
68
+ zip: string;
69
+ sha256: string;
70
+ };
71
+ /** Extract `TeamIdentifier=XXXX` from `codesign -dv --verbose=4` output (which is
72
+ * emitted on stderr). Returns null when absent (ad-hoc / unsigned). */
73
+ export declare function parseTeamId(codesignInfo: string): string | null;
74
+ /** Read the bundle's designated requirement string (`codesign -d --requirements -`).
75
+ * codesign writes it to stdout, but capture both streams so a diagnostic on
76
+ * stderr can never make the pin-check read empty and falsely pass. */
77
+ export declare function readDesignatedRequirement(appPath: string): string;
78
+ /**
79
+ * Pure predicate (no I/O) behind the DR pin: does the requirement string pin
80
+ * BOTH the expected CFBundleIdentifier and the Developer ID Team? Returns null
81
+ * when it does, else an actionable error message naming the missing pin. Kept
82
+ * pure so the truth table is unit-testable without a signed bundle or codesign.
83
+ * Mirrors the release-time gate in `scripts/verify-menubar-helper.sh` (identifier
84
+ * + team substring check against the same requirement string).
85
+ */
86
+ export declare function checkDesignatedRequirement(req: string, bundleId: string, teamId: string): string | null;
87
+ /**
88
+ * Verify the bundle's designated requirement pins the expected CFBundleIdentifier
89
+ * AND Developer ID Team. macOS re-validates each new version against this stored
90
+ * requirement to keep an Accessibility grant alive across upgrades; a downloaded
91
+ * bundle whose DR drops either pin would silently revoke that grant on the next
92
+ * paste, so refuse it.
93
+ */
94
+ export declare function verifyDesignatedRequirement(appPath: string, bundleId: string, teamId: string): void;
95
+ /**
96
+ * Verify a helper `.app` bundle is intact, signed by the expected Developer ID
97
+ * Team, (for the menu-bar helper) pinned by its designated requirement, and
98
+ * notarized (Gatekeeper-accepted). Throws with an actionable message on any
99
+ * failure — a downloaded bundle is never trusted without this.
100
+ */
101
+ export declare function verifyHelperApp(appPath: string, spec: HelperSpec): void;
102
+ /**
103
+ * Download a helper release asset for `version`, verify sha256, extract the
104
+ * `.app`, and verify its signature (+ DR pin when the spec requires one).
105
+ * Returns the path to the extracted bundle. A missing asset is a hard error
106
+ * naming the exact tag — never a silent fallback to another release.
107
+ */
108
+ export declare function downloadHelperApp(spec: HelperSpec, version: string): Promise<string>;
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Shared on-demand download + verification for the macOS native helper `.app`
3
+ * bundles that ship as signed + notarized GitHub release assets on each helper's
4
+ * OWN tag (`ComputerHelper.app`, `MenubarHelper.app`, `Agents CLI.app`) — see
5
+ * `helper-versions.ts` for why they are no longer keyed to the CLI's tag.
6
+ *
7
+ * A `.app` is a directory, so each asset is a zip (`ditto -c -k --keepParent`);
8
+ * a fresh `npm i -g` machine with no bundled copy fetches the asset for the
9
+ * pinned helper version, verifies its sha256 against the published `.sha256`,
10
+ * then verifies the code signature (Developer ID Team + notarization — and, for
11
+ * the menu-bar helper, its designated requirement) before it is ever installed.
12
+ *
13
+ * `computer/download.ts` (ComputerHelper) and `menubar/download-menubar.ts`
14
+ * (MenubarHelper) are thin per-helper wrappers over the primitives here — one
15
+ * download+verify machinery, two specs — so a fix to the verify/download logic
16
+ * lands for both helpers at once.
17
+ */
18
+ import { execFileSync, spawnSync } from 'node:child_process';
19
+ import * as fs from 'node:fs';
20
+ import * as path from 'node:path';
21
+ import { Readable } from 'node:stream';
22
+ import { pipeline } from 'node:stream/promises';
23
+ import { getCacheDir } from './state.js';
24
+ import { parseSha256Asset, sha256File } from './sha256-asset.js';
25
+ import { helperTag } from './helper-versions.js';
26
+ /** GitHub repo whose `v<version>` releases carry the helper assets. */
27
+ export const HELPER_RELEASE_REPO = 'phnx-labs/agents-cli';
28
+ /** Apple Developer ID Team every helper must be signed by ("Developer ID
29
+ * Application: Muqit Nawaz"). Defense in depth on top of `spctl` notarization. */
30
+ export const EXPECTED_TEAM_ID = '2HTP252L87';
31
+ /** Cache dir for a downloaded helper, one subdir per release tag. */
32
+ export function helperCacheDir(spec, version) {
33
+ return path.join(getCacheDir(), ...spec.cacheSubdir, `v${version}`);
34
+ }
35
+ /**
36
+ * Release-asset URLs for one helper build, at that HELPER's own tag.
37
+ *
38
+ * The tag is `<helper>/v<version>` (e.g. `menubar/v1.0.0`), never the CLI's
39
+ * `v<cliVersion>`. Keying these to the CLI tag is what forced every CLI release
40
+ * to re-stage every helper asset, and simultaneously made a helper fix
41
+ * unreachable without a CLI release — see `helper-versions.ts` for the full why.
42
+ *
43
+ * `version` is a HELPER version. Passing a CLI version here yields a tag that
44
+ * does not exist; callers default it from {@link helperFloor}.
45
+ */
46
+ export function helperAssetUrls(spec, version) {
47
+ const base = `https://github.com/${HELPER_RELEASE_REPO}/releases/download/${helperTag(spec.helper, version)}`;
48
+ // Asset names must not contain spaces: GitHub rewrites a space to a dot on
49
+ // upload, so a spaced name is requested at a URL that 404s forever. Caught on
50
+ // `Agents CLI.app.zip`, which GitHub served as `Agents.CLI.app.zip`. Use an
51
+ // underscore -- GitHub preserves it verbatim.
52
+ if (spec.assetName.includes(' ')) {
53
+ throw new Error(`helper asset name ${JSON.stringify(spec.assetName)} contains a space; GitHub rewrites it to a dot on `
54
+ + 'upload, so this URL would always 404. Use an underscore instead.');
55
+ }
56
+ return { zip: `${base}/${spec.assetName}`, sha256: `${base}/${spec.assetName}.sha256` };
57
+ }
58
+ /** Extract `TeamIdentifier=XXXX` from `codesign -dv --verbose=4` output (which is
59
+ * emitted on stderr). Returns null when absent (ad-hoc / unsigned). */
60
+ export function parseTeamId(codesignInfo) {
61
+ return codesignInfo.match(/TeamIdentifier=([A-Z0-9]+)/)?.[1] ?? null;
62
+ }
63
+ /** Read the bundle's designated requirement string (`codesign -d --requirements -`).
64
+ * codesign writes it to stdout, but capture both streams so a diagnostic on
65
+ * stderr can never make the pin-check read empty and falsely pass. */
66
+ export function readDesignatedRequirement(appPath) {
67
+ const r = spawnSync('/usr/bin/codesign', ['-d', '--requirements', '-', appPath], { encoding: 'utf8' });
68
+ return `${r.stdout ?? ''}${r.stderr ?? ''}`;
69
+ }
70
+ /**
71
+ * Pure predicate (no I/O) behind the DR pin: does the requirement string pin
72
+ * BOTH the expected CFBundleIdentifier and the Developer ID Team? Returns null
73
+ * when it does, else an actionable error message naming the missing pin. Kept
74
+ * pure so the truth table is unit-testable without a signed bundle or codesign.
75
+ * Mirrors the release-time gate in `scripts/verify-menubar-helper.sh` (identifier
76
+ * + team substring check against the same requirement string).
77
+ */
78
+ export function checkDesignatedRequirement(req, bundleId, teamId) {
79
+ const trimmed = req.trim();
80
+ const shown = trimmed ? JSON.stringify(trimmed.slice(0, 200)) : '<none>';
81
+ if (!trimmed.includes(`identifier "${bundleId}"`)) {
82
+ return (`helper designated requirement does not pin bundle id "${bundleId}" (read: ${shown}). ` +
83
+ `Installing it would revoke the Accessibility grant. Refusing to install.`);
84
+ }
85
+ if (!trimmed.includes(teamId)) {
86
+ return (`helper designated requirement does not pin Developer ID Team ${teamId} (read: ${shown}). ` +
87
+ `Installing it would revoke the Accessibility grant. Refusing to install.`);
88
+ }
89
+ return null;
90
+ }
91
+ /**
92
+ * Verify the bundle's designated requirement pins the expected CFBundleIdentifier
93
+ * AND Developer ID Team. macOS re-validates each new version against this stored
94
+ * requirement to keep an Accessibility grant alive across upgrades; a downloaded
95
+ * bundle whose DR drops either pin would silently revoke that grant on the next
96
+ * paste, so refuse it.
97
+ */
98
+ export function verifyDesignatedRequirement(appPath, bundleId, teamId) {
99
+ const err = checkDesignatedRequirement(readDesignatedRequirement(appPath), bundleId, teamId);
100
+ if (err)
101
+ throw new Error(err);
102
+ }
103
+ /**
104
+ * Verify a helper `.app` bundle is intact, signed by the expected Developer ID
105
+ * Team, (for the menu-bar helper) pinned by its designated requirement, and
106
+ * notarized (Gatekeeper-accepted). Throws with an actionable message on any
107
+ * failure — a downloaded bundle is never trusted without this.
108
+ */
109
+ export function verifyHelperApp(appPath, spec) {
110
+ // 1. Structural + signature integrity.
111
+ try {
112
+ execFileSync('/usr/bin/codesign', ['--verify', '--deep', '--strict', appPath], { stdio: 'pipe' });
113
+ }
114
+ catch (e) {
115
+ throw new Error(`code signature invalid for ${appPath}: ${e.message}`);
116
+ }
117
+ // 2. Team identity — must be our Developer ID, not some other valid signer.
118
+ // `codesign -dv` writes its details to STDERR even on success (exit 0), so we
119
+ // must read stderr, not stdout. spawnSync captures both streams regardless of
120
+ // exit code; execFileSync would return only (empty) stdout and the Team check
121
+ // would falsely reject every validly-signed helper.
122
+ const dv = spawnSync('/usr/bin/codesign', ['-dv', '--verbose=4', appPath], { encoding: 'utf8' });
123
+ const info = `${dv.stdout ?? ''}${dv.stderr ?? ''}`;
124
+ const team = parseTeamId(info);
125
+ if (team !== spec.expectedTeamId) {
126
+ throw new Error(`helper signed by unexpected Team (${team ?? 'none'}), expected ${spec.expectedTeamId}. Refusing to install.`);
127
+ }
128
+ // 3. Designated-requirement pin (menu-bar helper only) — before the Gatekeeper
129
+ // assessment so a DR mismatch fails with its specific, actionable message
130
+ // rather than a generic Gatekeeper rejection.
131
+ if (spec.expectedBundleId) {
132
+ verifyDesignatedRequirement(appPath, spec.expectedBundleId, spec.expectedTeamId);
133
+ }
134
+ // 4. Notarization / Gatekeeper — confirms Apple stapled a notarization ticket.
135
+ try {
136
+ execFileSync('/usr/sbin/spctl', ['--assess', '--type', 'execute', '--verbose', appPath], { stdio: 'pipe' });
137
+ }
138
+ catch (e) {
139
+ throw new Error(`helper is not notarized / rejected by Gatekeeper: ${e.message}. Refusing to install.`);
140
+ }
141
+ }
142
+ /**
143
+ * Download a helper release asset for `version`, verify sha256, extract the
144
+ * `.app`, and verify its signature (+ DR pin when the spec requires one).
145
+ * Returns the path to the extracted bundle. A missing asset is a hard error
146
+ * naming the exact tag — never a silent fallback to another release.
147
+ */
148
+ export async function downloadHelperApp(spec, version) {
149
+ const dir = helperCacheDir(spec, version);
150
+ const cachedApp = path.join(dir, spec.appName);
151
+ if (fs.existsSync(cachedApp)) {
152
+ // Re-verify a cached bundle cheaply; a tampered cache must not be trusted.
153
+ verifyHelperApp(cachedApp, spec);
154
+ return cachedApp;
155
+ }
156
+ // Name the HELPER's tag, not `v${version}`: after the per-helper split that
157
+ // string was a tag that does not exist, so a 404 pointed the reader at the
158
+ // wrong place to look.
159
+ const tag = helperTag(spec.helper, version);
160
+ const { zip: zipUrl, sha256: shaUrl } = helperAssetUrls(spec, version);
161
+ const missing = (status, url) => new Error(`no ${spec.assetName} release asset for tag ${tag} (HTTP ${status} on ${url}). ` +
162
+ `The macOS helper ships as a GitHub release asset on its own helper tag; ` +
163
+ `from a repo checkout you can build it locally instead: ${spec.localBuildHint}`);
164
+ // Checksum first: it is tiny and 404s fast when the tag has no assets.
165
+ const shaRes = await fetch(shaUrl, { signal: AbortSignal.timeout(30_000) });
166
+ if (!shaRes.ok)
167
+ throw missing(shaRes.status, shaUrl);
168
+ const expected = parseSha256Asset(await shaRes.text());
169
+ console.error(`Downloading ${spec.assetName} ${tag} from GitHub releases...`);
170
+ const zipRes = await fetch(zipUrl, { signal: AbortSignal.timeout(15 * 60_000) });
171
+ if (!zipRes.ok || !zipRes.body)
172
+ throw missing(zipRes.status, zipUrl);
173
+ fs.mkdirSync(dir, { recursive: true });
174
+ const partial = path.join(dir, `${spec.assetName}.download`);
175
+ try {
176
+ await pipeline(Readable.fromWeb(zipRes.body), fs.createWriteStream(partial));
177
+ const actual = await sha256File(partial);
178
+ if (actual !== expected) {
179
+ throw new Error(`sha256 mismatch for ${zipUrl}: expected ${expected}, got ${actual}`);
180
+ }
181
+ // Extract the zip (created with `ditto -c -k --keepParent`, so it contains
182
+ // <appName>/ at top level) into the version cache dir.
183
+ fs.rmSync(cachedApp, { recursive: true, force: true });
184
+ execFileSync('/usr/bin/ditto', ['-x', '-k', partial, dir], { stdio: 'pipe' });
185
+ if (!fs.existsSync(cachedApp)) {
186
+ throw new Error(`extracted asset did not contain ${spec.appName}`);
187
+ }
188
+ verifyHelperApp(cachedApp, spec);
189
+ }
190
+ finally {
191
+ fs.rmSync(partial, { force: true });
192
+ }
193
+ return cachedApp;
194
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Which build of each native helper this CLI expects, and where its release
3
+ * lives — the module that decouples helper distribution from CLI releases.
4
+ *
5
+ * WHY THIS EXISTS. `helperAssetUrls` used to build
6
+ * `releases/download/v${cliVersion}/<asset>`, keying every helper to the CLI's
7
+ * own tag. That is coupled in BOTH directions and each one hurts:
8
+ *
9
+ * - A CLI release had to re-stage every helper asset onto its new tag, or the
10
+ * download 404'd. That is what chained an ordinary release to a Mac.
11
+ * - A helper fix could not reach anyone without cutting a CLI release, because
12
+ * no URL existed that a shipped CLI would look at.
13
+ *
14
+ * Helpers now publish to their OWN tags (`<helper>/v<x.y.z>`) and this table
15
+ * records the FLOOR — the build this CLI was tested against. A floor, not an
16
+ * exact pin: a pin would fix the first problem and leave the second, since an
17
+ * improved helper would still need a CLI release to be reachable. The floor is
18
+ * what a channel manifest can later resolve *upward* from (newest >= floor),
19
+ * without another rewrite of the download path.
20
+ *
21
+ * The floor is also the offline answer: it names an IMMUTABLE tag that always
22
+ * exists, so a machine that cannot reach a manifest still has exactly one
23
+ * correct URL rather than a guess.
24
+ *
25
+ * Bumping an entry here is a deliberate act — it says "this CLI has been tested
26
+ * against that helper build". It is not a mirror of the CLI version and must
27
+ * never be derived from `getCliVersion()`.
28
+ */
29
+ /** The helpers that have their own release train. */
30
+ export type HelperName = 'menubar' | 'keychain' | 'computer-mac' | 'computer-win';
31
+ /** One helper's release identity. */
32
+ export interface HelperRelease {
33
+ /** Tag prefix — the release is `<tagPrefix>/v<version>`. */
34
+ tagPrefix: string;
35
+ /**
36
+ * Lowest helper build this CLI is known to work with. Resolution may pick a
37
+ * NEWER build; it must never pick an older one.
38
+ */
39
+ floor: string;
40
+ }
41
+ /**
42
+ * Floors, by helper.
43
+ *
44
+ * `menubar` and `keychain` start at 1.0.0 — the first builds published under
45
+ * their own tags rather than the CLI's. They are not "version 1 of the helper";
46
+ * they are version 1 of its independent release train.
47
+ */
48
+ export declare const HELPER_RELEASES: Readonly<Record<HelperName, HelperRelease>>;
49
+ /** The release tag for one helper at one version, e.g. `menubar/v1.0.0`. */
50
+ export declare function helperTag(helper: HelperName, version: string): string;
51
+ /** The floor build for one helper — the version used when nothing resolves higher. */
52
+ export declare function helperFloor(helper: HelperName): string;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Which build of each native helper this CLI expects, and where its release
3
+ * lives — the module that decouples helper distribution from CLI releases.
4
+ *
5
+ * WHY THIS EXISTS. `helperAssetUrls` used to build
6
+ * `releases/download/v${cliVersion}/<asset>`, keying every helper to the CLI's
7
+ * own tag. That is coupled in BOTH directions and each one hurts:
8
+ *
9
+ * - A CLI release had to re-stage every helper asset onto its new tag, or the
10
+ * download 404'd. That is what chained an ordinary release to a Mac.
11
+ * - A helper fix could not reach anyone without cutting a CLI release, because
12
+ * no URL existed that a shipped CLI would look at.
13
+ *
14
+ * Helpers now publish to their OWN tags (`<helper>/v<x.y.z>`) and this table
15
+ * records the FLOOR — the build this CLI was tested against. A floor, not an
16
+ * exact pin: a pin would fix the first problem and leave the second, since an
17
+ * improved helper would still need a CLI release to be reachable. The floor is
18
+ * what a channel manifest can later resolve *upward* from (newest >= floor),
19
+ * without another rewrite of the download path.
20
+ *
21
+ * The floor is also the offline answer: it names an IMMUTABLE tag that always
22
+ * exists, so a machine that cannot reach a manifest still has exactly one
23
+ * correct URL rather than a guess.
24
+ *
25
+ * Bumping an entry here is a deliberate act — it says "this CLI has been tested
26
+ * against that helper build". It is not a mirror of the CLI version and must
27
+ * never be derived from `getCliVersion()`.
28
+ */
29
+ /**
30
+ * Floors, by helper.
31
+ *
32
+ * `menubar` and `keychain` start at 1.0.0 — the first builds published under
33
+ * their own tags rather than the CLI's. They are not "version 1 of the helper";
34
+ * they are version 1 of its independent release train.
35
+ */
36
+ export const HELPER_RELEASES = {
37
+ menubar: { tagPrefix: 'menubar', floor: '1.0.0' },
38
+ keychain: { tagPrefix: 'keychain', floor: '1.0.0' },
39
+ 'computer-mac': { tagPrefix: 'computer-mac', floor: '1.0.0' },
40
+ // The Windows helper is a bare .exe, not an .app bundle, so it does not share
41
+ // helper-download.ts's zip/codesign/notarize machinery -- but it has the same
42
+ // URL problem, so it shares the tag namespace and the floor.
43
+ 'computer-win': { tagPrefix: 'computer-win', floor: '1.0.0' },
44
+ };
45
+ /** The release tag for one helper at one version, e.g. `menubar/v1.0.0`. */
46
+ export function helperTag(helper, version) {
47
+ const release = HELPER_RELEASES[helper];
48
+ if (!release)
49
+ throw new Error(`unknown helper '${helper}' (want: ${Object.keys(HELPER_RELEASES).join(', ')})`);
50
+ return `${release.tagPrefix}/v${version}`;
51
+ }
52
+ /** The floor build for one helper — the version used when nothing resolves higher. */
53
+ export function helperFloor(helper) {
54
+ const release = HELPER_RELEASES[helper];
55
+ if (!release)
56
+ throw new Error(`unknown helper '${helper}' (want: ${Object.keys(HELPER_RELEASES).join(', ')})`);
57
+ return release.floor;
58
+ }
@@ -283,6 +283,17 @@ def should_fire():
283
283
  if pm and pm not in allowed:
284
284
  return False
285
285
 
286
+ v = m.get("permission_mode_not")
287
+ if v is not None:
288
+ denied = arr(v)
289
+ if denied:
290
+ # Negative form: naming the mode to SKIP keeps every unknown mode
291
+ # firing, so the failure direction is "ran unnecessarily", never
292
+ # "did not run". Absence of a mode fires, same as permission_mode.
293
+ pm = inp.get("permission_mode") or inp.get("permissionMode")
294
+ if pm and pm in denied:
295
+ return False
296
+
286
297
  v = m.get("tool_args_match")
287
298
  if v is not None:
288
299
  ta = inp.get("tool_args")
@@ -3255,8 +3255,8 @@ function resolveSessionTrackerRoot() {
3255
3255
  if (fs.existsSync(path.join(built, 'dist', 'hook.sh'))) {
3256
3256
  return built;
3257
3257
  }
3258
- // Source layout: apps/cli/src/lib/hooks/ -> repo root -> packages/session-tracker
3259
- const source = path.resolve(here, '..', '..', '..', '..', '..', 'packages', 'session-tracker');
3258
+ // Source layout: cli/src/lib/hooks/ -> repo root (4 up) -> packages/session-tracker
3259
+ const source = path.resolve(here, '..', '..', '..', '..', 'packages', 'session-tracker');
3260
3260
  if (fs.existsSync(path.join(source, 'src', 'hook.sh'))) {
3261
3261
  return source;
3262
3262
  }
@@ -6,7 +6,7 @@
6
6
  * codex/grok/gemini `auth.json`-style files — so the box booted logged-in. Every
7
7
  * one of those is a **rotating, interactive OAuth / session credential**, and
8
8
  * copying it between devices is forbidden by the fleet-auth contract
9
- * (`docs/specifications.md` SING-1b, `docs/credential-management.md` invariant 2):
9
+ * (`docs/specifications.md` SING-1b, `docs/secrets.md`):
10
10
  * a shared refresh token rotates server-side on the next refresh and invalidates
11
11
  * every other copy — the "droid collapsed 10 boxes to 1 overnight" failure.
12
12
  *