@phnx-labs/agents-cli 1.22.42 → 1.22.44

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 (256) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +15 -13
  3. package/dist/bootstrap.js +33 -13
  4. package/dist/cli/command-registry.d.ts +0 -8
  5. package/dist/cli/command-registry.js +1 -25
  6. package/dist/commands/alias.d.ts +7 -4
  7. package/dist/commands/alias.js +33 -8
  8. package/dist/commands/apply.d.ts +7 -10
  9. package/dist/commands/apply.js +15 -35
  10. package/dist/commands/artifacts.js +4 -5
  11. package/dist/commands/audit.d.ts +6 -6
  12. package/dist/commands/audit.js +12 -12
  13. package/dist/commands/auth.js +14 -6
  14. package/dist/commands/beta.d.ts +7 -1
  15. package/dist/commands/beta.js +16 -9
  16. package/dist/commands/commands.js +1 -1
  17. package/dist/commands/computer.js +20 -6
  18. package/dist/commands/daemon.js +1 -1
  19. package/dist/commands/doctor.d.ts +4 -2
  20. package/dist/commands/doctor.js +21 -9
  21. package/dist/commands/events.d.ts +2 -2
  22. package/dist/commands/events.js +6 -5
  23. package/dist/commands/exec.d.ts +1 -1
  24. package/dist/commands/exec.js +11 -6
  25. package/dist/commands/factory.d.ts +1 -1
  26. package/dist/commands/feed.js +1 -30
  27. package/dist/commands/fleet-capture.d.ts +1 -1
  28. package/dist/commands/fleet-capture.js +4 -5
  29. package/dist/commands/harness.js +4 -52
  30. package/dist/commands/hooks.js +1 -1
  31. package/dist/commands/insights.d.ts +1 -1
  32. package/dist/commands/insights.js +3 -2
  33. package/dist/commands/menubar.js +58 -1
  34. package/dist/commands/monitors.js +1 -1
  35. package/dist/commands/org.d.ts +5 -0
  36. package/dist/commands/org.js +83 -29
  37. package/dist/commands/output.js +2 -2
  38. package/dist/commands/profiles.js +2 -2
  39. package/dist/commands/prune.js +5 -4
  40. package/dist/commands/routines.js +43 -25
  41. package/dist/commands/routines.test-fixture.d.ts +86 -0
  42. package/dist/commands/routines.test-fixture.js +345 -0
  43. package/dist/commands/sessions-picker.js +124 -122
  44. package/dist/commands/sessions.test-fixture.d.ts +24 -0
  45. package/dist/commands/sessions.test-fixture.js +224 -0
  46. package/dist/commands/setup.js +9 -1
  47. package/dist/commands/share.d.ts +9 -10
  48. package/dist/commands/share.js +68 -50
  49. package/dist/commands/skills.js +1 -1
  50. package/dist/commands/snapshot.d.ts +1 -1
  51. package/dist/commands/snapshot.js +4 -4
  52. package/dist/commands/ssh.js +15 -62
  53. package/dist/commands/status.d.ts +4 -2
  54. package/dist/commands/status.js +10 -7
  55. package/dist/commands/subagents.js +1 -1
  56. package/dist/commands/sync.d.ts +1 -0
  57. package/dist/commands/sync.js +29 -2
  58. package/dist/commands/view.js +11 -2
  59. package/dist/commands/webhook.js +1 -1
  60. package/dist/lib/accounting/rotate.d.ts +5 -3
  61. package/dist/lib/accounting/rotate.js +36 -7
  62. package/dist/lib/accounting/usage.d.ts +18 -4
  63. package/dist/lib/accounting/usage.js +138 -27
  64. package/dist/lib/agent-spec/agents.d.ts +7 -7
  65. package/dist/lib/agent-spec/agents.js +15 -15
  66. package/dist/lib/analytics/mix-commands.d.ts +6 -28
  67. package/dist/lib/analytics/mix-commands.js +14 -40
  68. package/dist/lib/audit/log.d.ts +2 -2
  69. package/dist/lib/audit/log.js +2 -2
  70. package/dist/lib/beta.js +1 -1
  71. package/dist/lib/browser/domain-skills.d.ts +26 -6
  72. package/dist/lib/browser/domain-skills.js +81 -43
  73. package/dist/lib/cloud/rush.d.ts +0 -15
  74. package/dist/lib/cloud/rush.js +0 -35
  75. package/dist/lib/crabbox/runtimes.d.ts +6 -2
  76. package/dist/lib/crabbox/runtimes.js +21 -5
  77. package/dist/lib/daemon/daemon.js +97 -58
  78. package/dist/lib/daemon/daemon.test-fixture.d.ts +24 -0
  79. package/dist/lib/daemon/daemon.test-fixture.js +71 -0
  80. package/dist/lib/daemon/runner.d.ts +18 -1
  81. package/dist/lib/daemon/runner.js +105 -11
  82. package/dist/lib/daemon-ticks.d.ts +38 -1
  83. package/dist/lib/daemon-ticks.js +52 -5
  84. package/dist/lib/device-config.js +1 -2
  85. package/dist/lib/devices/discovery-policy.d.ts +3 -3
  86. package/dist/lib/devices/discovery-policy.js +3 -3
  87. package/dist/lib/devices/doctor-findings.js +1 -1
  88. package/dist/lib/devices/fleet.d.ts +4 -8
  89. package/dist/lib/devices/fleet.js +3 -15
  90. package/dist/lib/devices/health-report.js +4 -4
  91. package/dist/lib/devices/pool.d.ts +0 -6
  92. package/dist/lib/devices/pool.js +0 -6
  93. package/dist/lib/devices/registry.d.ts +0 -15
  94. package/dist/lib/devices/registry.js +0 -9
  95. package/dist/lib/devices/stats-cache.d.ts +12 -0
  96. package/dist/lib/devices/stats-cache.js +24 -5
  97. package/dist/lib/drift-sync.d.ts +1 -1
  98. package/dist/lib/drift-sync.js +2 -2
  99. package/dist/lib/drift.d.ts +1 -1
  100. package/dist/lib/exec.js +13 -3
  101. package/dist/lib/fleet/capture.d.ts +1 -1
  102. package/dist/lib/fleet/manifest.js +1 -1
  103. package/dist/lib/fleet/remote-login.d.ts +2 -2
  104. package/dist/lib/fleet/remote-login.js +3 -4
  105. package/dist/lib/format.d.ts +9 -0
  106. package/dist/lib/format.js +16 -0
  107. package/dist/lib/github/pr-mergeable.d.ts +49 -0
  108. package/dist/lib/github/pr-mergeable.js +157 -0
  109. package/dist/lib/github/pr-verdict.d.ts +67 -0
  110. package/dist/lib/github/pr-verdict.js +72 -0
  111. package/dist/lib/harness/adapter.d.ts +6 -5
  112. package/dist/lib/hosts/dispatch.d.ts +2 -0
  113. package/dist/lib/hosts/dispatch.js +8 -1
  114. package/dist/lib/hosts/passthrough.d.ts +10 -0
  115. package/dist/lib/hosts/passthrough.js +28 -7
  116. package/dist/lib/hosts/providers/devices.js +1 -13
  117. package/dist/lib/hosts/ready.d.ts +12 -3
  118. package/dist/lib/hosts/ready.js +27 -12
  119. package/dist/lib/hosts/registry.d.ts +4 -5
  120. package/dist/lib/hosts/registry.js +3 -7
  121. package/dist/lib/hosts/session-index.js +1 -1
  122. package/dist/lib/installations/shims.js +1 -1
  123. package/dist/lib/installations/versions.d.ts +40 -0
  124. package/dist/lib/installations/versions.js +78 -4
  125. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +1 -5
  126. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  127. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -15
  128. package/dist/lib/menubar/install-menubar.d.ts +109 -0
  129. package/dist/lib/menubar/install-menubar.js +218 -12
  130. package/dist/lib/monitors/config.d.ts +2 -2
  131. package/dist/lib/monitors/config.js +3 -2
  132. package/dist/lib/origin-machine.d.ts +18 -0
  133. package/dist/lib/origin-machine.js +34 -0
  134. package/dist/lib/perf/db.js +1 -1
  135. package/dist/lib/perf/spool.js +1 -1
  136. package/dist/lib/prix-account.d.ts +2 -1
  137. package/dist/lib/prix-account.js +2 -1
  138. package/dist/lib/profiles.d.ts +7 -0
  139. package/dist/lib/profiles.js +26 -1
  140. package/dist/lib/remote-agents-json.js +1 -7
  141. package/dist/lib/routines-placement.d.ts +1 -1
  142. package/dist/lib/routines-placement.js +1 -1
  143. package/dist/lib/sandbox.d.ts +24 -0
  144. package/dist/lib/sandbox.js +101 -0
  145. package/dist/lib/scheduling/routines.d.ts +18 -9
  146. package/dist/lib/scheduling/routines.js +16 -8
  147. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  148. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -2
  149. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  150. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -13
  151. package/dist/lib/secrets/agent.js +9 -1
  152. package/dist/lib/secrets/index.js +1 -1
  153. package/dist/lib/secrets/list-filter.js +1 -1
  154. package/dist/lib/service-manifest.d.ts +20 -0
  155. package/dist/lib/service-manifest.js +34 -0
  156. package/dist/lib/session/active.d.ts +4 -16
  157. package/dist/lib/session/active.js +2 -6
  158. package/dist/lib/session/cloud.js +1 -1
  159. package/dist/lib/session/db.d.ts +13 -98
  160. package/dist/lib/session/db.js +5 -12
  161. package/dist/lib/session/discover.d.ts +5 -81
  162. package/dist/lib/session/discover.js +5 -5
  163. package/dist/lib/session/fork.js +1 -1
  164. package/dist/lib/session/live-metadata.js +1 -1
  165. package/dist/lib/session/origin-machine.d.ts +1 -18
  166. package/dist/lib/session/origin-machine.js +4 -34
  167. package/dist/lib/session/relative-time.d.ts +1 -44
  168. package/dist/lib/session/relative-time.js +4 -120
  169. package/dist/lib/session/remote/remote-bundle.d.ts +12 -0
  170. package/dist/lib/session/remote/remote-bundle.js +61 -0
  171. package/dist/lib/session/remote/remote-list.d.ts +148 -0
  172. package/dist/lib/session/remote/remote-list.js +607 -0
  173. package/dist/lib/session/remote/remote.d.ts +132 -0
  174. package/dist/lib/session/remote/remote.js +314 -0
  175. package/dist/lib/session/remote/watch.d.ts +101 -0
  176. package/dist/lib/session/remote/watch.js +241 -0
  177. package/dist/lib/session/remote-bundle.d.ts +1 -12
  178. package/dist/lib/session/remote-bundle.js +3 -61
  179. package/dist/lib/session/remote-list.d.ts +1 -148
  180. package/dist/lib/session/remote-list.js +3 -607
  181. package/dist/lib/session/remote.d.ts +1 -132
  182. package/dist/lib/session/remote.js +3 -314
  183. package/dist/lib/session/short-id.d.ts +1 -17
  184. package/dist/lib/session/short-id.js +4 -20
  185. package/dist/lib/session/watch.d.ts +1 -101
  186. package/dist/lib/session/watch.js +3 -242
  187. package/dist/lib/session/width.d.ts +1 -29
  188. package/dist/lib/session/width.js +4 -101
  189. package/dist/lib/share/config.d.ts +12 -2
  190. package/dist/lib/share/config.js +47 -11
  191. package/dist/lib/share/delete.js +1 -1
  192. package/dist/lib/share/publish.js +1 -1
  193. package/dist/lib/share/worker-template.js +2 -2
  194. package/dist/lib/smart-launch.d.ts +4 -4
  195. package/dist/lib/smart-launch.js +8 -18
  196. package/dist/lib/snapshot.d.ts +2 -2
  197. package/dist/lib/snapshot.js +1 -1
  198. package/dist/lib/staleness/index.d.ts +3 -1
  199. package/dist/lib/staleness/index.js +21 -1
  200. package/dist/lib/staleness/types.d.ts +11 -0
  201. package/dist/lib/staleness/writers/commands.js +13 -4
  202. package/dist/lib/staleness/writers/hooks.js +3 -1
  203. package/dist/lib/staleness/writers/mcp.d.ts +0 -7
  204. package/dist/lib/staleness/writers/mcp.js +16 -1
  205. package/dist/lib/staleness/writers/rules.js +1 -1
  206. package/dist/lib/staleness/writers/skills.js +3 -1
  207. package/dist/lib/staleness/writers/subagents.d.ts +0 -9
  208. package/dist/lib/staleness/writers/subagents.js +18 -1
  209. package/dist/lib/staleness/writers/types.d.ts +10 -0
  210. package/dist/lib/startup/command-registry.d.ts +10 -1
  211. package/dist/lib/startup/command-registry.js +26 -7
  212. package/dist/lib/startup/root-command.d.ts +18 -1
  213. package/dist/lib/startup/root-command.js +18 -1
  214. package/dist/lib/state.js +7 -0
  215. package/dist/lib/teams/placement-probe.js +28 -13
  216. package/dist/lib/teams/scheduler.d.ts +3 -1
  217. package/dist/lib/teams/scheduler.js +1 -1
  218. package/dist/lib/text/relative-time.d.ts +44 -0
  219. package/dist/lib/text/relative-time.js +120 -0
  220. package/dist/lib/text/short-id.d.ts +17 -0
  221. package/dist/lib/text/short-id.js +20 -0
  222. package/dist/lib/text/width.d.ts +29 -0
  223. package/dist/lib/text/width.js +101 -0
  224. package/dist/lib/triggers/handlers.d.ts +7 -0
  225. package/dist/lib/triggers/handlers.js +8 -0
  226. package/dist/lib/triggers/webhook.d.ts +1 -0
  227. package/dist/lib/triggers/webhook.js +2 -1
  228. package/dist/lib/types.d.ts +1 -1
  229. package/dist/lib/view-types.d.ts +7 -0
  230. package/package.json +2 -3
  231. package/dist/bin/agents +0 -0
  232. package/dist/commands/serve.d.ts +0 -10
  233. package/dist/commands/serve.js +0 -68
  234. package/dist/commands/tickets.d.ts +0 -2
  235. package/dist/commands/tickets.js +0 -43
  236. package/dist/commands/trends.d.ts +0 -10
  237. package/dist/commands/trends.js +0 -12
  238. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  239. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  240. package/dist/lib/observe-aliases.d.ts +0 -30
  241. package/dist/lib/observe-aliases.js +0 -38
  242. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  243. package/dist/lib/serve/control.d.ts +0 -95
  244. package/dist/lib/serve/control.js +0 -260
  245. package/dist/lib/serve/data.d.ts +0 -81
  246. package/dist/lib/serve/data.js +0 -91
  247. package/dist/lib/serve/page.d.ts +0 -7
  248. package/dist/lib/serve/page.js +0 -140
  249. package/dist/lib/serve/server.d.ts +0 -80
  250. package/dist/lib/serve/server.js +0 -145
  251. package/dist/lib/serve/stream.d.ts +0 -43
  252. package/dist/lib/serve/stream.js +0 -116
  253. package/dist/lib/serve/token.d.ts +0 -35
  254. package/dist/lib/serve/token.js +0 -85
  255. package/dist/lib/tickets/list.d.ts +0 -53
  256. package/dist/lib/tickets/list.js +0 -153
@@ -11,7 +11,7 @@ import * as fs from 'fs';
11
11
  import * as path from 'path';
12
12
  import * as os from 'os';
13
13
  import { getDaemonDir } from '../state.js';
14
- import { isolatedHomeSuffix, namespacedServiceLabel, serviceManifestHomeEnv } from '../service-manifest.js';
14
+ import { isolatedHomeSuffix, namespacedServiceLabel, serviceManifestHomeEnv, serviceManagerRegistrationAllowed } from '../service-manifest.js';
15
15
  import { isAlive, killTree, backgroundSpawnOptions, waitForExit } from '../platform/index.js';
16
16
  import { listJobs as listAllJobs } from '../scheduling/routines.js';
17
17
  import { syncAllProjectRoutines } from '../routines-project.js';
@@ -1763,6 +1763,9 @@ export { getAgentsBinPath };
1763
1763
  * in fact up. Returns null when the service isn't running or the query fails.
1764
1764
  */
1765
1765
  function readServiceManagerPid(platform = os.platform()) {
1766
+ const reg = serviceManagerRegistrationAllowed();
1767
+ if (!reg.allowed)
1768
+ return null;
1766
1769
  try {
1767
1770
  if (platform === 'linux') {
1768
1771
  const out = execFileSync('systemctl', ['--user', 'show', '-p', 'MainPID', '--value', daemonSystemdUnitName()], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
@@ -1865,6 +1868,17 @@ export function ensureDaemonStarted() {
1865
1868
  // callers that branch on this return (e.g. secrets/agent.ts).
1866
1869
  if (isDaemonRunning())
1867
1870
  return startDaemon();
1871
+ // RUSH-3021: never LAUNCH a daemon from a redirected (sandbox/test) HOME.
1872
+ // #2860 gated service-manager registration on this signal but left the
1873
+ // detached spawn itself ungated, so a test-spawned CLI could fork a daemon
1874
+ // into the test's temp HOME; the child outlives the test and races its
1875
+ // recursive teardown rm (ENOTEMPTY). Placed after the already-running branch
1876
+ // — reporting a live daemon stays allowed, same as the circuit breaker.
1877
+ // AGENTS_SERVICE_MANAGER_ALLOW_REDIRECTED_HOME=1 is the test seam for suites
1878
+ // that exercise daemon startup deliberately; `agents daemon start` remains
1879
+ // the operator override.
1880
+ if (!serviceManagerRegistrationAllowed().allowed)
1881
+ return null;
1868
1882
  // RUSH-2418: the auto-start circuit breaker. A daemon that dies during
1869
1883
  // startup would otherwise be relaunched by EVERY foreground command that
1870
1884
  // wants one (secrets unlock, browser start, watchdog, ...) — an
@@ -1912,61 +1926,72 @@ function startDaemonLocked(agentsBin, releaseLock) {
1912
1926
  releaseLock();
1913
1927
  return startDetached({ agentsBin });
1914
1928
  };
1929
+ const reg = serviceManagerRegistrationAllowed();
1915
1930
  if (platform === 'darwin') {
1916
- try {
1917
- const plistPath = getLaunchdPlistPath();
1918
- const plistDir = path.dirname(plistPath);
1919
- if (!fs.existsSync(plistDir)) {
1920
- fs.mkdirSync(plistDir, { recursive: true });
1921
- }
1922
- // The plist carries no credential (RUSH-1759 — the daemon reads the OAuth
1923
- // token itself at startup); still create owner-only atomically to match the
1924
- // detached path and keep the log/PATH surface owner-private.
1925
- writeOwnerOnlyServiceManifest(plistPath, generateLaunchdPlist(agentsBin));
1931
+ if (reg.allowed) {
1926
1932
  try {
1927
- execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
1933
+ const plistPath = getLaunchdPlistPath();
1934
+ const plistDir = path.dirname(plistPath);
1935
+ if (!fs.existsSync(plistDir)) {
1936
+ fs.mkdirSync(plistDir, { recursive: true });
1937
+ }
1938
+ // The plist carries no credential (RUSH-1759 — the daemon reads the OAuth
1939
+ // token itself at startup); still create owner-only atomically to match the
1940
+ // detached path and keep the log/PATH surface owner-private.
1941
+ writeOwnerOnlyServiceManifest(plistPath, generateLaunchdPlist(agentsBin));
1942
+ try {
1943
+ execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
1944
+ }
1945
+ catch { /* not loaded, expected */ }
1946
+ // launchctl prints `Load failed:` and exits 0 when the label is in a
1947
+ // stuck state from a prior session — so a zero exit code isn't proof
1948
+ // of success. If no pid materializes within the window, give up on
1949
+ // launchd and fall through to a plain detached spawn.
1950
+ execFileSync('launchctl', ['load', plistPath], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
1951
+ // Launch issued — the child needs this lock to claim (RUSH-2417).
1952
+ releaseLock();
1953
+ const pid = waitForPid(3000) ?? readServiceManagerPid();
1954
+ if (pid)
1955
+ return { pid, method: 'launchd' };
1956
+ // launchctl claimed success but nothing ran. Fall through.
1957
+ }
1958
+ catch {
1959
+ // load threw — fall through to detached spawn
1928
1960
  }
1929
- catch { /* not loaded, expected */ }
1930
- // launchctl prints `Load failed:` and exits 0 when the label is in a
1931
- // stuck state from a prior session — so a zero exit code isn't proof
1932
- // of success. If no pid materializes within the window, give up on
1933
- // launchd and fall through to a plain detached spawn.
1934
- execFileSync('launchctl', ['load', plistPath], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
1935
- // Launch issued — the child needs this lock to claim (RUSH-2417).
1936
- releaseLock();
1937
- const pid = waitForPid(3000) ?? readServiceManagerPid();
1938
- if (pid)
1939
- return { pid, method: 'launchd' };
1940
- // launchctl claimed success but nothing ran. Fall through.
1941
1961
  }
1942
- catch {
1943
- // load threw — fall through to detached spawn
1962
+ else {
1963
+ process.stderr.write(`[agents] ${reg.reason}\n`);
1944
1964
  }
1945
1965
  return detachedFallback();
1946
1966
  }
1947
1967
  if (platform === 'linux') {
1948
- try {
1949
- const unitPath = getSystemdUnitPath();
1950
- const unitDir = path.dirname(unitPath);
1951
- if (!fs.existsSync(unitDir)) {
1952
- fs.mkdirSync(unitDir, { recursive: true });
1968
+ if (reg.allowed) {
1969
+ try {
1970
+ const unitPath = getSystemdUnitPath();
1971
+ const unitDir = path.dirname(unitPath);
1972
+ if (!fs.existsSync(unitDir)) {
1973
+ fs.mkdirSync(unitDir, { recursive: true });
1974
+ }
1975
+ // Carries no credential (RUSH-1759 — the daemon reads the OAuth token
1976
+ // itself at startup); owner-only to keep the PATH/log surface private.
1977
+ writeOwnerOnlyServiceManifest(unitPath, generateSystemdUnit(agentsBin));
1978
+ execFileSync('systemctl', ['--user', 'daemon-reload'], { encoding: 'utf-8' });
1979
+ execFileSync('systemctl', ['--user', 'enable', daemonSystemdUnitName()], { encoding: 'utf-8' });
1980
+ execFileSync('systemctl', ['--user', 'start', daemonSystemdUnitName()], { encoding: 'utf-8' });
1981
+ // Launch issued — the child needs this lock to claim (RUSH-2417).
1982
+ releaseLock();
1983
+ const pid = waitForPid(3000) ?? readServiceManagerPid();
1984
+ if (pid)
1985
+ return { pid, method: 'systemd' };
1986
+ // systemctl returned success but no PID surfaced — fall through to a
1987
+ // plain detached spawn rather than reporting a null PID.
1988
+ }
1989
+ catch {
1990
+ // start threw — fall through to detached spawn
1953
1991
  }
1954
- // Carries no credential (RUSH-1759 — the daemon reads the OAuth token
1955
- // itself at startup); owner-only to keep the PATH/log surface private.
1956
- writeOwnerOnlyServiceManifest(unitPath, generateSystemdUnit(agentsBin));
1957
- execFileSync('systemctl', ['--user', 'daemon-reload'], { encoding: 'utf-8' });
1958
- execFileSync('systemctl', ['--user', 'enable', daemonSystemdUnitName()], { encoding: 'utf-8' });
1959
- execFileSync('systemctl', ['--user', 'start', daemonSystemdUnitName()], { encoding: 'utf-8' });
1960
- // Launch issued — the child needs this lock to claim (RUSH-2417).
1961
- releaseLock();
1962
- const pid = waitForPid(3000) ?? readServiceManagerPid();
1963
- if (pid)
1964
- return { pid, method: 'systemd' };
1965
- // systemctl returned success but no PID surfaced — fall through to a
1966
- // plain detached spawn rather than reporting a null PID.
1967
1992
  }
1968
- catch {
1969
- // start threw — fall through to detached spawn
1993
+ else {
1994
+ process.stderr.write(`[agents] ${reg.reason}\n`);
1970
1995
  }
1971
1996
  return detachedFallback();
1972
1997
  }
@@ -2261,29 +2286,43 @@ export function stopDaemon() {
2261
2286
  const released = [];
2262
2287
  const surviving = [];
2263
2288
  let escalated = false;
2289
+ const reg = serviceManagerRegistrationAllowed();
2264
2290
  if (platform === 'darwin') {
2265
2291
  const plistPath = getLaunchdPlistPath();
2266
2292
  if (fs.existsSync(plistPath)) {
2293
+ if (reg.allowed) {
2294
+ try {
2295
+ execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8' });
2296
+ }
2297
+ catch (err) {
2298
+ if (process.env.AGENTS_DEBUG) {
2299
+ console.error(`[debug] launchctl unload failed: ${err.message}`);
2300
+ }
2301
+ }
2302
+ }
2303
+ else {
2304
+ process.stderr.write(`[agents] ${reg.reason}\n`);
2305
+ }
2267
2306
  try {
2268
- execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8' });
2269
2307
  fs.unlinkSync(plistPath);
2270
2308
  }
2309
+ catch { /* plist already removed */ }
2310
+ }
2311
+ }
2312
+ if (platform === 'linux') {
2313
+ if (reg.allowed) {
2314
+ try {
2315
+ execFileSync('systemctl', ['--user', 'stop', daemonSystemdUnitName()], { encoding: 'utf-8' });
2316
+ execFileSync('systemctl', ['--user', 'disable', daemonSystemdUnitName()], { encoding: 'utf-8' });
2317
+ }
2271
2318
  catch (err) {
2272
2319
  if (process.env.AGENTS_DEBUG) {
2273
- console.error(`[debug] launchctl unload failed: ${err.message}`);
2320
+ console.error(`[debug] systemctl stop failed: ${err.message}`);
2274
2321
  }
2275
2322
  }
2276
2323
  }
2277
- }
2278
- if (platform === 'linux') {
2279
- try {
2280
- execFileSync('systemctl', ['--user', 'stop', daemonSystemdUnitName()], { encoding: 'utf-8' });
2281
- execFileSync('systemctl', ['--user', 'disable', daemonSystemdUnitName()], { encoding: 'utf-8' });
2282
- }
2283
- catch (err) {
2284
- if (process.env.AGENTS_DEBUG) {
2285
- console.error(`[debug] systemctl stop failed: ${err.message}`);
2286
- }
2324
+ else {
2325
+ process.stderr.write(`[agents] ${reg.reason}\n`);
2287
2326
  }
2288
2327
  const unitPath = getSystemdUnitPath();
2289
2328
  if (fs.existsSync(unitPath)) {
@@ -0,0 +1,24 @@
1
+ import { type KeychainBackend } from '../secrets/index.js';
2
+ /**
3
+ * Shared fixture for the daemon.*.test.ts suite slices (RUSH-2819).
4
+ *
5
+ * daemon.test.ts was a single 2201-line file (88 tests, ~112s in CI) — the
6
+ * slowest file in the daemon test-ownership group, serializing an entire
7
+ * vitest fork while every other selected file finished. The suite is split
8
+ * into topical slices so per-file fork parallelism can spread the
9
+ * process-spawning / real-daemon integration tests across workers; the
10
+ * helpers shared by more than one slice live here.
11
+ */
12
+ /** An in-memory KeychainBackend stand-in, so a test never touches the real OS keychain. */
13
+ export declare function makeMemoryBackend(): {
14
+ backend: KeychainBackend;
15
+ store: Map<string, string>;
16
+ };
17
+ /**
18
+ * Registers the keychain-backend swap + AGENTS_SECRETS_NO_AGENT hermeticity
19
+ * beforeEach/afterEach every slice of the original daemon.test.ts ran under,
20
+ * verbatim. Call once at the top level of each slice file.
21
+ */
22
+ export declare function installKeychainHermeticity(): void;
23
+ export declare const REPO_ROOT: string;
24
+ export declare const DIST_ENTRY: string;
@@ -0,0 +1,71 @@
1
+ import { beforeEach, afterEach } from 'vitest';
2
+ import * as path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { setKeychainBackendForTest } from '../secrets/index.js';
5
+ import { deleteBundle } from '../secrets/bundles.js';
6
+ /**
7
+ * Shared fixture for the daemon.*.test.ts suite slices (RUSH-2819).
8
+ *
9
+ * daemon.test.ts was a single 2201-line file (88 tests, ~112s in CI) — the
10
+ * slowest file in the daemon test-ownership group, serializing an entire
11
+ * vitest fork while every other selected file finished. The suite is split
12
+ * into topical slices so per-file fork parallelism can spread the
13
+ * process-spawning / real-daemon integration tests across workers; the
14
+ * helpers shared by more than one slice live here.
15
+ */
16
+ /** An in-memory KeychainBackend stand-in, so a test never touches the real OS keychain. */
17
+ export function makeMemoryBackend() {
18
+ const store = new Map();
19
+ const backend = {
20
+ has: (item) => store.has(item),
21
+ get: (item) => {
22
+ const v = store.get(item);
23
+ if (v === undefined)
24
+ throw new Error(`Keychain item '${item}' not found.`);
25
+ return v;
26
+ },
27
+ set: (item, value) => { store.set(item, value); },
28
+ delete: (item) => store.delete(item),
29
+ list: (prefix) => Array.from(store.keys()).filter((k) => k.startsWith(prefix)),
30
+ };
31
+ return { backend, store };
32
+ }
33
+ /**
34
+ * Registers the keychain-backend swap + AGENTS_SECRETS_NO_AGENT hermeticity
35
+ * beforeEach/afterEach every slice of the original daemon.test.ts ran under,
36
+ * verbatim. Call once at the top level of each slice file.
37
+ */
38
+ export function installKeychainHermeticity() {
39
+ let restore = null;
40
+ let prevNoAgent;
41
+ beforeEach(() => {
42
+ const m = makeMemoryBackend();
43
+ restore = setKeychainBackendForTest(m.backend);
44
+ // Hermeticity: readAndResolveBundleEnv consults the running secrets-agent
45
+ // (bundles.ts agentGetSync fast-path) BEFORE the injected keychain backend.
46
+ // On a dev machine where the agent is live and the real `claude` bundle is
47
+ // unlocked, that returns the machine's real CLAUDE_CODE_OAUTH_TOKEN and this
48
+ // test reads a live credential instead of the seeded value (CI has no agent,
49
+ // so it only bites locally). Disable the agent so the read falls through to
50
+ // the in-memory backend above — hermetic regardless of host state.
51
+ prevNoAgent = process.env.AGENTS_SECRETS_NO_AGENT;
52
+ process.env.AGENTS_SECRETS_NO_AGENT = '1';
53
+ });
54
+ afterEach(() => {
55
+ try {
56
+ deleteBundle('claude');
57
+ }
58
+ catch { /* not created */ }
59
+ setKeychainBackendForTest(restore);
60
+ if (prevNoAgent === undefined)
61
+ delete process.env.AGENTS_SECRETS_NO_AGENT;
62
+ else
63
+ process.env.AGENTS_SECRETS_NO_AGENT = prevNoAgent;
64
+ });
65
+ }
66
+ // The real compiled CLI entry, shared by every slice that drives an actual
67
+ // `__daemon-run` subprocess (lifecycle/registry/stop). Computed relative to
68
+ // this file's own location, which stays in src/lib/daemon/ alongside every
69
+ // slice, so the path math is unaffected by the split.
70
+ export const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
71
+ export const DIST_ENTRY = path.join(REPO_ROOT, 'dist', 'index.js');
@@ -74,6 +74,23 @@ interface RoutineAttempt {
74
74
  * but the gate should have paused such a routine before it ever spawned.
75
75
  */
76
76
  export declare function routineSpawnCwd(config: Pick<JobConfig, 'name' | 'project' | 'cwd' | 'agent' | 'workflow' | 'command'>): string;
77
+ /**
78
+ * Bake the daemon-job argv skeleton from AGENT_COMMANDS.
79
+ *
80
+ * The two tables used to be independent copies of the same launch decision.
81
+ * Argv now comes from AGENT_COMMANDS.base / promptFlag / jsonFlags / modeFlags.
82
+ * Two documented exceptions, not a second table:
83
+ * - kimi uses `--prompt` (long form). `-p` is the exec alias; combining
84
+ * either with --plan/--auto/--yolo aborts, so routineModeArgs emits no
85
+ * mode flag. The daemon has always spawned the long form.
86
+ * - claude places `--verbose` (from jsonFlags) before the prompt and bakes
87
+ * modeFlags.plan so claudeAdapter.routineModeArgs can splice
88
+ * plan → acceptEdits / auto / skip.
89
+ *
90
+ * Returns undefined when `agent` is outside ROUTINE_AGENT_IDS (gemini, grok,
91
+ * and every other harness the daemon does not fire locally).
92
+ */
93
+ export declare function bakeRoutineArgv(agent: string): string[] | undefined;
77
94
  /** Build the full CLI argv for executing a job, applying mode, model, and permission flags. */
78
95
  export declare function buildJobCommand(config: JobConfig, resolvedPrompt: string, forwardAccount?: boolean): string[];
79
96
  /**
@@ -161,7 +178,7 @@ export declare function buildHostDispatchOptions(config: JobConfig, ctx: {
161
178
  runDir: string;
162
179
  detached: boolean;
163
180
  }): import('../hosts/run-target.js').HostPromptRun;
164
- export declare function dispatchesViaAgentsRun(config: Pick<JobConfig, 'workflow' | 'resume'>): boolean;
181
+ export declare function dispatchesViaAgentsRun(config: Pick<JobConfig, 'workflow' | 'resume' | 'agent'>): boolean;
165
182
  /**
166
183
  * Merge sandbox/base env with the canonical per-version exec env
167
184
  * (CLAUDE_CONFIG_DIR / CODEX_HOME / …) so routines share account isolation
@@ -21,7 +21,7 @@ import { getCliLaunch, getAgentsBinDir } from '../cli-entry.js';
21
21
  import { resolveJobPrompt, parseTimeout, writeRunMeta, listRuns, getJobRunsDir, getRunDir, checkJobDeviceEligibility, finalizeRunMeta, resolveJobExecutionContext, slotRunId, claimRunSlot, readRunMeta, resolveHostStrategy, } from '../scheduling/routines.js';
22
22
  import { getRunsDir, getUserAgentsDir, readMeta, getDaemonDir } from '../state.js';
23
23
  import { shortCodexHome } from '../codex-home.js';
24
- import { prepareJobHome, buildSpawnEnv, getJobHomePath } from '../sandbox.js';
24
+ import { prepareJobHome, buildSpawnEnv, getJobHomePath, assertSandboxForwardsHostGhAuth } from '../sandbox.js';
25
25
  import { resolveModel, buildReasoningFlags } from '../models.js';
26
26
  import { createTimer, redactPrompt, emitRoutineEnd } from '../feed/events.js';
27
27
  import { resolveHarnessAdapter } from '../harness/index.js';
@@ -39,7 +39,8 @@ 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 { isSelfUpdatingAgent, ROUTINE_AGENT_COMMANDS, isAgentHardDeprecated, hardDeprecationError } from '../agents.js';
42
+ import { isSelfUpdatingAgent, ROUTINE_AGENT_IDS, isAgentHardDeprecated, hardDeprecationError } from '../agents.js';
43
+ import { isCustomHarnessName, readProfile } from '../profiles.js';
43
44
  export class RoutineAlreadyRunningError extends Error {
44
45
  constructor(jobName, runId) {
45
46
  super(`Routine '${jobName}' already has a running execution (${runId})`);
@@ -308,7 +309,7 @@ function allocateRoutineAttempt(config, trigger) {
308
309
  // harness leaves the command table (gemini did), and the legacy routine must
309
310
  // still land a visible 'blocked' record, never a generic buildJobCommand
310
311
  // failure (RUSH-2202).
311
- if (!config.workflow && config.agent && isAgentHardDeprecated(config.agent)) {
312
+ if (!config.workflow && config.agent && !isCustomHarnessName(config.agent) && isAgentHardDeprecated(config.agent)) {
312
313
  const reason = hardDeprecationError(config.agent);
313
314
  return {
314
315
  proceed: false,
@@ -488,6 +489,48 @@ export function routineSpawnCwd(config) {
488
489
  const ctx = resolveJobExecutionContext(config, { mode: 'local' });
489
490
  return ctx.absoluteCwd ?? os.homedir();
490
491
  }
492
+ /**
493
+ * Bake the daemon-job argv skeleton from AGENT_COMMANDS.
494
+ *
495
+ * The two tables used to be independent copies of the same launch decision.
496
+ * Argv now comes from AGENT_COMMANDS.base / promptFlag / jsonFlags / modeFlags.
497
+ * Two documented exceptions, not a second table:
498
+ * - kimi uses `--prompt` (long form). `-p` is the exec alias; combining
499
+ * either with --plan/--auto/--yolo aborts, so routineModeArgs emits no
500
+ * mode flag. The daemon has always spawned the long form.
501
+ * - claude places `--verbose` (from jsonFlags) before the prompt and bakes
502
+ * modeFlags.plan so claudeAdapter.routineModeArgs can splice
503
+ * plan → acceptEdits / auto / skip.
504
+ *
505
+ * Returns undefined when `agent` is outside ROUTINE_AGENT_IDS (gemini, grok,
506
+ * and every other harness the daemon does not fire locally).
507
+ */
508
+ export function bakeRoutineArgv(agent) {
509
+ if (!ROUTINE_AGENT_IDS.includes(agent))
510
+ return undefined;
511
+ const template = AGENT_COMMANDS[agent];
512
+ if (!template)
513
+ return undefined;
514
+ const json = template.jsonFlags ?? [];
515
+ const cmd = [...template.base];
516
+ if (agent === 'kimi') {
517
+ cmd.push('--prompt', '{prompt}', ...json);
518
+ return cmd;
519
+ }
520
+ if (agent === 'claude') {
521
+ const verbose = json.includes('--verbose') ? ['--verbose'] : [];
522
+ const jsonRest = json.filter((flag) => flag !== '--verbose');
523
+ cmd.push(template.promptFlag, ...verbose, '{prompt}', ...jsonRest, ...(template.modeFlags.plan ?? []));
524
+ return cmd;
525
+ }
526
+ if (template.promptFlag === 'positional') {
527
+ cmd.push('{prompt}', ...json);
528
+ }
529
+ else {
530
+ cmd.push(template.promptFlag, '{prompt}', ...json);
531
+ }
532
+ return cmd;
533
+ }
491
534
  /** Build the full CLI argv for executing a job, applying mode, model, and permission flags. */
492
535
  export function buildJobCommand(config, resolvedPrompt, forwardAccount = true) {
493
536
  // Workflow branch: delegate to `agents run <workflow>` which handles subagent
@@ -514,9 +557,25 @@ export function buildJobCommand(config, resolvedPrompt, forwardAccount = true) {
514
557
  cmd.push('--account', config.account);
515
558
  return cmd;
516
559
  }
517
- const template = ROUTINE_AGENT_COMMANDS[agent];
560
+ // Custom-harness branch: delegate to `agents run <name>`, which owns profile
561
+ // resolution (host binary, model env, provider auth) — the same delegation
562
+ // the workflow and resume branches use. The profile pins its own version and
563
+ // auth, so no command template, binary pinning, or account-env injection
564
+ // applies here.
565
+ if (isCustomHarnessName(agent)) {
566
+ const cmd = ['agents', 'run', agent, resolvedPrompt, '--mode', config.mode];
567
+ if (config.account && forwardAccount)
568
+ cmd.push('--account', config.account);
569
+ return cmd;
570
+ }
571
+ const template = bakeRoutineArgv(agent);
518
572
  if (!template) {
519
- throw new Error(`Unsupported agent for daemon jobs: ${agent}`);
573
+ // A name outside both tables was either never valid or was a custom
574
+ // harness whose profile has since been deleted — validateJob accepted it
575
+ // against a profile that existed then. Name the repair, not just the miss.
576
+ throw new Error(`Unsupported agent for daemon jobs: ${agent}. ` +
577
+ `If '${agent}' was a custom harness, its profile no longer exists on this device — ` +
578
+ `recreate it (agents harness add ${agent} ...) or point the routine at another agent.`);
520
579
  }
521
580
  let cmd = template.map((part) => part.replace('{prompt}', resolvedPrompt));
522
581
  // Canonicalize mode (accepts legacy `full` as alias for `skip`).
@@ -554,7 +613,8 @@ export function buildJobCommand(config, resolvedPrompt, forwardAccount = true) {
554
613
  * Reasoning level (config.config.reasoning) maps to per-agent flags via models.ts.
555
614
  */
556
615
  function appendModelAndReasoning(cmd, config) {
557
- // Only called from buildJobCommand's agent path config.agent is set.
616
+ // Only called from buildJobCommand's agent path AFTER the custom-harness
617
+ // branch returned — config.agent is a native id here.
558
618
  const agent = config.agent;
559
619
  const model = config.config?.model;
560
620
  if (model) {
@@ -636,6 +696,8 @@ function transcriptBasePath(runDir) {
636
696
  export function snapshotRoutineTranscriptBase(meta, runDir, overlayHome) {
637
697
  if (!meta.agent)
638
698
  return;
699
+ // A host/cloud custom-harness run records the harness name, which has no
700
+ // transcript spec — the null-specs guard below skips it, as before.
639
701
  const specs = ROUTINE_TRANSCRIPT_SPECS[meta.agent];
640
702
  if (!specs)
641
703
  return;
@@ -674,6 +736,8 @@ function readTranscriptBase(runDir) {
674
736
  export function archiveRoutineTranscripts(meta, runDir, overlayHome) {
675
737
  if (!meta.agent)
676
738
  return;
739
+ // A host/cloud custom-harness run records the harness name, which has no
740
+ // transcript spec — the null-specs guard below skips it, as before.
677
741
  const specs = ROUTINE_TRANSCRIPT_SPECS[meta.agent];
678
742
  if (!specs)
679
743
  return;
@@ -808,6 +872,13 @@ export async function resolveRoutineLaunch(config, cwd = process.cwd(), deps = {
808
872
  if (config.workflow) {
809
873
  return { chain: [], rotation: null, pinned: false };
810
874
  }
875
+ if (config.agent && isCustomHarnessName(config.agent)) {
876
+ // A custom harness pins its own version/auth in the profile, so there is
877
+ // no version/account chain to resolve here — `agents run <name>` owns it
878
+ // (matching exec's "strategy ignored: custom harness pins its own
879
+ // version/auth").
880
+ return { chain: [], rotation: null, pinned: false };
881
+ }
811
882
  // resolveRoutineLaunch is only called for agent jobs (workflow returns above;
812
883
  // command jobs branch out of execute*Job before reaching this).
813
884
  const agent = config.agent;
@@ -1012,7 +1083,7 @@ export function buildHostDispatchOptions(config, ctx) {
1012
1083
  };
1013
1084
  }
1014
1085
  export function dispatchesViaAgentsRun(config) {
1015
- return Boolean(config.workflow || config.resume);
1086
+ return Boolean(config.workflow || config.resume || (config.agent && isCustomHarnessName(config.agent)));
1016
1087
  }
1017
1088
  /**
1018
1089
  * Merge sandbox/base env with the canonical per-version exec env
@@ -1214,7 +1285,11 @@ async function executeJobPlaced(config, deps, attempt) {
1214
1285
  // the agent's config dir, and the sandbox overlay home has only a freshly-generated
1215
1286
  // config with no session store. So a resume job is never sandboxed, regardless of
1216
1287
  // `config.sandbox` (see the resume branch in buildJobCommand).
1217
- const useSandbox = config.sandbox !== false && !config.resume;
1288
+ // Resume needs the REAL home (session store); a custom harness needs it too —
1289
+ // the delegated `agents run <name>` resolves ~/.agents (profiles, setup
1290
+ // sentinel, version homes) from HOME, which the overlay would hide. Exec
1291
+ // still isolates the run in the host version home it swaps HOME into.
1292
+ const useSandbox = config.sandbox !== false && !config.resume && !(config.agent && isCustomHarnessName(config.agent));
1218
1293
  const overlayHome = useSandbox ? prepareJobHome(config) : undefined;
1219
1294
  const runId = attempt.runId;
1220
1295
  const runDir = getRunDir(config.name, runId);
@@ -1225,7 +1300,11 @@ async function executeJobPlaced(config, deps, attempt) {
1225
1300
  // Workflows run via `agents run <workflow>` which delegates to claude under the hood.
1226
1301
  // Use 'claude' as the effective agent for report extraction and metadata when workflow is set.
1227
1302
  // (command jobs branched out earlier, so config.agent is set on the non-workflow path.)
1228
- const effectiveAgent = config.workflow ? 'claude' : config.agent;
1303
+ const effectiveAgent = config.workflow
1304
+ ? 'claude'
1305
+ : isCustomHarnessName(config.agent)
1306
+ ? readProfile(config.agent).host.agent
1307
+ : config.agent;
1229
1308
  if (!dispatchesViaAgentsRun(config)) {
1230
1309
  const { findUnifiedAccount, resolveAccountSelection, resolveCredentialAccount } = await import('../account-registry.js');
1231
1310
  const meta = readMeta();
@@ -1238,6 +1317,9 @@ async function executeJobPlaced(config, deps, attempt) {
1238
1317
  Object.assign(baseEnv, resolveCredentialAccount(selectedAccount, effectiveAgent).env);
1239
1318
  }
1240
1319
  }
1320
+ // RUSH-2860: if this host holds gh auth, the sandbox child MUST see it.
1321
+ if (useSandbox)
1322
+ assertSandboxForwardsHostGhAuth(baseEnv);
1241
1323
  const meta = {
1242
1324
  jobName: config.name,
1243
1325
  runId,
@@ -1728,7 +1810,11 @@ async function executeJobDetachedClaimed(config, attempt, hooks) {
1728
1810
  // the agent's config dir, and the sandbox overlay home has only a freshly-generated
1729
1811
  // config with no session store. So a resume job is never sandboxed, regardless of
1730
1812
  // `config.sandbox` (see the resume branch in buildJobCommand).
1731
- const useSandbox = config.sandbox !== false && !config.resume;
1813
+ // Resume needs the REAL home (session store); a custom harness needs it too —
1814
+ // the delegated `agents run <name>` resolves ~/.agents (profiles, setup
1815
+ // sentinel, version homes) from HOME, which the overlay would hide. Exec
1816
+ // still isolates the run in the host version home it swaps HOME into.
1817
+ const useSandbox = config.sandbox !== false && !config.resume && !(config.agent && isCustomHarnessName(config.agent));
1732
1818
  const overlayHome = useSandbox ? prepareJobHome(config) : undefined;
1733
1819
  const runId = attempt.runId;
1734
1820
  const runDir = getRunDir(config.name, runId);
@@ -1755,7 +1841,15 @@ async function executeJobDetachedClaimed(config, attempt, hooks) {
1755
1841
  })()
1756
1842
  // Non-command path only: config.agent is always set here (command/workflow branch earlier).
1757
1843
  : buildRoutineSpawnEnv(baseEnv, config.agent, version, config.timezone, overlayHome);
1758
- const effectiveAgent = config.workflow ? 'claude' : config.agent;
1844
+ // RUSH-2860: if this host holds gh auth, the sandbox child MUST see it —
1845
+ // otherwise monitors runs records ok while every gh call inside fails.
1846
+ if (useSandbox)
1847
+ assertSandboxForwardsHostGhAuth(spawnEnv);
1848
+ const effectiveAgent = config.workflow
1849
+ ? 'claude'
1850
+ : isCustomHarnessName(config.agent)
1851
+ ? readProfile(config.agent).host.agent
1852
+ : config.agent;
1759
1853
  const meta = {
1760
1854
  jobName: config.name,
1761
1855
  runId,
@@ -18,11 +18,48 @@ export declare function isFreshFleetAuthSnapshot(value: {
18
18
  row: FleetStatusRow;
19
19
  authRows: AuthProbeRow[];
20
20
  }, minimumCapturedAt: number): boolean;
21
+ /**
22
+ * How stale a cached auth verdict may get before the periodic tick re-probes.
23
+ *
24
+ * The auth verdict rides the same rate-limited `/api/oauth/usage` endpoint as
25
+ * the usage probe. Firing it every 3-minute tick on every fleet device drove one
26
+ * per-account request quota to a permanent 429, and the shared backoff then
27
+ * parked usage fleet-wide and froze the usage cache (RUSH-2998). Re-probing at
28
+ * most every 20 minutes cuts that endpoint traffic ~5x while still catching a
29
+ * revocation within one window — and every device keeps a REAL verdict (not a
30
+ * degraded "unverified"), so `agents devices ping --strict` and the run
31
+ * auth-preflight keep working on every host, unlike a publisher/subscriber split
32
+ * that would blind every non-primary box to revocation. Fleet status still
33
+ * publishes every tick — it does not ride that endpoint.
34
+ */
35
+ export declare const AUTH_PROBE_MAX_AGE_MS: number;
36
+ /**
37
+ * True when every cached auth row was probed within {@link AUTH_PROBE_MAX_AGE_MS}
38
+ * — i.e. reusing them would not let a verdict get staler than one probe window.
39
+ * Empty cache is never fresh (nothing to reuse). Pure — unit-tested.
40
+ */
41
+ export declare function isCachedFleetAuthProbeFresh(authRows: AuthProbeRow[], now: number, maxAgeMs?: number): boolean;
42
+ /**
43
+ * Whether the tick may reuse the cached auth verdict instead of re-probing the
44
+ * rate-limited endpoint. `force` (an on-demand `agents devices ping`) ALWAYS
45
+ * re-probes — the whole point of the command is a genuinely live verdict, and
46
+ * missing this `!force` is exactly how a `--strict` check silently passed a
47
+ * revoked account (the second `runFleetPing` call site, RUSH-2998). Pure —
48
+ * unit-tested so that inversion cannot regress unnoticed.
49
+ */
50
+ export declare function shouldReuseCachedAuthProbe(force: boolean, cached: AuthProbeRow[], now: number, maxAgeMs?: number): boolean;
21
51
  /**
22
52
  * Fleet cache warm: publish THIS host's row for the caches `agents fleet
23
53
  * status` / `agents devices list` read (PUBLISH-OWN / READ-UNION, RUSH-2061).
54
+ *
55
+ * `force` is set by on-demand callers (`agents devices ping`) that must return a
56
+ * genuinely live verdict; the periodic daemon tick leaves it unset so it reuses a
57
+ * recent verdict per {@link AUTH_PROBE_MAX_AGE_MS} instead of hammering the
58
+ * rate-limited endpoint every 3 minutes (RUSH-2998).
24
59
  */
25
- export declare function refreshLocalFleetAuthState(): Promise<{
60
+ export declare function refreshLocalFleetAuthState(opts?: {
61
+ force?: boolean;
62
+ }): Promise<{
26
63
  row: FleetStatusRow;
27
64
  authRows: import('./auth-health.js').AuthProbeRow[];
28
65
  }>;