@humwick/runner 0.0.0-stage → 0.130.0

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 +46 -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
@@ -0,0 +1,419 @@
1
+ import { relayEnv } from "@humwick/sdk/env";
2
+ import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { sanitizeFsName } from "@humwick/sdk/fs-name";
6
+ import { isRecord, projectSettingSourcesOn as sdkProjectSettingSourcesOn, type ProvisioningMcpServer, type SpawnProvider } from "@humwick/sdk";
7
+ import { getManifest } from "@humwick/providers";
8
+ import { profileAllowsRelayFeature, type RunnerSpawnConfig } from "./adapter";
9
+ import { claudeRelayManual } from "./relay-instructions";
10
+ import { providerHomeRootFromEnv } from "./config";
11
+ import { applyClaudeConfigPromptGatePreventions } from "./adapters/claude-prompt-gates";
12
+ import { ensureClaudeWorkspaceTrust } from "./claude-workspace-trust";
13
+ import { logger } from "./logger";
14
+ import { writeProviderHomeMarker } from "./provider-home-marker";
15
+ import { reclaimProviderHomeTombstone } from "./provider-home-tombstone";
16
+ import { runnerHostConfigOffload } from "./fs-offload";
17
+ import { hostSourcesFor, writeFileAtomic } from "./host-asset-plan";
18
+
19
+ type ProviderHome = {
20
+ path: string;
21
+ authLinked: string[];
22
+ };
23
+
24
+ export function profileAllowsHostGlobalInstructions(config: { agentProfile?: RunnerSpawnConfig["agentProfile"] }): boolean {
25
+ return hostSourcesFor(config).instructions;
26
+ }
27
+
28
+ /**
29
+ * Whether a profile enables any Project setting-source cell. This is deliberately
30
+ * separate from the managed-home decision: the native Claude cascade is the
31
+ * Project re-injection mechanism.
32
+ *
33
+ * The predicate itself lives in the sdk ({@link sdkProjectSettingSourcesOn}) so the
34
+ * effective-profile survey and the v2 cell deriver stay in step with the runner's
35
+ * actual launch derivation — see #2084.
36
+ */
37
+ export function projectSettingSourcesOn(config: { agentProfile?: RunnerSpawnConfig["agentProfile"] }): boolean {
38
+ const profile = config.agentProfile;
39
+ if (!profile) return false;
40
+ return sdkProjectSettingSourcesOn(profile);
41
+ }
42
+
43
+ /** The exact Claude setting-source list for a profiled launch. */
44
+ export function projectSettingSources(config: { agentProfile?: RunnerSpawnConfig["agentProfile"] }): string[] | undefined {
45
+ if (!config.agentProfile) return undefined;
46
+ return projectSettingSourcesOn(config) ? ["user", "project"] : ["user"];
47
+ }
48
+
49
+ // #3387 — every runner-managed profiled launch runs in a Relay-owned home; `base:"host"` only
50
+ // changes which host classes an omitted Host field inherits (see hostSourcesFor). A profile-less
51
+ // launch is a user's own wrapped session and keeps the provider's native home.
52
+ function profileRequiresIsolatedHome(config: RunnerSpawnConfig): boolean {
53
+ return Boolean(config.agentProfile);
54
+ }
55
+
56
+ // #557 — PURE: the Relay-owned provider config home path a launch WILL use, or undefined for a
57
+ // profile-less launch (the provider's normal home). The launch assembler computes the home this way
58
+ // to report it + build CLAUDE_CONFIG_DIR/skill/plugin paths without side effects;
59
+ // prepare*ProfileHome creates it at the same path at spawn time.
60
+ export function providerHomePathFor(provider: SpawnProvider, config: RunnerSpawnConfig): string | undefined {
61
+ if (!profileRequiresIsolatedHome(config)) return undefined;
62
+ return providerHomePath(provider, config);
63
+ }
64
+
65
+ // First-run bootstrap (provider layer)
66
+ // -------------------------------------
67
+ // Isolated profile homes are keyed by instanceId, so every spawn gets a brand
68
+ // new CLAUDE_CONFIG_DIR / CODEX_HOME — to the provider it always looks like the
69
+ // first time it has ever run, even in a repo we've used before. Without
70
+ // bootstrapping, the CLI stalls on first-run gates (theme/onboarding picker,
71
+ // "do you trust this folder?") and the agent never starts; the runner then sees
72
+ // repeated ~2s exits and gives up. bootstrap<Provider>FirstRun makes a fresh
73
+ // home launch-ready non-interactively, guaranteeing three things:
74
+ // 1. host auth is linked in,
75
+ // 2. first-run / onboarding flags are pre-accepted (where the provider has them),
76
+ // 3. the cwd workspace is trusted.
77
+ // Workspace trust is independent of repoInstructions: "ignore" means don't load
78
+ // the repo's CLAUDE.md / AGENTS.md as behavioral instructions, NOT "don't
79
+ // operate here" — an isolated agent must still read/edit/execute in its own cwd.
80
+ //
81
+ // #2254 — claude-rig / host-base launches skip the onboarding/theme bootstrap (an
82
+ // onboarded host config already answers those), but they do NOT dodge folder trust:
83
+ // --dangerously-skip-permissions does not bypass the trust gate, and a cwd the host
84
+ // config has never seen still renders the dialog and wedges the spawn. Trust is
85
+ // therefore seeded for EVERY Claude launch — see prepareClaudeProfileHome.
86
+
87
+ // Provider-specific first-run bootstrap hooks, registered at module load.
88
+ type BootstrapHook = (home: string, config: RunnerSpawnConfig) => void;
89
+ const BOOTSTRAP_HOOKS = new Map<string, BootstrapHook>();
90
+
91
+ /**
92
+ * The ONE materialization seam for an instance-keyed provider home
93
+ * (`<root>/<provider>/<profile>/<instanceId>`) — every adapter that creates one MUST route
94
+ * through it (#2624: the OpenCode family built its home by hand, so its homes carried no
95
+ * owner-marker and no tombstone reclaim, and the reaper skipped every one of them as
96
+ * "unmarked" forever). `identity` must be exactly the path segments of `target`: the reaper
97
+ * refuses a marker that does not describe its own directory.
98
+ */
99
+ export function materializeProviderHome(target: string, identity: { provider: string; profile: string; instanceId: string; agentId?: string }, mode?: number): void {
100
+ // #1256 spawn-side reclaim — FIRST recover any quarantine tombstone the reaper left for
101
+ // this exact instanceId (atomic rename back to `target`), BEFORE creating a fresh home.
102
+ // This makes reconnection always safe (a reconnecting/resuming agent recovers its home)
103
+ // and lets a crash-stranded tombstone self-heal. A lost reclaim race just falls through
104
+ // to a fresh mkdir below — never a corrupt home.
105
+ reclaimProviderHomeTombstone(target);
106
+ mkdirSync(target, { recursive: true, ...(mode === undefined ? {} : { mode }) });
107
+ // #1256 — stamp the owner-marker so the reaper can later reclaim this home if it becomes
108
+ // orphaned, but only ever on THIS host (cross-host safety).
109
+ writeProviderHomeMarker(target, identity);
110
+ }
111
+
112
+ // Shared skeleton: gate on isolated-profile, make the instance-keyed home,
113
+ // link auth items from the manifest, and run any registered first-run hook.
114
+ function prepareProviderHome(provider: SpawnProvider, config: RunnerSpawnConfig): ProviderHome | undefined {
115
+ if (!profileRequiresIsolatedHome(config)) return undefined;
116
+ const target = providerHomePath(provider, config);
117
+ materializeProviderHome(target, {
118
+ provider,
119
+ profile: sanitizePathPart(config.agentProfile?.name || config.profile || "profile"),
120
+ instanceId: sanitizePathPart(config.instanceId || config.runnerId),
121
+ agentId: config.agentId,
122
+ });
123
+ const hook = BOOTSTRAP_HOOKS.get(provider);
124
+ if (hook) hook(target, config);
125
+ const manifest = getManifest(provider);
126
+ const authItems = manifest?.home?.authItems ?? [];
127
+ const sourceHome = providerSourceHome(provider);
128
+ const authLinked = linkExistingAuthItems(sourceHome, target, authItems);
129
+ return { path: target, authLinked };
130
+ }
131
+
132
+ export const prepareCodexProfileHome = (config: RunnerSpawnConfig) => prepareProviderHome("codex", config);
133
+
134
+ /**
135
+ * #2254 — every Claude launch, isolated or host-base, leaves this function with folder trust
136
+ * already granted in the config file it will read. `prepareProviderHome` returns undefined for
137
+ * a host-base profile (and for a profile-less spawn), which is exactly the class that used to
138
+ * be seeded nowhere and wedged on the trust dialog; `ensureClaudeWorkspaceTrust` therefore runs
139
+ * unconditionally, targeting the shared host config when there is no isolated home.
140
+ *
141
+ * #2513 — provider-id-dynamic for the claude family: claude-b passes its own id so the
142
+ * isolated home, bootstrap hook, and auth links resolve against `~/.claude-b` (its manifest
143
+ * configDir), never `~/.claude`.
144
+ */
145
+ export const prepareClaudeProfileHome = (config: RunnerSpawnConfig, provider: SpawnProvider = "claude"): ProviderHome | undefined => {
146
+ const home = prepareProviderHome(provider, config);
147
+ // #2513 review HIGH-1 — a dedicatedHome provider's host-base launch still reads the
148
+ // provider's OWN effective home (launch assembly exports CLAUDE_CONFIG_DIR at it), so
149
+ // folder trust must be seeded in THAT home's .claude.json, not the shared ~/.claude.json.
150
+ const trust = ensureClaudeWorkspaceTrust(config.cwd, home?.path ?? providerHostConfigHome(provider));
151
+ // A seed that did not land puts this launch straight back into the #2254 wedge — CC opens
152
+ // its trust dialog and the spawn hangs until a human answers. The failure modes are all
153
+ // environmental (an unparseable or unwritable host config, an operator opt-out), so the fix
154
+ // cannot self-heal; it must at least say so instead of failing silently the way the missing
155
+ // seed did for months. "no-cwd" is the one benign case: no cwd, nothing to trust.
156
+ if (!trust.verified && trust.skipped !== "no-cwd") {
157
+ logger.warn("profile-home", `claude workspace trust NOT seeded (${trust.skipped ?? "unverified"}) in ${trust.path} for keys [${trust.keys.join(", ")}] — this launch may wedge on the folder-trust dialog`);
158
+ }
159
+ return home;
160
+ };
161
+
162
+ function bootstrapCodexFirstRun(codexHome: string, config: RunnerSpawnConfig): void {
163
+ // Keep bootstrap to one config read/write while repairing auth links (#2670).
164
+ const path = join(codexHome, "config.toml");
165
+ const current = existsSync(path) ? readFileSync(path, "utf8") : "";
166
+ const next = withCodexWorkspaceTrust(withTomlFeatureFlag(current, "apps", false), config);
167
+ if (next !== current) writeFileSync(path, next, { mode: 0o600 });
168
+ }
169
+
170
+ function bootstrapClaudeFirstRun(claudeHome: string, config: RunnerSpawnConfig): void {
171
+ if (profileAllowsRelayFeature(config, "context") && !profileAllowsHostGlobalInstructions(config)) {
172
+ writeClaudeRelayManual(claudeHome);
173
+ }
174
+ ensureClaudeConfigSeed(claudeHome, config);
175
+ }
176
+
177
+ BOOTSTRAP_HOOKS.set("codex", bootstrapCodexFirstRun);
178
+ BOOTSTRAP_HOOKS.set("claude", bootstrapClaudeFirstRun);
179
+ // #2513 — the claude-b provider runs the identical claude first-run bootstrap (relay
180
+ // manual + config seed) inside its own claude-b-keyed home.
181
+ BOOTSTRAP_HOOKS.set("claude-b", bootstrapClaudeFirstRun);
182
+ BOOTSTRAP_HOOKS.set("claude-api", bootstrapClaudeFirstRun);
183
+
184
+ export function providerSourceHome(provider: string): string {
185
+ const manifest = getManifest(provider);
186
+ const envVar = manifest?.home?.configDirEnvVar;
187
+ const configDir = manifest?.home?.configDir ?? `.${provider}`;
188
+ const sourceEnvVar = manifest?.home?.sourceEnvVar ?? envVar;
189
+ return (sourceEnvVar && relayEnv(process.env, sourceEnvVar)) || join(homedir(), configDir);
190
+ }
191
+
192
+ /**
193
+ * #2513 — the effective HOST config home a `dedicatedHome` provider (claude-b) launches
194
+ * from when it has no isolated managed home, or undefined for the primary provider
195
+ * (claude), whose host-base launches keep their historical behavior of falling back to
196
+ * the binary's own resolution ($CLAUDE_CONFIG_DIR ?? ~/.claude). Callers that need one
197
+ * config dir for EVERY launch of a dedicated-home provider use
198
+ * `isolatedHome ?? providerHostConfigHome(provider)`.
199
+ */
200
+ export function providerHostConfigHome(provider: SpawnProvider): string | undefined {
201
+ return getManifest(provider)?.home?.dedicatedHome ? providerSourceHome(provider) : undefined;
202
+ }
203
+
204
+ function ensureClaudeConfigSeed(claudeHome: string, config: RunnerSpawnConfig): void {
205
+ const path = join(claudeHome, ".claude.json");
206
+ const host = config.hostClaudeConfig;
207
+ const seed = applyClaudeConfigPromptGatePreventions({}, host, config);
208
+ if (!existsSync(path)) {
209
+ writeJsonAtomic(path, seed);
210
+ return;
211
+ }
212
+ // #1368/#harden — the isolated home is instance-keyed and PERSISTS across resumes/reconnects, so a
213
+ // relaunch (e.g. auto-resume after a tmux/provider exit) reuses the existing `.claude.json` and
214
+ // never re-runs the absent-file seed above. Re-heal every prompt-gate key the seed would have
215
+ // applied on a fresh home (onboarding, theme, bypass-permissions, workspace-trust): a resumed
216
+ // config stale/missing on ANY of them boots straight into that gate's interactive prompt, can't
217
+ // answer headlessly, and crash-loops the agent offline. A corrupt/unparseable/non-object existing
218
+ // file can't be surgically merged — its prior contents are unrecoverable anyway — so it gets a
219
+ // full reseed instead of bailing and leaving every gate unguarded; a well-formed file gets a
220
+ // sibling-preserving merge.
221
+ const existing = readExistingClaudeConfig(path);
222
+ if (!existing) {
223
+ writeJsonAtomic(path, seed);
224
+ return;
225
+ }
226
+ if (mergeClaudeConfigGates(existing, seed)) writeJsonAtomic(path, existing);
227
+ }
228
+
229
+ // Parses an existing `.claude.json`. Returns undefined when the file is unparseable JSON or not a
230
+ // plain object, signaling the caller to reseed fully rather than merge into it.
231
+ function readExistingClaudeConfig(path: string): Record<string, unknown> | undefined {
232
+ try {
233
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
234
+ return isRecord(parsed) ? parsed : undefined;
235
+ } catch {
236
+ return undefined;
237
+ }
238
+ }
239
+
240
+ // Idempotently re-applies every key the seed computed onto an already-existing, well-formed config,
241
+ // without touching any sibling key the provider has written since first spawn (session history,
242
+ // other project entries, etc.). Boolean gate keys (hasCompletedOnboarding, bypassPermissionsModeAccepted,
243
+ // and the two workspace-trust flags) are forced to `true` whenever they aren't already `true`, so a
244
+ // stale `false` is promoted the same as a missing key. Non-boolean keys (theme, lastOnboardingVersion)
245
+ // are only filled in when entirely absent, so a value the provider or user has since set is never
246
+ // clobbered. `seed` already reflects each prevention's own gating (e.g. bypassPermissionsModeAccepted
247
+ // is simply absent unless approvalMode is "open"), so a guarded profile is never forced into it here.
248
+ // Returns whether anything changed.
249
+ function mergeClaudeConfigGates(existing: Record<string, unknown>, seed: Record<string, unknown>): boolean {
250
+ let dirty = false;
251
+ for (const [key, value] of Object.entries(seed)) {
252
+ if (key === "projects") continue;
253
+ if (typeof value === "boolean") {
254
+ if (existing[key] !== true) {
255
+ existing[key] = true;
256
+ dirty = true;
257
+ }
258
+ } else if (existing[key] === undefined) {
259
+ existing[key] = value;
260
+ dirty = true;
261
+ }
262
+ }
263
+ if (isRecord(seed.projects)) {
264
+ const existingProjects = isRecord(existing.projects) ? existing.projects : {};
265
+ for (const [projectPath, seedEntry] of Object.entries(seed.projects)) {
266
+ if (!isRecord(seedEntry)) continue;
267
+ const existingEntry = isRecord(existingProjects[projectPath]) ? existingProjects[projectPath] : {};
268
+ for (const [entryKey, entryValue] of Object.entries(seedEntry)) {
269
+ if (typeof entryValue !== "boolean") continue;
270
+ if (existingEntry[entryKey] !== true) {
271
+ existingEntry[entryKey] = true;
272
+ dirty = true;
273
+ }
274
+ }
275
+ existingProjects[projectPath] = existingEntry;
276
+ }
277
+ existing.projects = existingProjects;
278
+ }
279
+ return dirty;
280
+ }
281
+
282
+ // Atomic write: temp file in the SAME directory, then rename(2). A crash or kill mid-write leaves
283
+ // only an orphaned temp file — never a truncated/corrupt `.claude.json` — because rename only ever
284
+ // swaps in a complete file.
285
+ function writeJsonAtomic(path: string, value: unknown): void {
286
+ const tmpPath = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
287
+ writeFileSync(tmpPath, JSON.stringify(value, null, 2), { mode: 0o600 });
288
+ renameSync(tmpPath, path);
289
+ }
290
+
291
+ /**
292
+ * Reads and parses the potentially multi-megabyte host config before launch assembly. The whole
293
+ * operation runs in the bounded worker pool: async file I/O alone would still leave JSON.parse
294
+ * pinning the runner event loop for large host history files.
295
+ */
296
+ export async function hydrateClaudeHostConfig(config: RunnerSpawnConfig, provider: SpawnProvider = "claude"): Promise<RunnerSpawnConfig> {
297
+ const [hostClaudeConfig, hostClaudeSettings] = await Promise.all([
298
+ config.hostClaudeConfig === undefined ? readClaudeHostConfig(provider) : Promise.resolve(config.hostClaudeConfig),
299
+ config.hostClaudeSettings === undefined ? readClaudeHostSettings(provider) : Promise.resolve(config.hostClaudeSettings),
300
+ ]);
301
+ return {
302
+ ...config,
303
+ ...(hostClaudeConfig ? { hostClaudeConfig } : {}),
304
+ ...(hostClaudeSettings ? { hostClaudeSettings } : {}),
305
+ };
306
+ }
307
+
308
+ /**
309
+ * Loads only the seed values and host-scope MCP declaration required by a Claude launch. This
310
+ * shared worker-pool seam gives registration projection and synchronous launch assembly the same
311
+ * snapshot without ever parsing the host's potentially huge config on the runner event loop.
312
+ * #2513 — keyed by provider id so a claude-family provider (claude-b) reads ITS source home.
313
+ */
314
+ export async function readClaudeHostConfig(provider: SpawnProvider = "claude"): Promise<Record<string, unknown> | undefined> {
315
+ const hostPath = join(providerSourceHome(provider), ".claude.json");
316
+ return runnerHostConfigOffload().run<{ path: string }, Record<string, unknown> | undefined>("runner-claude-host-config-read", { path: hostPath });
317
+ }
318
+
319
+ /** Load only the executable hooks portion of host settings.json off-thread. */
320
+ export async function readClaudeHostSettings(provider: SpawnProvider = "claude"): Promise<Record<string, unknown> | undefined> {
321
+ const settingsPath = join(providerSourceHome(provider), "settings.json");
322
+ return runnerHostConfigOffload().run<{ path: string; kind: "settings" }, Record<string, unknown> | undefined>("runner-claude-host-config-read", { path: settingsPath, kind: "settings" });
323
+ }
324
+
325
+ // #662 — the host USER-scope MCP servers: the top-level `mcpServers` of `~/.claude.json`.
326
+ // These are the ambient host MCP a host-base Claude auto-loads on EVERY launch when NOT
327
+ // strict (user scope is global across all projects — callmux/qmd/gh on this host). Since
328
+ // the #662 wedge fix makes every managed launch `--strict-mcp-config` (so Claude ignores
329
+ // all on-disk config and never renders the project-`.mcp.json` enable modal), an
330
+ // `mcp.mode:"host"` profile must re-inject these explicitly into `--mcp-config` to keep
331
+ // honoring host mode. Deliberately NOT the project-cwd `.mcp.json` (project scope — the
332
+ // modal's source, which must stay excluded) nor `projects[cwd].mcpServers` (local scope).
333
+ export function hostUserScopeMcpServers(config: Pick<RunnerSpawnConfig, "hostClaudeConfig">): Record<string, ProvisioningMcpServer> {
334
+ const servers = config.hostClaudeConfig?.mcpServers;
335
+ return isRecord(servers) ? servers as Record<string, ProvisioningMcpServer> : {};
336
+ }
337
+
338
+ function providerHomePath(provider: SpawnProvider, config: RunnerSpawnConfig): string {
339
+ const root = providerHomeRootFromEnv();
340
+ const profileName = sanitizePathPart(config.agentProfile?.name || config.profile || "profile");
341
+ const instance = sanitizePathPart(config.instanceId || config.runnerId);
342
+ return join(root, provider, profileName, instance);
343
+ }
344
+
345
+ function sanitizePathPart(value: string): string {
346
+ return sanitizeFsName(value, { replacement: "-", collapse: false, maxLen: 120, fallback: "profile" });
347
+ }
348
+
349
+ function linkExistingAuthItems(sourceHome: string, targetHome: string, items: string[]): string[] {
350
+ const linked: string[] = [];
351
+ for (const item of items) {
352
+ if (linkExistingPath(join(sourceHome, item), join(targetHome, item))) linked.push(item);
353
+ }
354
+ return linked;
355
+ }
356
+
357
+ function linkExistingPath(source: string, target: string): boolean {
358
+ if (!existsSync(source) || existsSync(target)) return false;
359
+ // #2670 — existsSync follows links; a reaped auth source leaves a dangling entry behind.
360
+ if (lstatSync(target, { throwIfNoEntry: false })?.isSymbolicLink()) unlinkSync(target);
361
+ symlinkSync(source, target);
362
+ return true;
363
+ }
364
+
365
+ function withCodexWorkspaceTrust(current: string, config: RunnerSpawnConfig): string {
366
+ if (!config.cwd) return current;
367
+ const project = tomlBasicString(resolve(config.cwd));
368
+ const section = `[projects.${project}]`;
369
+ const entry = `${section}\ntrust_level = "trusted"\n`;
370
+ if (current.includes(section)) return current;
371
+ const prefix = current.trimEnd();
372
+ return `${prefix ? `${prefix}\n\n` : ""}${entry}`;
373
+ }
374
+
375
+ function withTomlFeatureFlag(input: string, key: string, value: boolean): string {
376
+ const lines = input.length ? input.split("\n") : [];
377
+ const headerIndex = lines.findIndex((line) => line.trim() === "[features]");
378
+ const entry = `${key} = ${value ? "true" : "false"}`;
379
+ if (headerIndex === -1) {
380
+ const prefix = input.trimEnd();
381
+ return `${prefix ? `${prefix}\n\n` : ""}[features]\n${entry}\n`;
382
+ }
383
+
384
+ let sectionEnd = lines.length;
385
+ for (let i = headerIndex + 1; i < lines.length; i += 1) {
386
+ const line = lines[i] ?? "";
387
+ if (/^\s*\[[^\]]+\]\s*(?:#.*)?$/.test(line)) {
388
+ sectionEnd = i;
389
+ break;
390
+ }
391
+ }
392
+
393
+ const flagPattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
394
+ for (let i = headerIndex + 1; i < sectionEnd; i += 1) {
395
+ const line = lines[i] ?? "";
396
+ if (flagPattern.test(line)) {
397
+ if (lines[i] === entry) return input;
398
+ lines[i] = entry;
399
+ return lines.join("\n");
400
+ }
401
+ }
402
+
403
+ lines.splice(headerIndex + 1, 0, entry);
404
+ return lines.join("\n");
405
+ }
406
+
407
+ function escapeRegExp(value: string): string {
408
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
409
+ }
410
+
411
+ // A reused home may still hold the host CLAUDE.md link from an Instructions-on launch: never write
412
+ // through it into the host file.
413
+ function writeClaudeRelayManual(claudeHome: string): void {
414
+ writeFileAtomic(join(claudeHome, "CLAUDE.md"), claudeRelayManual());
415
+ }
416
+
417
+ function tomlBasicString(value: string): string {
418
+ return JSON.stringify(value);
419
+ }