@humwick/runner 0.0.0-stage → 0.130.1

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 (293) hide show
  1. package/LICENSE +167 -0
  2. package/agent-bin/agent-relay-browser +10 -0
  3. package/agent-bin/humwick +10 -0
  4. package/hooks/fff-search-redirect/fff-search-redirect.cjs +161 -0
  5. package/hooks/fff-search-redirect/hooks.json +13 -0
  6. package/hooks/rg-r-guard/hooks.json +18 -0
  7. package/hooks/rg-r-guard/rg-r-guard.cjs +144 -0
  8. package/hooks/web-tools-redirect/hooks.json +13 -0
  9. package/hooks/web-tools-redirect/web-tools-redirect.cjs +84 -0
  10. package/node_modules/@humwick/offload/LICENSE +167 -0
  11. package/node_modules/@humwick/offload/README.md +57 -0
  12. package/node_modules/@humwick/offload/dist/async-runner.d.ts +18 -0
  13. package/node_modules/@humwick/offload/dist/async-runner.d.ts.map +1 -0
  14. package/node_modules/@humwick/offload/dist/async-runner.js +158 -0
  15. package/node_modules/@humwick/offload/dist/async-runner.js.map +1 -0
  16. package/node_modules/@humwick/offload/dist/envelope.d.ts +25 -0
  17. package/node_modules/@humwick/offload/dist/envelope.d.ts.map +1 -0
  18. package/node_modules/@humwick/offload/dist/envelope.js +80 -0
  19. package/node_modules/@humwick/offload/dist/envelope.js.map +1 -0
  20. package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts +10 -0
  21. package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts.map +1 -0
  22. package/node_modules/@humwick/offload/dist/fixtures/worker-task.js +59 -0
  23. package/node_modules/@humwick/offload/dist/fixtures/worker-task.js.map +1 -0
  24. package/node_modules/@humwick/offload/dist/index.d.ts +5 -0
  25. package/node_modules/@humwick/offload/dist/index.d.ts.map +1 -0
  26. package/node_modules/@humwick/offload/dist/index.js +5 -0
  27. package/node_modules/@humwick/offload/dist/index.js.map +1 -0
  28. package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts +62 -0
  29. package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts.map +1 -0
  30. package/node_modules/@humwick/offload/dist/subprocess-runner.js +295 -0
  31. package/node_modules/@humwick/offload/dist/subprocess-runner.js.map +1 -0
  32. package/node_modules/@humwick/offload/dist/types.d.ts +111 -0
  33. package/node_modules/@humwick/offload/dist/types.d.ts.map +1 -0
  34. package/node_modules/@humwick/offload/dist/types.js +176 -0
  35. package/node_modules/@humwick/offload/dist/types.js.map +1 -0
  36. package/node_modules/@humwick/offload/dist/worker-pool.d.ts +23 -0
  37. package/node_modules/@humwick/offload/dist/worker-pool.d.ts.map +1 -0
  38. package/node_modules/@humwick/offload/dist/worker-pool.js +435 -0
  39. package/node_modules/@humwick/offload/dist/worker-pool.js.map +1 -0
  40. package/node_modules/@humwick/offload/dist/worker-runtime.d.ts +2 -0
  41. package/node_modules/@humwick/offload/dist/worker-runtime.d.ts.map +1 -0
  42. package/node_modules/@humwick/offload/dist/worker-runtime.js +27 -0
  43. package/node_modules/@humwick/offload/dist/worker-runtime.js.map +1 -0
  44. package/node_modules/@humwick/offload/dist/worker.d.ts +5 -0
  45. package/node_modules/@humwick/offload/dist/worker.d.ts.map +1 -0
  46. package/node_modules/@humwick/offload/dist/worker.js +1 -0
  47. package/node_modules/@humwick/offload/dist/worker.js.map +1 -0
  48. package/node_modules/@humwick/offload/package.json +42 -0
  49. package/node_modules/@humwick/offload/src/async-runner.ts +170 -0
  50. package/node_modules/@humwick/offload/src/envelope.ts +92 -0
  51. package/node_modules/@humwick/offload/src/fixtures/worker-task.ts +49 -0
  52. package/node_modules/@humwick/offload/src/index.ts +4 -0
  53. package/node_modules/@humwick/offload/src/subprocess-runner.ts +334 -0
  54. package/node_modules/@humwick/offload/src/types.ts +235 -0
  55. package/node_modules/@humwick/offload/src/worker-pool.ts +463 -0
  56. package/node_modules/@humwick/offload/src/worker-runtime.ts +32 -0
  57. package/node_modules/@humwick/offload/src/worker.ts +5 -0
  58. package/package.json +47 -4
  59. package/plugins/browser/skills/playwright-cli/SKILL.md +53 -0
  60. package/plugins/claude/.claude-plugin/plugin.json +8 -0
  61. package/plugins/claude/claude-statusline-probe.cjs +144 -0
  62. package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +914 -0
  63. package/plugins/claude/hooks/gate-run-guard.sh +9 -0
  64. package/plugins/claude/hooks/gate-run-guard.ts +237 -0
  65. package/plugins/claude/hooks/hooks.json +158 -0
  66. package/plugins/claude/hooks/kill-guard.provisioned.mjs +7027 -0
  67. package/plugins/claude/hooks/kill-guard.sh +341 -0
  68. package/plugins/claude/hooks/kill-guard.ts +337 -0
  69. package/plugins/claude/hooks/permission-request.sh +22 -0
  70. package/plugins/claude/hooks/post-compact.sh +10 -0
  71. package/plugins/claude/hooks/post-tool-use.sh +8 -0
  72. package/plugins/claude/hooks/pre-compact.sh +12 -0
  73. package/plugins/claude/hooks/read-only-bash-guard.sh +587 -0
  74. package/plugins/claude/hooks/relay-status.sh +373 -0
  75. package/plugins/claude/hooks/session-end.sh +26 -0
  76. package/plugins/claude/hooks/session-start.sh +31 -0
  77. package/plugins/claude/hooks/shell-trust.ts +28 -0
  78. package/plugins/claude/hooks/stop-failure.sh +47 -0
  79. package/plugins/claude/hooks/stop.sh +48 -0
  80. package/plugins/claude/hooks/subagent-guard.cjs +26 -0
  81. package/plugins/claude/hooks/subagent-skill-policy.cjs +160 -0
  82. package/plugins/claude/hooks/subagent-start.sh +13 -0
  83. package/plugins/claude/hooks/subagent-stop.sh +19 -0
  84. package/plugins/claude/hooks/user-prompt-submit.sh +18 -0
  85. package/plugins/claude/hooks/wait-loop-guard.ts +120 -0
  86. package/plugins/claude/monitors/monitors.json +7 -0
  87. package/plugins/claude/monitors/relay-monitor.provisioned.mjs +786 -0
  88. package/plugins/claude/monitors/relay-monitor.ts +58 -0
  89. package/plugins/claude/skills/disconnect/SKILL.md +16 -0
  90. package/plugins/claude/skills/label/SKILL.md +23 -0
  91. package/plugins/claude/skills/message/SKILL.md +40 -0
  92. package/plugins/claude/skills/pair/SKILL.md +30 -0
  93. package/plugins/claude/skills/react/SKILL.md +18 -0
  94. package/plugins/claude/skills/read-message/SKILL.md +24 -0
  95. package/plugins/claude/skills/reply/SKILL.md +44 -0
  96. package/plugins/claude/skills/send-claimable/SKILL.md +39 -0
  97. package/plugins/claude/skills/slice-first/SKILL.md +32 -0
  98. package/plugins/claude/skills/slice-first/references/plan-recipe.md +46 -0
  99. package/plugins/claude/skills/status/SKILL.md +16 -0
  100. package/plugins/claude/skills/tags/SKILL.md +25 -0
  101. package/plugins/claude/skills/task/SKILL.md +28 -0
  102. package/plugins/claude-output-style/.claude-plugin/plugin.json +5 -0
  103. package/plugins/claude-output-style/output-styles/agent-relay.md +5 -0
  104. package/plugins/codex/skills/disconnect/SKILL.md +15 -0
  105. package/plugins/codex/skills/guide/SKILL.md +15 -0
  106. package/plugins/codex/skills/label/SKILL.md +22 -0
  107. package/plugins/codex/skills/message/SKILL.md +40 -0
  108. package/plugins/codex/skills/pair/SKILL.md +29 -0
  109. package/plugins/codex/skills/react/SKILL.md +17 -0
  110. package/plugins/codex/skills/read-message/SKILL.md +23 -0
  111. package/plugins/codex/skills/reply/SKILL.md +44 -0
  112. package/plugins/codex/skills/send-claimable/SKILL.md +39 -0
  113. package/plugins/codex/skills/slice-first/SKILL.md +32 -0
  114. package/plugins/codex/skills/slice-first/references/plan-recipe.md +46 -0
  115. package/plugins/codex/skills/status/SKILL.md +15 -0
  116. package/plugins/codex/skills/tags/SKILL.md +24 -0
  117. package/plugins/codex/skills/task/SKILL.md +28 -0
  118. package/src/adapter.ts +1061 -0
  119. package/src/adapters/__fixtures__/codex-app-server.ts +47 -0
  120. package/src/adapters/claude-api.ts +243 -0
  121. package/src/adapters/claude-context-command.ts +16 -0
  122. package/src/adapters/claude-credentials.ts +22 -0
  123. package/src/adapters/claude-delivery-lanes.ts +28 -0
  124. package/src/adapters/claude-delivery.ts +166 -0
  125. package/src/adapters/claude-effort.ts +94 -0
  126. package/src/adapters/claude-exit.ts +290 -0
  127. package/src/adapters/claude-headless-mirror.ts +88 -0
  128. package/src/adapters/claude-headless-refusal.ts +51 -0
  129. package/src/adapters/claude-headless.ts +617 -0
  130. package/src/adapters/claude-monitor-delivery-plan.ts +317 -0
  131. package/src/adapters/claude-permission.ts +243 -0
  132. package/src/adapters/claude-prompt-gates.ts +269 -0
  133. package/src/adapters/claude-prompt-hook.ts +145 -0
  134. package/src/adapters/claude-session-capture.ts +293 -0
  135. package/src/adapters/claude-session-probe.ts +414 -0
  136. package/src/adapters/claude-shutdown.ts +190 -0
  137. package/src/adapters/claude-status-detectors.ts +426 -0
  138. package/src/adapters/claude-subagents.ts +93 -0
  139. package/src/adapters/claude-submit-confirmation.ts +287 -0
  140. package/src/adapters/claude-tmux.ts +548 -0
  141. package/src/adapters/claude-token-usage-capture.ts +253 -0
  142. package/src/adapters/claude-tool-policy.ts +165 -0
  143. package/src/adapters/claude-transcript-tail.ts +193 -0
  144. package/src/adapters/claude-transcript.ts +812 -0
  145. package/src/adapters/claude.ts +1097 -0
  146. package/src/adapters/codex-agent-message-capture.ts +189 -0
  147. package/src/adapters/codex-app-server.ts +70 -0
  148. package/src/adapters/codex-client.ts +362 -0
  149. package/src/adapters/codex-effective-config.ts +87 -0
  150. package/src/adapters/codex-launch-config.ts +153 -0
  151. package/src/adapters/codex-rollout-context.ts +272 -0
  152. package/src/adapters/codex-skill-discovery.ts +18 -0
  153. package/src/adapters/codex-token-usage-capture.ts +163 -0
  154. package/src/adapters/codex-tool-output.ts +53 -0
  155. package/src/adapters/codex-turn-outcome.ts +27 -0
  156. package/src/adapters/codex-version.ts +64 -0
  157. package/src/adapters/codex.ts +1442 -0
  158. package/src/adapters/incremental-file.ts +110 -0
  159. package/src/adapters/opencode-api.ts +71 -0
  160. package/src/adapters/opencode-context.ts +277 -0
  161. package/src/adapters/opencode-model-registry.ts +126 -0
  162. package/src/adapters/opencode-provider.ts +169 -0
  163. package/src/adapters/opencode-server-retry.ts +50 -0
  164. package/src/adapters/opencode-soft-steer.ts +302 -0
  165. package/src/adapters/opencode-spawn.ts +145 -0
  166. package/src/adapters/opencode-subscribe.ts +62 -0
  167. package/src/adapters/opencode-token-usage-capture.ts +187 -0
  168. package/src/adapters/opencode-tool-events.ts +302 -0
  169. package/src/adapters/opencode-tools-config.ts +17 -0
  170. package/src/adapters/opencode.ts +2264 -0
  171. package/src/adapters/project-shell.ts +80 -0
  172. package/src/adapters/provider-launch-config.ts +21 -0
  173. package/src/adapters/provider-quota-harvest.ts +127 -0
  174. package/src/adapters/service-access.ts +153 -0
  175. package/src/adapters/tmux-offload.ts +207 -0
  176. package/src/adapters/tool-summary.ts +214 -0
  177. package/src/agent-redact.ts +75 -0
  178. package/src/agent-runner/commands/dispatch.ts +185 -0
  179. package/src/agent-runner/commands/lifecycle.ts +84 -0
  180. package/src/agent-runner/commands/prompt.ts +284 -0
  181. package/src/agent-runner/credentials.ts +208 -0
  182. package/src/agent-runner/http-liveness.ts +85 -0
  183. package/src/agent-runner/lifecycle-milestones.ts +207 -0
  184. package/src/agent-runner/lifecycle-phase.ts +80 -0
  185. package/src/agent-runner/registration.ts +163 -0
  186. package/src/agent-runner/runner-context.ts +158 -0
  187. package/src/agent-runner/session-mirror.ts +278 -0
  188. package/src/agent-runner/task-claims.ts +116 -0
  189. package/src/agent-runner/token-usage-coverage.ts +54 -0
  190. package/src/agent-runner/turn-report.ts +269 -0
  191. package/src/api-budget-accounting.ts +91 -0
  192. package/src/area-memory.ts +255 -0
  193. package/src/artifact-path-materialize.ts +52 -0
  194. package/src/async-guard.ts +35 -0
  195. package/src/attachment-cache-sweep.ts +31 -0
  196. package/src/attachment-cache.ts +195 -0
  197. package/src/background-script-reconcile.ts +106 -0
  198. package/src/bounded-log.ts +59 -0
  199. package/src/bounded-spawn-sync.ts +18 -0
  200. package/src/busy-reconciler.ts +282 -0
  201. package/src/claim-tracker.ts +140 -0
  202. package/src/claude-settings.ts +167 -0
  203. package/src/claude-workspace-trust.ts +289 -0
  204. package/src/config.ts +547 -0
  205. package/src/context-boundary.ts +101 -0
  206. package/src/context-command.ts +151 -0
  207. package/src/context-probe-state.ts +6 -0
  208. package/src/continuation-archive.ts +179 -0
  209. package/src/control-server.ts +730 -0
  210. package/src/delivery-policy.ts +9 -0
  211. package/src/delivery-result.ts +24 -0
  212. package/src/effort-command.ts +125 -0
  213. package/src/empty-turn.ts +816 -0
  214. package/src/free-port.ts +25 -0
  215. package/src/fs-offload.ts +26 -0
  216. package/src/gate-run-config.ts +35 -0
  217. package/src/gate-run-lease-probe.ts +38 -0
  218. package/src/host-asset-plan.ts +450 -0
  219. package/src/host-claude-config-worker.ts +34 -0
  220. package/src/host-sources.ts +40 -0
  221. package/src/inbox-carrier.ts +7 -0
  222. package/src/index.ts +386 -0
  223. package/src/launch-assembly.ts +955 -0
  224. package/src/liveness.ts +105 -0
  225. package/src/logger.ts +108 -0
  226. package/src/mcp-outbox.ts +204 -0
  227. package/src/mcp-preflight.ts +86 -0
  228. package/src/mcp-source-selection.ts +54 -0
  229. package/src/mcp-tool-deferral.ts +35 -0
  230. package/src/mcp-vault-env.ts +25 -0
  231. package/src/message-body-config/config.ts +16 -0
  232. package/src/message-body-config.ts +1 -0
  233. package/src/mid-turn-delivery.ts +43 -0
  234. package/src/native-memory-lever.ts +329 -0
  235. package/src/native-self-resume.ts +244 -0
  236. package/src/outbox.ts +453 -0
  237. package/src/park-post.ts +17 -0
  238. package/src/pending-permission-store.ts +148 -0
  239. package/src/process-meta.ts +9 -0
  240. package/src/profile-home.ts +419 -0
  241. package/src/profile-projection.ts +411 -0
  242. package/src/project-hooks.ts +65 -0
  243. package/src/project-instructions.ts +119 -0
  244. package/src/provider-double.ts +59 -0
  245. package/src/provider-hold.ts +228 -0
  246. package/src/provider-home-marker.ts +3 -0
  247. package/src/provider-home-tombstone.ts +4 -0
  248. package/src/provider-input-queue.ts +134 -0
  249. package/src/provider-integration.ts +93 -0
  250. package/src/provider-processes.ts +17 -0
  251. package/src/provider-submit-failure.ts +15 -0
  252. package/src/providers.ts +35 -0
  253. package/src/provisioning.ts +703 -0
  254. package/src/question-decision.ts +18 -0
  255. package/src/quota.ts +22 -0
  256. package/src/rate-limit.ts +351 -0
  257. package/src/refused-prompt-store.ts +36 -0
  258. package/src/relay-injection-events.ts +161 -0
  259. package/src/relay-instructions.ts +107 -0
  260. package/src/relay-mcp-proxy.long-turn-e2e.ts +183 -0
  261. package/src/relay-mcp-proxy.ts +704 -0
  262. package/src/relay-mcp.ts +583 -0
  263. package/src/reply-obligation-cache.ts +211 -0
  264. package/src/response-capture-report.ts +111 -0
  265. package/src/response-routing.ts +6 -0
  266. package/src/runner-core.ts +2955 -0
  267. package/src/runner-exit.ts +12 -0
  268. package/src/runner-helpers.ts +582 -0
  269. package/src/runner-insights.ts +105 -0
  270. package/src/runner-mcp-proxy.ts +46 -0
  271. package/src/runner-spawn-config.ts +68 -0
  272. package/src/runner-stale-scratch-sweep.ts +21 -0
  273. package/src/runner.ts +9 -0
  274. package/src/runtime-token-file.ts +45 -0
  275. package/src/runtime-token-remint.ts +56 -0
  276. package/src/service-access.ts +88 -0
  277. package/src/session-destroy.ts +27 -0
  278. package/src/session-insights.ts +118 -0
  279. package/src/session-scratch.ts +296 -0
  280. package/src/spawn-input.ts +147 -0
  281. package/src/startup-config-fetch.ts +48 -0
  282. package/src/subreaper.ts +113 -0
  283. package/src/template-resolver.ts +29 -0
  284. package/src/test-run-dedup.ts +435 -0
  285. package/src/transcript-context.ts +99 -0
  286. package/src/transport-errors.ts +48 -0
  287. package/src/turn-opener.ts +55 -0
  288. package/src/turn-reconcile-event.ts +88 -0
  289. package/src/vault-proxy-hop.ts +99 -0
  290. package/src/version.ts +44 -0
  291. package/src/web-tool-call-proxy.ts +128 -0
  292. package/src/web-tools.ts +59 -0
  293. package/README.md +0 -3
package/src/index.ts ADDED
@@ -0,0 +1,386 @@
1
+ #!/usr/bin/env -S bun --no-env-file
2
+ import { hostname } from "node:os";
3
+ import { disableCoreDumps } from "@humwick/sdk/core-dumps";
4
+ import { AgentRunner } from "./runner";
5
+ import {
6
+ agentProfileJsonFromEnv,
7
+ harnessSubagentsFromEnv,
8
+ agentProfileNameFromEnv,
9
+ automationIdFromEnv,
10
+ automationRunIdFromEnv,
11
+ lifecycleFromEnv,
12
+ loadGlobalConfig,
13
+ loadProviderConfigFromRelay,
14
+ policyNameFromEnv,
15
+ promptFromEnv,
16
+ providerConfigLookupFromEnv,
17
+ relayInjectionEventsJsonFromEnv,
18
+ resolveCwd,
19
+ runnerId,
20
+ runtimeTokenExpiresAtFromEnv,
21
+ runtimeTokenJtiFromEnv,
22
+ runtimeTokenProfileFromEnv,
23
+ spawnRequestIdFromEnv,
24
+ systemPromptAppendFromEnv,
25
+ taskIdFromEnv,
26
+ tmuxSessionFromEnv,
27
+ workspaceJsonFromEnv,
28
+ projectToolchainFromEnv,
29
+ } from "./config";
30
+ import { createProviderAdapter } from "./providers";
31
+ import { parseRunnerCliArgs, runnerHelp } from "./spawn-input";
32
+ import { VERSION } from "./version";
33
+ import type { AgentProfile, WorkspaceMetadata } from "@humwick/sdk";
34
+ import { exitRunner } from "./runner-exit";
35
+ import { codexConfigOverrides, errMessage, formatRunnerExitLine, isRecord, isRelayInjectionCategory, normalizeAgentLifecycle, RELAY_TOKEN_HEADER, providerArgsSelectStreamJson, startArtifactCheckoutLease } from "@humwick/sdk";
36
+ import { supportedLaunchModesForProvider } from "@humwick/providers";
37
+ import { installUnhandledRejectionGuard } from "./async-guard";
38
+ import type { RunnerRelayInjectionEvent } from "./relay-injection-events";
39
+ import { enableRunnerSubreaper } from "./subreaper";
40
+
41
+ export async function main(argv = process.argv): Promise<void> {
42
+ disableCoreDumps();
43
+ // #2443 — last-resort observability, installed before anything can schedule work. Two managed
44
+ // OpenCode sessions died with a clean `exit(1)` and an EMPTY stderr capture: the actual cause
45
+ // (a terminal provider failure resolving through onProviderExit, see runner-core.ts) turned out
46
+ // not to be a crash at all, but this guard closes the still-real, long-documented gap (#1665)
47
+ // for any genuine unhandled rejection or uncaught exception elsewhere in the runner — neither
48
+ // currently leaves a trace beyond "exited after Ns". `unhandledRejection` mirrors the
49
+ // orchestrator's non-fatal "log and survive" backstop (see the SDK function's doc): nothing
50
+ // here respawns a dead runner (#1618), so surviving beats dying silently. `uncaughtException`
51
+ // stays fatal (a synchronous throw can leave a half-applied mutation behind — same ruling as
52
+ // the orchestrator) but is logged deterministically via multiple paths first, so a crash always
53
+ // leaves a stack trace regardless of whether Bun's own default reporter would have shown one.
54
+ // Installed inside `main()`, not at module scope: this file is imported directly by tests
55
+ // (config.test.ts, runner.test.ts) that never call `main()`, and a top-level `process.on(...)`
56
+ // would leak a real, process-fatal `uncaughtException` handler into the whole `bun test` run.
57
+ installUnhandledRejectionGuard();
58
+ process.on("uncaughtException", (error) => {
59
+ const detail = error instanceof Error ? (error.stack ?? error.message) : String(error);
60
+ try { process.stderr.write(`[runner] uncaughtException (fatal): ${detail}\n`); } catch { /* best-effort */ }
61
+ try { console.error(`[runner] uncaughtException (fatal): ${errMessage(error)}`); } catch { /* best-effort */ }
62
+ exitRunner(1, `uncaughtException: ${errMessage(error)}`);
63
+ });
64
+ if (isVersionRequest(argv)) {
65
+ console.log(VERSION);
66
+ return;
67
+ }
68
+ const opts = parseRunnerCliArgs(argv);
69
+ if (opts.help) {
70
+ console.log(runnerHelp(opts.provider));
71
+ return;
72
+ }
73
+ enableRunnerSubreaper();
74
+ // Started only once we know this is a real launch, not --version/--help: the lease holds an
75
+ // un-refed setInterval alive (see sdk/src/artifact-checkout-gc.ts), so starting it earlier
76
+ // stops --version/--help from ever exiting on a host whose runtime is a checkout symlink
77
+ // (e.g. the wbox self-hosted runner) — the process hangs until externally killed. #2122
78
+ startArtifactCheckoutLease();
79
+ const globalConfig = loadGlobalConfig();
80
+ const relayUrl = opts.relayUrl ?? globalConfig.relayUrl;
81
+ const providerConfig = await loadRunnerProviderConfig(opts.provider, relayUrl, opts.token ?? globalConfig.token);
82
+ const cwd = resolveCwd(opts.cwd, globalConfig.defaultCwd);
83
+ const id = runnerId(opts.provider, cwd, opts.label);
84
+ // Providers offering both modes always record the one they run in; raw stream-json args stay a legacy headless selector.
85
+ const launchMode = opts.launchMode ?? (supportedLaunchModesForProvider(opts.provider).length
86
+ ? (providerArgsSelectStreamJson(opts.providerArgs) ? "headless" : "interactive")
87
+ : undefined);
88
+ const adapter = createProviderAdapter(opts.provider, opts.providerArgs, launchMode);
89
+ // Resolved BEFORE the interactive token exchange so the mint can stamp the posture into the signed
90
+ // constraints (#2392): `agent.meta.approvalMode` is authz-relevant and therefore stripped at
91
+ // registration unless the token can prove it (src/authz-meta-keys.ts).
92
+ const approvalMode = opts.approvalMode
93
+ ?? detectApprovalModeFromArgs(opts.provider, providerConfig.defaultArgs, opts.providerArgs)
94
+ ?? providerConfig.defaultApprovalMode;
95
+ const runtimeAuth = await resolveRunnerToken({
96
+ interactive: opts.interactive,
97
+ relayUrl,
98
+ token: opts.token ?? globalConfig.token,
99
+ runtimeTokenProfile: runtimeTokenProfileFromEnv(),
100
+ runtimeTokenJti: runtimeTokenJtiFromEnv(),
101
+ runtimeTokenExpiresAt: parseTokenExpiresAt(runtimeTokenExpiresAtFromEnv()),
102
+ provider: opts.provider,
103
+ cwd,
104
+ runnerId: id,
105
+ agentId: opts.agentId ?? id,
106
+ label: opts.label,
107
+ approvalMode,
108
+ });
109
+
110
+ let exitResolve: (code: number) => void;
111
+ let exitReason: string | undefined;
112
+ const exitPromise = new Promise<number>((resolve) => { exitResolve = resolve; });
113
+
114
+ // Provider-owned async preload reaches registration projection before any synchronous launch
115
+ // assembly runs, without leaking provider-specific host config parsing into this CLI boundary.
116
+ const launchConfig = await adapter.preloadRunnerConfig?.();
117
+
118
+ const runner = new AgentRunner({
119
+ provider: opts.provider,
120
+ model: opts.model,
121
+ effort: opts.effort,
122
+ // #2595 — effective-catalog window resolved server-side; the OpenCode adapter prefers it
123
+ // over its static manifest so override-defined models (muse-spark) report context usage.
124
+ ...(opts.contextWindowTokens !== undefined ? { contextWindowTokens: opts.contextWindowTokens } : {}),
125
+ // #2599 — the full record (protocol ≥ 2); the adapter applies payload > manifest precedence.
126
+ ...(opts.modelRecord ? { modelRecord: opts.modelRecord } : {}),
127
+ runnerId: id,
128
+ instanceId: crypto.randomUUID(),
129
+ agentId: opts.agentId,
130
+ relayUrl,
131
+ token: runtimeAuth.token,
132
+ tokenJti: runtimeAuth.jti,
133
+ tokenProfileId: runtimeAuth.profileId,
134
+ tokenExpiresAt: runtimeAuth.expiresAt,
135
+ rootTokenFallback: runtimeAuth.rootFallback,
136
+ cwd,
137
+ headless: opts.headless,
138
+ ...(launchMode ? { launchMode } : {}),
139
+ approvalMode,
140
+ label: opts.label,
141
+ rig: opts.rig,
142
+ profile: opts.profile ?? agentProfileNameFromEnv(),
143
+ harnessSubagents: harnessSubagentsFromEnv(),
144
+ agentProfile: parseAgentProfileEnv(agentProfileJsonFromEnv()),
145
+ workspace: parseWorkspaceEnv(workspaceJsonFromEnv()),
146
+ toolchain: projectToolchainFromEnv(),
147
+ prompt: opts.prompt ?? promptFromEnv(),
148
+ systemPromptAppend: opts.systemPromptAppend ?? systemPromptAppendFromEnv(),
149
+ relayInjectionEvents: parseRelayInjectionEventsEnv(relayInjectionEventsJsonFromEnv()),
150
+ tmuxSession: tmuxSessionFromEnv(),
151
+ tags: opts.tags,
152
+ capabilities: opts.caps,
153
+ providerArgs: opts.providerArgs,
154
+ policyName: policyNameFromEnv(),
155
+ spawnRequestId: spawnRequestIdFromEnv(),
156
+ taskId: taskIdFromEnv(),
157
+ automationId: automationIdFromEnv(),
158
+ automationRunId: automationRunIdFromEnv(),
159
+ lifecycle: normalizeAgentLifecycle(lifecycleFromEnv()) ?? "persistent",
160
+ startedAt: Date.now(),
161
+ providerConfig,
162
+ ...(launchConfig ? { launchConfig } : {}),
163
+ adapter,
164
+ // #2443 — printed via console.error (captured into the systemd-redirected stdout/stderr log
165
+ // the orchestrator tails on exit, see logFileDiagnostics/readLogTail) BEFORE process.exit runs
166
+ // below, so a terminal-provider-failure exit reads "model-unavailable: APIError 404 ..." in
167
+ // that log instead of leaving it empty.
168
+ onProviderExit: (code, reason) => {
169
+ exitReason = reason;
170
+ exitResolve(code ?? 1);
171
+ },
172
+ });
173
+ await runner.run();
174
+
175
+ const code = await Promise.race([exitPromise, signalPromise(runner)]);
176
+ // #2619 — the structured exit record is written before stop() can stall: the orchestrator parses
177
+ // it back from the log tail when systemd has already collected the unit. exitRunner then exits.
178
+ const reason = exitReason ?? (code === 130 || code === 143 ? `signal ${code === 130 ? "SIGINT" : "SIGTERM"}` : undefined);
179
+ console.error(formatRunnerExitLine(code, reason));
180
+ await runner.stop();
181
+ exitRunner(code, reason);
182
+ }
183
+
184
+ /** The runner main-path central-config bootstrap, kept callable for seam tests. */
185
+ export function loadRunnerProviderConfig(provider: string, relayUrl: string, token?: string) {
186
+ return loadProviderConfigFromRelay(provider, {
187
+ relayUrl,
188
+ token,
189
+ ...providerConfigLookupFromEnv(),
190
+ });
191
+ }
192
+
193
+ export async function resolveRunnerToken(input: {
194
+ interactive: boolean;
195
+ relayUrl: string;
196
+ token?: string;
197
+ runtimeTokenProfile?: string;
198
+ runtimeTokenJti?: string;
199
+ runtimeTokenExpiresAt?: number;
200
+ provider: string;
201
+ cwd: string;
202
+ runnerId: string;
203
+ agentId: string;
204
+ label?: string;
205
+ /** #2392 — the posture this interactive session actually launches with. Sent so the relay can stamp
206
+ * it into the signed constraints; without it the session's `meta.approvalMode` is unprovable and
207
+ * gets stripped at registration, silently downgrading spawned children to `guarded`. */
208
+ approvalMode?: string;
209
+ }): Promise<{ token?: string; jti?: string; profileId?: string; expiresAt?: number; rootFallback?: boolean }> {
210
+ if (!input.token) return {};
211
+ const claims = decodeComponentTokenClaims(input.token);
212
+ if (input.runtimeTokenProfile) {
213
+ return {
214
+ token: input.token,
215
+ jti: input.runtimeTokenJti ?? claims?.jti,
216
+ profileId: input.runtimeTokenProfile,
217
+ expiresAt: input.runtimeTokenExpiresAt ?? claims?.exp,
218
+ };
219
+ }
220
+ const inferredRuntime = inferRuntimeTokenMetadata(claims);
221
+ if (!input.interactive && inferredRuntime) return { token: input.token, ...inferredRuntime };
222
+ if (!input.interactive) return { token: input.token };
223
+
224
+ try {
225
+ const url = new URL("/api/runtime-tokens/interactive-runner", input.relayUrl);
226
+ const res = await fetch(url, {
227
+ method: "POST",
228
+ headers: {
229
+ "Content-Type": "application/json",
230
+ [RELAY_TOKEN_HEADER]: input.token,
231
+ },
232
+ body: JSON.stringify({
233
+ provider: input.provider,
234
+ cwd: input.cwd,
235
+ runnerId: input.runnerId,
236
+ agentId: input.agentId,
237
+ label: input.label,
238
+ host: hostname(),
239
+ ...(input.approvalMode ? { approvalMode: input.approvalMode } : {}),
240
+ }),
241
+ signal: AbortSignal.timeout(5_000),
242
+ });
243
+ const body = await res.json().catch(() => null) as { token?: string; record?: { jti?: string; profileId?: string; expiresAt?: number }; error?: string } | null;
244
+ if (res.ok && body?.token) return { token: body.token, jti: body.record?.jti, profileId: body.record?.profileId ?? "provider-interactive", expiresAt: body.record?.expiresAt };
245
+ const detail = body?.error ? `: ${body.error}` : "";
246
+ console.warn(`[agent-relay] interactive scoped-token exchange failed (${res.status}${detail}); continuing with existing token. Root-token runtime fallback is deprecated.`);
247
+ } catch (error) {
248
+ console.warn(`[agent-relay] interactive scoped-token exchange failed (${errMessage(error)}); continuing with existing token. Root-token runtime fallback is deprecated.`);
249
+ }
250
+ return { token: input.token, rootFallback: true };
251
+ }
252
+
253
+ function decodeComponentTokenClaims(token: string | undefined): { sub?: string; role?: string; exp?: number; jti?: string } | undefined {
254
+ const payload = token?.split(".")[1];
255
+ if (!payload) return undefined;
256
+ try {
257
+ const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
258
+ const decoded = JSON.parse(Buffer.from(normalized, "base64").toString("utf8")) as Record<string, unknown>;
259
+ const exp = typeof decoded.exp === "number" && Number.isSafeInteger(decoded.exp) && decoded.exp > 0 ? decoded.exp : undefined;
260
+ return {
261
+ ...(typeof decoded.sub === "string" ? { sub: decoded.sub } : {}),
262
+ ...(typeof decoded.role === "string" ? { role: decoded.role } : {}),
263
+ ...(typeof decoded.jti === "string" ? { jti: decoded.jti } : {}),
264
+ ...(exp ? { exp } : {}),
265
+ };
266
+ } catch {
267
+ return undefined;
268
+ }
269
+ }
270
+
271
+ function inferRuntimeTokenMetadata(claims: { sub?: string; role?: string; exp?: number; jti?: string } | undefined): { jti?: string; profileId?: string; expiresAt?: number } | undefined {
272
+ if (claims?.role !== "provider" || !claims.exp) return undefined;
273
+ const sub = claims.sub ?? "";
274
+ const profileId = sub.startsWith("runner:interactive:")
275
+ ? "provider-interactive"
276
+ : sub.startsWith("runner:spawn:") || sub.startsWith("runner:policy:") || /^runner:(codex|claude|provider):/.test(sub)
277
+ ? "provider-agent"
278
+ : undefined;
279
+ if (!profileId) return undefined;
280
+ return {
281
+ ...(claims.jti ? { jti: claims.jti } : {}),
282
+ profileId,
283
+ expiresAt: claims.exp,
284
+ };
285
+ }
286
+
287
+ function parseTokenExpiresAt(value: string | undefined): number | undefined {
288
+ if (!value) return undefined;
289
+ const parsed = Number(value);
290
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
291
+ }
292
+
293
+ export function isVersionRequest(argv: string[]): boolean {
294
+ const relayArgs = argv.slice(2);
295
+ const providerSep = relayArgs.indexOf("--");
296
+ const ownArgs = providerSep >= 0 ? relayArgs.slice(0, providerSep) : relayArgs;
297
+ return ownArgs.some((arg) => arg === "--version" || arg === "-v");
298
+ }
299
+
300
+
301
+ function parseAgentProfileEnv(raw: string | undefined): AgentProfile | undefined {
302
+ if (!raw) return undefined;
303
+ try {
304
+ const parsed = JSON.parse(raw) as Partial<AgentProfile>;
305
+ return parsed && typeof parsed === "object" && typeof parsed.name === "string" ? parsed as AgentProfile : undefined;
306
+ } catch {
307
+ return undefined;
308
+ }
309
+ }
310
+
311
+ function parseWorkspaceEnv(raw: string | undefined): WorkspaceMetadata | undefined {
312
+ if (!raw) return undefined;
313
+ try {
314
+ const parsed = JSON.parse(raw) as Partial<WorkspaceMetadata>;
315
+ return parsed && typeof parsed === "object" && typeof parsed.mode === "string" ? parsed as WorkspaceMetadata : undefined;
316
+ } catch {
317
+ return undefined;
318
+ }
319
+ }
320
+
321
+ function parseRelayInjectionEventsEnv(raw: string | undefined): RunnerRelayInjectionEvent[] | undefined {
322
+ if (!raw) return undefined;
323
+ try {
324
+ const parsed = JSON.parse(raw);
325
+ if (!Array.isArray(parsed)) return undefined;
326
+ const events = parsed.flatMap((item): RunnerRelayInjectionEvent[] => {
327
+ if (!isRecord(item) || typeof item.summary !== "string" || !isRelayInjectionCategory(item.category)) return [];
328
+ const detail = isRecord(item.detail) ? item.detail : {};
329
+ return [{ category: item.category, summary: item.summary, detail }];
330
+ });
331
+ return events.length ? events : undefined;
332
+ } catch {
333
+ return undefined;
334
+ }
335
+ }
336
+
337
+ // #2414 — arm the `shuttingDown` restart-suppression latch synchronously, in the signal handler itself, BEFORE resolving this
338
+ // promise (which only unblocks `main()`'s `await Promise.race(...)`, itself an extra tick before
339
+ // `await runner.stop()` ever runs). See `markShuttingDown`'s doc comment in runner-core.ts for the
340
+ // race this closes: a direct-child provider (Codex/OpenCode) dying from the SAME process-group
341
+ // signal the orchestrator just sent this process can otherwise race `handleAdapterStatusChange`
342
+ // ahead of `stop()` and get treated as an unexpected crash worth restarting.
343
+ export function signalPromise(runner: Pick<AgentRunner, "markShuttingDown">): Promise<number> {
344
+ return new Promise<number>((resolve) => {
345
+ process.on("SIGINT", () => { runner.markShuttingDown(); resolve(130); });
346
+ process.on("SIGTERM", () => { runner.markShuttingDown(); resolve(143); });
347
+ });
348
+ }
349
+
350
+ type ApprovalModeDetector = (allArgs: string[]) => string | undefined;
351
+
352
+ const APPROVAL_MODE_DETECTORS: Record<string, ApprovalModeDetector> = {
353
+ claude(allArgs) {
354
+ if (allArgs.includes("--dangerously-skip-permissions")) return "open";
355
+ const modeIdx = allArgs.lastIndexOf("--permission-mode");
356
+ if (modeIdx >= 0) {
357
+ const mode = allArgs[modeIdx + 1];
358
+ if (mode === "dontAsk") return "read-only";
359
+ if (mode === "default") return "guarded";
360
+ }
361
+ return undefined;
362
+ },
363
+ codex(allArgs) {
364
+ if (allArgs.includes("--yolo")) return "open";
365
+ for (const value of codexConfigOverrides(allArgs)) {
366
+ if (/sandbox_mode\s*=\s*"?danger-full-access"?/.test(value)) return "open";
367
+ if (/sandbox_mode\s*=\s*"?read-only"?/.test(value)) return "read-only";
368
+ }
369
+ return undefined;
370
+ },
371
+ };
372
+
373
+ export function detectApprovalModeFromArgs(provider: string, defaultArgs: string[], providerArgs: string[]): string | undefined {
374
+ const detector = APPROVAL_MODE_DETECTORS[provider];
375
+ return detector ? detector([...defaultArgs, ...providerArgs]) : undefined;
376
+ }
377
+
378
+ if (import.meta.main) {
379
+ main().catch((error) => {
380
+ // A fatal startup failure (e.g. the runner could not register with the relay within the
381
+ // deadline) must exit non-zero, not hang or swallow — the orchestrator's exit detection
382
+ // then reports it and the spawn-parent is notified instead of waiting on a silent zombie.
383
+ console.error(`[agent-relay] runner fatal: ${errMessage(error)}`);
384
+ exitRunner(1, `runner fatal: ${errMessage(error)}`);
385
+ });
386
+ }