github-router 0.3.276 → 0.3.285

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 (107) hide show
  1. package/dist/{attribution-settings-VOPvSn6v.js → attribution-settings-B8M2fvhz.js} +44 -45
  2. package/dist/attribution-settings-B8M2fvhz.js.map +1 -0
  3. package/dist/{auth-CYoRwhC9.js → auth-VUL2Zxvw.js} +3 -3
  4. package/dist/{auth-CYoRwhC9.js.map → auth-VUL2Zxvw.js.map} +1 -1
  5. package/dist/browser-ext/manifest.json +1 -1
  6. package/dist/{check-usage-DDUyg8ZB.js → check-usage-CcLdFPGr.js} +4 -4
  7. package/dist/{check-usage-DDUyg8ZB.js.map → check-usage-CcLdFPGr.js.map} +1 -1
  8. package/dist/{claude-OTHh7iKz.js → claude-e-JIQGhR.js} +55 -38
  9. package/dist/claude-e-JIQGhR.js.map +1 -0
  10. package/dist/{codex-tCxK30Su.js → codex-DSnVZ9oK.js} +5 -5
  11. package/dist/{codex-tCxK30Su.js.map → codex-DSnVZ9oK.js.map} +1 -1
  12. package/dist/{debug-CJgsxoVw.js → debug-CtzYWxpJ.js} +2 -2
  13. package/dist/{debug-CJgsxoVw.js.map → debug-CtzYWxpJ.js.map} +1 -1
  14. package/dist/engine-DMveEa9x.js +2 -0
  15. package/dist/{gate-discovery-nf7PGgnS.js → gate-discovery-BCFwLm0q.js} +5 -5
  16. package/dist/{gate-discovery-nf7PGgnS.js.map → gate-discovery-BCFwLm0q.js.map} +1 -1
  17. package/dist/{get-copilot-usage-D1OAH0EU.js → get-copilot-usage-B-EDAqQb.js} +2 -2
  18. package/dist/{get-copilot-usage-D1OAH0EU.js.map → get-copilot-usage-B-EDAqQb.js.map} +1 -1
  19. package/dist/hooks.mjs +37870 -0
  20. package/dist/hooks.sha256 +1 -0
  21. package/dist/{internal-artifact-open-BCz9lnq0.js → internal-artifact-open-Dj5Nlq0L.js} +2 -2
  22. package/dist/{internal-artifact-open-BCz9lnq0.js.map → internal-artifact-open-Dj5Nlq0L.js.map} +1 -1
  23. package/dist/{internal-first-mate-guard-7v2CtMA6.js → internal-first-mate-guard-B7p4NttK.js} +1 -1
  24. package/dist/{internal-first-mate-guard-La0tIjTU.js → internal-first-mate-guard-DOgFVki5.js} +5 -4
  25. package/dist/{internal-first-mate-guard-La0tIjTU.js.map → internal-first-mate-guard-DOgFVki5.js.map} +1 -1
  26. package/dist/{internal-plan-review-jRMpHu68.js → internal-plan-review-DRFHET_B.js} +11 -4
  27. package/dist/internal-plan-review-DRFHET_B.js.map +1 -0
  28. package/dist/{internal-prompt-submit-CqWRFCuv.js → internal-prompt-submit-f1LR2P2G.js} +4 -4
  29. package/dist/{internal-prompt-submit-CqWRFCuv.js.map → internal-prompt-submit-f1LR2P2G.js.map} +1 -1
  30. package/dist/{internal-session-bind-BiHWDxuC.js → internal-session-bind-DhZhxJ3T.js} +2 -2
  31. package/dist/{internal-session-bind-BiHWDxuC.js.map → internal-session-bind-DhZhxJ3T.js.map} +1 -1
  32. package/dist/{internal-stop-hook-Ds9LwmZA.js → internal-stop-hook-Cf-7w3RH.js} +68 -20
  33. package/dist/internal-stop-hook-Cf-7w3RH.js.map +1 -0
  34. package/dist/{internal-stop-review-BOFj7R3q.js → internal-stop-review-BBsLcbPG.js} +2 -2
  35. package/dist/{internal-stop-review-BOFj7R3q.js.map → internal-stop-review-BBsLcbPG.js.map} +1 -1
  36. package/dist/{internal-worker-guard-Lu8VHj5k.js → internal-worker-guard-CKgYFiFO.js} +2 -2
  37. package/dist/{internal-worker-guard-Lu8VHj5k.js.map → internal-worker-guard-CKgYFiFO.js.map} +1 -1
  38. package/dist/{internal-workspace-header-BRMz0Yql.js → internal-workspace-header-OYgHEnFt.js} +2 -2
  39. package/dist/{internal-workspace-header-BRMz0Yql.js.map → internal-workspace-header-OYgHEnFt.js.map} +1 -1
  40. package/dist/lib/tree-sitter-pool/lifecycle-C7W8D2lx.js +165 -0
  41. package/dist/lib/tree-sitter-pool/lifecycle-Dx0SnP7Z.js +157 -0
  42. package/dist/lib/tree-sitter-pool/worker.js +287 -14
  43. package/dist/{lifecycle-LA4rAuAL.js → lifecycle-B7CHqKlF.js} +2 -2
  44. package/dist/{lifecycle-LA4rAuAL.js.map → lifecycle-B7CHqKlF.js.map} +1 -1
  45. package/dist/lifecycle-CbmHMMSD.js +2 -0
  46. package/dist/{lifecycle-AGXnd-ZY.js → lifecycle-D-rL81tT.js} +2 -2
  47. package/dist/{lifecycle-AGXnd-ZY.js.map → lifecycle-D-rL81tT.js.map} +1 -1
  48. package/dist/lifecycle-DTcZwa7U.js +2 -0
  49. package/dist/lifecycle-K9oVtGde.mjs +16 -0
  50. package/dist/main.js +18 -18
  51. package/dist/{mcp-workspace-header-CGJbNeHb.js → mcp-workspace-header-ucs2SDST.js} +5 -6
  52. package/dist/mcp-workspace-header-ucs2SDST.js.map +1 -0
  53. package/dist/{models-4Q45Q2Dw.js → models-C16mBK2M.js} +3 -3
  54. package/dist/{models-4Q45Q2Dw.js.map → models-C16mBK2M.js.map} +1 -1
  55. package/dist/{orchestration-CpkG8ScN.js → orchestration-CtM6FYNx.js} +2 -2
  56. package/dist/{orchestration-CpkG8ScN.js.map → orchestration-CtM6FYNx.js.map} +1 -1
  57. package/dist/package-root-B-osctCk.js +29 -0
  58. package/dist/package-root-B-osctCk.js.map +1 -0
  59. package/dist/paths-CV9K7Xqm.js +2 -0
  60. package/dist/{paths-j2B7b0DZ.js → paths-wLC0InjX.js} +28 -4
  61. package/dist/paths-wLC0InjX.js.map +1 -0
  62. package/dist/{peer-mcp-personas-DRL_xT4h.js → peer-mcp-personas-V6stFvpq.js} +1158 -297
  63. package/dist/peer-mcp-personas-V6stFvpq.js.map +1 -0
  64. package/dist/{plan-review-hook-Dk5zTNke.js → plan-review-hook-Lf9ISdF6.js} +5 -6
  65. package/dist/plan-review-hook-Lf9ISdF6.js.map +1 -0
  66. package/dist/{prompt-submit-hook-lvTWwaTV.js → prompt-submit-hook-BlijaOn7.js} +6 -11
  67. package/dist/prompt-submit-hook-BlijaOn7.js.map +1 -0
  68. package/dist/{provision-HzZ547dW.js → provision-BpL6gZIt.js} +4 -4
  69. package/dist/{provision-HzZ547dW.js.map → provision-BpL6gZIt.js.map} +1 -1
  70. package/dist/self-invocation-CKMjcA5F.js +380 -0
  71. package/dist/self-invocation-CKMjcA5F.js.map +1 -0
  72. package/dist/{serve-CM3OmF4I.js → serve-CXUf7RtJ.js} +34 -27
  73. package/dist/serve-CXUf7RtJ.js.map +1 -0
  74. package/dist/{server-setup-C9r7jYnz.js → server-setup-Bppjt9xO.js} +81 -207
  75. package/dist/server-setup-Bppjt9xO.js.map +1 -0
  76. package/dist/{start-KAUCsDao.js → start-C1-jrHfU.js} +3 -3
  77. package/dist/{start-KAUCsDao.js.map → start-C1-jrHfU.js.map} +1 -1
  78. package/dist/{stop-gate-hook-CaUrldLh.js → stop-gate-hook-DgJ6sW8N.js} +100 -52
  79. package/dist/stop-gate-hook-DgJ6sW8N.js.map +1 -0
  80. package/dist/{stop-gate-policy-lAabj_pP.js → stop-gate-policy-DG5yYWGn.js} +2 -2
  81. package/dist/{stop-gate-policy-lAabj_pP.js.map → stop-gate-policy-DG5yYWGn.js.map} +1 -1
  82. package/dist/{token-CxPaBEUS.js → token-CnlB0884.js} +2 -2
  83. package/dist/token-CnlB0884.js.map +1 -0
  84. package/dist/{version-_Q1WpsQp.js → version-C8x2hQrZ.js} +14 -2
  85. package/dist/version-C8x2hQrZ.js.map +1 -0
  86. package/dist/{worker-dispatch-Bj1uYyG9.js → worker-dispatch-zW8Zi69V.js} +11 -7
  87. package/dist/worker-dispatch-zW8Zi69V.js.map +1 -0
  88. package/package.json +2 -2
  89. package/dist/attribution-settings-VOPvSn6v.js.map +0 -1
  90. package/dist/claude-OTHh7iKz.js.map +0 -1
  91. package/dist/engine-BKJKMAzh.js +0 -2
  92. package/dist/internal-plan-review-jRMpHu68.js.map +0 -1
  93. package/dist/internal-stop-hook-Ds9LwmZA.js.map +0 -1
  94. package/dist/lifecycle-DCrKbaIU.js +0 -2
  95. package/dist/lifecycle-KKSTKwd6.js +0 -2
  96. package/dist/mcp-workspace-header-CGJbNeHb.js.map +0 -1
  97. package/dist/paths-DN3Nio42.js +0 -2
  98. package/dist/paths-j2B7b0DZ.js.map +0 -1
  99. package/dist/peer-mcp-personas-DRL_xT4h.js.map +0 -1
  100. package/dist/plan-review-hook-Dk5zTNke.js.map +0 -1
  101. package/dist/prompt-submit-hook-lvTWwaTV.js.map +0 -1
  102. package/dist/serve-CM3OmF4I.js.map +0 -1
  103. package/dist/server-setup-C9r7jYnz.js.map +0 -1
  104. package/dist/stop-gate-hook-CaUrldLh.js.map +0 -1
  105. package/dist/token-CxPaBEUS.js.map +0 -1
  106. package/dist/version-_Q1WpsQp.js.map +0 -1
  107. package/dist/worker-dispatch-Bj1uYyG9.js.map +0 -1
@@ -1,5 +1,6 @@
1
1
  import { r as parseBoolEnv } from "./exec-y8C_MU8A.js";
2
- import { a as isSubagentContext } from "./stop-gate-policy-lAabj_pP.js";
2
+ import { t as buildSelfCommand } from "./self-invocation-CKMjcA5F.js";
3
+ import { a as isSubagentContext } from "./stop-gate-policy-DG5yYWGn.js";
3
4
  import { n as callMcpTool } from "./hook-mcp-client-DBvm9608.js";
4
5
  import { promises } from "node:fs";
5
6
  import path from "node:path";
@@ -148,12 +149,10 @@ async function runPlanReview(input) {
148
149
  }
149
150
  }
150
151
  /** Build the command registered for PostToolUse(ExitPlanMode). */
151
- function buildPlanReviewHookCommand(execPath, scriptPath) {
152
- const q = (s) => `"${s}"`;
153
- if (scriptPath && scriptPath !== execPath) return `${q(execPath)} ${q(scriptPath)} internal-plan-review`;
154
- return `${q(execPath)} internal-plan-review`;
152
+ function buildPlanReviewHookCommand(invocation) {
153
+ return buildSelfCommand(invocation, "internal-plan-review");
155
154
  }
156
155
  //#endregion
157
156
  export { runPlanReview as a, planReviewEnabled as i, decidePlanReviewHook as n, filePlanReviewDebounce as r, buildPlanReviewHookCommand as t };
158
157
 
159
- //# sourceMappingURL=plan-review-hook-Dk5zTNke.js.map
158
+ //# sourceMappingURL=plan-review-hook-Lf9ISdF6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan-review-hook-Lf9ISdF6.js","names":["nodePath","fs"],"sources":["../src/lib/orchestration/plan-review-hook.ts"],"sourcesContent":["import { createHash } from \"node:crypto\"\nimport { promises as fs } from \"node:fs\"\nimport nodePath from \"node:path\"\n\nimport { parseBoolEnv } from \"~/lib/exec\"\nimport { buildSelfCommand, type SelfInvocation } from \"~/lib/hook-launcher/self-invocation\"\n\nimport { callMcpTool, type HookMcpRuntime, type McpToolResult } from \"./hook-mcp-client\"\nimport {\n isSubagentContext,\n type FindingsStore,\n type ReviewDebounce,\n} from \"./stop-gate-policy\"\n\n/** Minimum finalized-plan size worth spending a model review on. */\nexport const PLAN_REVIEW_MIN_CHARS = 220\n\n/** Hard wall-clock for the advisory plan critic. */\nexport const PLAN_REVIEW_TIMEOUT_MS = 25_000\n\n/** Cap the plan embedded in the critic brief so a huge plan file cannot dominate. */\nconst MAX_REVIEWED_PLAN_CHARS = 80 * 1024\n\nexport interface PlanReviewSpawnContext {\n sessionId: string\n cwd: string\n plan: string\n planHash: string\n}\n\nexport type PlanReviewDecision =\n | { kind: \"skip\"; reason: string }\n | ({ kind: \"spawn\" } & PlanReviewSpawnContext)\n\n/** Default-on advisory plan review. Opt out with GH_ROUTER_DISABLE_PLAN_REVIEW=1. */\nexport function planReviewEnabled(env: NodeJS.ProcessEnv = process.env): boolean {\n return parseBoolEnv(env.GH_ROUTER_DISABLE_PLAN_REVIEW) !== true\n}\n\n/** File-backed per-session debounce for finalized plans, separate from Stop diff hashes. */\nexport function filePlanReviewDebounce(stateDir: string): ReviewDebounce {\n const fileFor = (sid: string): string =>\n nodePath.join(stateDir, `plan-review-hash-${createHash(\"sha256\").update(sid).digest(\"hex\").slice(0, 32)}`)\n const readLast = async (sid: string): Promise<string> => {\n try {\n return (await fs.readFile(fileFor(sid), \"utf8\")).trim()\n } catch {\n return \"\"\n }\n }\n return {\n async shouldReview(sid, planHash) {\n if (planHash.length === 0) return false\n return (await readLast(sid)) !== planHash\n },\n async markReviewed(sid, planHash) {\n await fs.mkdir(stateDir, { recursive: true })\n await fs.writeFile(fileFor(sid), planHash, { mode: 0o600 })\n },\n }\n}\n\nfunction parseObject(raw: string): Record<string, unknown> | undefined {\n try {\n const parsed: unknown = JSON.parse(raw)\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) return parsed as Record<string, unknown>\n } catch {\n /* malformed hook payload -> skip */\n }\n return undefined\n}\n\nasync function resolvePlanText(\n toolInput: unknown,\n readFile: (filePath: string) => Promise<string>,\n): Promise<string> {\n if (!toolInput || typeof toolInput !== \"object\") return \"\"\n const ti = toolInput as { planFilePath?: unknown; plan?: unknown }\n const planFilePath = typeof ti.planFilePath === \"string\" && ti.planFilePath.trim().length > 0\n ? ti.planFilePath.trim()\n : \"\"\n if (planFilePath.length > 0) {\n try {\n const fromFile = await readFile(planFilePath)\n if (fromFile.trim().length > 0) return fromFile\n } catch {\n /* fall through to inline plan */\n }\n }\n return typeof ti.plan === \"string\" ? ti.plan : \"\"\n}\n\n/**\n * Decide whether the ExitPlanMode PostToolUse hook should spawn a detached review.\n * Pure aside from injected file reads + debounce store. Fail-open: any malformed\n * payload, missing runtime, subagent context, trivial/absent plan, or store error\n * returns a skip decision.\n */\nexport async function decidePlanReviewHook(input: {\n stdin: string\n runtimeAvailable: boolean\n debounce: ReviewDebounce\n fallbackCwd: string\n minChars?: number\n readFile?: (filePath: string) => Promise<string>\n}): Promise<PlanReviewDecision> {\n try {\n if (!input.runtimeAvailable) return { kind: \"skip\", reason: \"missing-runtime\" }\n const payload = parseObject(input.stdin)\n if (!payload) return { kind: \"skip\", reason: \"bad-payload\" }\n if (isSubagentContext(payload)) return { kind: \"skip\", reason: \"subagent\" }\n\n const sessionId = typeof payload.session_id === \"string\" && payload.session_id.length > 0 ? payload.session_id : \"\"\n if (!sessionId) return { kind: \"skip\", reason: \"missing-session\" }\n const cwd = typeof payload.cwd === \"string\" && payload.cwd.length > 0 ? payload.cwd : input.fallbackCwd\n const plan = await resolvePlanText(\n payload.tool_input,\n input.readFile ?? ((filePath: string) => fs.readFile(filePath, \"utf8\")),\n )\n const trimmed = plan.trim()\n if (trimmed.length < (input.minChars ?? PLAN_REVIEW_MIN_CHARS)) return { kind: \"skip\", reason: \"trivial-plan\" }\n\n const planHash = createHash(\"sha256\").update(trimmed).digest(\"hex\")\n if (!(await input.debounce.shouldReview(sessionId, planHash))) return { kind: \"skip\", reason: \"debounced\" }\n await input.debounce.markReviewed(sessionId, planHash)\n return { kind: \"spawn\", sessionId, cwd, plan: trimmed, planHash }\n } catch {\n return { kind: \"skip\", reason: \"error\" }\n }\n}\n\nfunction reviewBrief(plan: string, cwd: string): string {\n const embeddedPlan =\n plan.length > MAX_REVIEWED_PLAN_CHARS\n ? `${plan.slice(0, MAX_REVIEWED_PLAN_CHARS)}\\n\\n[plan truncated at ${MAX_REVIEWED_PLAN_CHARS} characters]`\n : plan\n return (\n \"You are an independent plan reviewer. A coding agent has just finalized a plan with ExitPlanMode. \"\n + \"Your job is to find load-bearing risks before implementation starts: unstated assumptions, missing \"\n + \"failure modes, gaps in verification, dangerous sequencing, or places where the plan could satisfy the \"\n + \"letter of the request while missing the user's likely intent.\\n\\n\"\n + \"Be concise and skeptical. Do NOT rewrite the plan, do NOT praise it, and do NOT invent new product \"\n + \"requirements. Report only material objections that would change what the agent should do next. Include \"\n + \"file:line anchors when the plan names concrete files or when the risk depends on repo code. If the plan \"\n + \"is sound, say exactly: \\\"no material objection\\\".\\n\\n\"\n + `Workspace: ${cwd}\\n\\n`\n + \"FINALIZED PLAN:\\n\"\n + embeddedPlan\n )\n}\n\nfunction hasMaterialFinding(text: string): boolean {\n const normalized = text.trim().toLowerCase().replace(/[.!]+$/g, \"\")\n return normalized.length > 0 && normalized !== \"no material objection\" && normalized !== \"no material objections\"\n}\n\nfunction framePlanFindings(text: string): string {\n return (\n \"PLAN REVIEW: an independent cross-lab critic reviewed the finalized plan and found \"\n + \"the following material concern(s).\\n\"\n + text.trim()\n )\n}\n\n/** Run the bounded critic call and write material findings to the shared findings store. */\nexport async function runPlanReview(input: {\n runtime: HookMcpRuntime\n sessionId: string\n cwd: string\n plan: string\n findingsStore: FindingsStore\n timeoutMs?: number\n callReview?: (brief: string, signal: AbortSignal) => Promise<McpToolResult>\n}): Promise<void> {\n const timeoutMs = input.timeoutMs ?? PLAN_REVIEW_TIMEOUT_MS\n let timer: ReturnType<typeof setTimeout> | undefined\n const controller = new AbortController()\n try {\n const brief = reviewBrief(input.plan, input.cwd)\n const review = (input.callReview ?? ((prompt: string, signal: AbortSignal) => callMcpTool({\n runtime: input.runtime,\n group: \"peers\",\n tool: \"codex_critic\",\n args: { prompt, effort: \"high\" },\n timeoutMs,\n signal,\n })))(brief, controller.signal)\n review.catch(() => {})\n const raced = await Promise.race<McpToolResult | \"timeout\">([\n review,\n new Promise<\"timeout\">((resolve) => {\n timer = setTimeout(() => resolve(\"timeout\"), timeoutMs)\n }),\n ])\n if (raced === \"timeout\" || raced.isError || !hasMaterialFinding(raced.text)) return\n\n const framed = framePlanFindings(raced.text)\n const existing = await input.findingsStore.read(input.sessionId).catch(() => null)\n const next = existing && existing.trim().length > 0 ? `${existing.trim()}\\n\\n${framed}` : framed\n await input.findingsStore.write(input.sessionId, next)\n } catch {\n /* advisory layer must never surface an error */\n } finally {\n if (timer) clearTimeout(timer)\n controller.abort()\n }\n}\n\n/** Build the command registered for PostToolUse(ExitPlanMode). */\nexport function buildPlanReviewHookCommand(invocation: SelfInvocation): string {\n return buildSelfCommand(invocation, \"internal-plan-review\")\n}\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
- import { a as isSubagentContext } from "./stop-gate-policy-lAabj_pP.js";
1
+ import { t as buildSelfCommand } from "./self-invocation-CKMjcA5F.js";
2
+ import { a as isSubagentContext } from "./stop-gate-policy-DG5yYWGn.js";
2
3
  //#region src/lib/orchestration/prompt-submit-hook.ts
3
4
  /**
4
5
  * Pure decision logic for the `UserPromptSubmit` hook (the front-end of the
@@ -137,17 +138,11 @@ async function decidePromptSubmitV2(input) {
137
138
  ]);
138
139
  return decision;
139
140
  }
140
- /**
141
- * Build the shell command Claude Code runs for the `UserPromptSubmit` hook —
142
- * the running github-router via its node/bun binary so it works regardless of
143
- * PATH. Mirrors `buildStopHookCommand`.
144
- */
145
- function buildPromptSubmitHookCommand(execPath, scriptPath) {
146
- const q = (s) => `"${s}"`;
147
- if (scriptPath && scriptPath !== execPath) return `${q(execPath)} ${q(scriptPath)} internal-prompt-submit`;
148
- return `${q(execPath)} internal-prompt-submit`;
141
+ /** Build the shell command Claude Code runs for the `UserPromptSubmit` hook. */
142
+ function buildPromptSubmitHookCommand(invocation) {
143
+ return buildSelfCommand(invocation, "internal-prompt-submit");
149
144
  }
150
145
  //#endregion
151
146
  export { decidePromptSubmit as n, decidePromptSubmitV2 as r, buildPromptSubmitHookCommand as t };
152
147
 
153
- //# sourceMappingURL=prompt-submit-hook-lvTWwaTV.js.map
148
+ //# sourceMappingURL=prompt-submit-hook-BlijaOn7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt-submit-hook-BlijaOn7.js","names":[],"sources":["../src/lib/orchestration/prompt-submit-hook.ts"],"sourcesContent":["/**\n * Pure decision logic for the `UserPromptSubmit` hook (the front-end of the\n * floor-raising surface). It serves TWO duties on the one event:\n *\n * 1. Per-prompt budget reset — clears the Stop-gate's block counter for this\n * session so `maxBlocks` is \"per user prompt\", not \"per session lifetime\"\n * (the per-prompt scoping the Stop-gate relies on).\n * 2. Goal steer — for a NON-trivial prompt, inject an advisory GOAL directive\n * (additive context, never blocking) pointing the model at `/gh-research`\n * then `/gh-orchestrate`. It raises the prior; it does not force execution.\n *\n * Top-level only: like the Stop-gate, it stands down in a subagent/teammate\n * context (the payload carries `agent_type`/`agent_id` there) so it never\n * recurses into spawned workers. Fail-open and never blocking: any error or a\n * trivial prompt yields an empty injection.\n */\n\nimport { buildSelfCommand, type SelfInvocation } from \"../hook-launcher/self-invocation\"\nimport { isSubagentContext } from \"./stop-gate-policy\"\n\n/**\n * The advisory goal injected for a non-trivial prompt. Uses the skills' slash\n * invocation form. The model still decides whether to follow it; the Stop-gate\n * backstops correctness at the output end.\n */\nexport const PROMPT_STEER_GOAL =\n \"GOAL (advisory): for a non-trivial task, FIRST run /gh-research on this ask to \"\n + \"information saturation — verify the load-bearing claims against the actual code \"\n + \"before planning, and do not plan or write code until research is saturated. THEN, \"\n + \"for an implementation or change task, run /gh-orchestrate to compose and run a \"\n + \"floor-raising workflow (it checkpoints before expensive work). Skip both for a \"\n + \"trivial ask; you may decline if they do not fit.\"\n\n/**\n * Cheap, conservative complexity heuristic — a long prompt, an imperative\n * build/change verb, or an explicit multi-file scope. Trivial prompts get no\n * steer (no analysis-paralysis tax on quick asks).\n */\nexport function isNonTrivialPrompt(prompt: string): boolean {\n const p = prompt.trim()\n if (p.length === 0) return false\n if (p.length >= 280) return true\n const verb =\n /\\b(implement|build|refactor|migrate|fix|debug|diagnose|design|add|create|rewrite|optimi[sz]e|integrate|architect|investigate|audit)\\b/i\n if (verb.test(p)) return true\n // Multi-file / cross-cutting scope signal.\n return /\\b(across|throughout|every|all)\\b.*\\b(file|module|test|route|component)s?\\b/i.test(p)\n}\n\nexport interface PromptSubmitDecision {\n /** Session whose Stop-gate block budget to reset (undefined = none). */\n resetSession?: string\n /** Text to print to stdout (added to the model's context). Empty = nothing. */\n inject: string\n}\n\nexport function decidePromptSubmit(input: { stdin: string; steerEnabled: boolean }): PromptSubmitDecision {\n let payload: { session_id?: unknown; prompt?: unknown; agent_type?: unknown; agent_id?: unknown } = {}\n try {\n const p: unknown = JSON.parse(input.stdin)\n if (p && typeof p === \"object\") payload = p as typeof payload\n } catch {\n return { inject: \"\" }\n }\n // Top-level only: a subagent/teammate prompt gets neither reset nor steer.\n if (isSubagentContext(payload)) return { inject: \"\" }\n const decision: PromptSubmitDecision = { inject: \"\" }\n const sessionId = typeof payload.session_id === \"string\" && payload.session_id.length > 0 ? payload.session_id : \"\"\n if (sessionId) decision.resetSession = sessionId\n const prompt = typeof payload.prompt === \"string\" ? payload.prompt : \"\"\n if (input.steerEnabled && isNonTrivialPrompt(prompt)) decision.inject = PROMPT_STEER_GOAL\n return decision\n}\n\n// ─── UserPromptSubmit V2 (advisory: classify + grounded scope/goal) ──────────\n\n/**\n * Static encouragement injected only for a non-trivial prompt (no extra model\n * call, no latency tax): nudge parallel lexical+semantic search before\n * concluding. Mirrors the v1 advisory tone: additive, never blocking.\n */\nexport const PROMPT_SEARCH_TIP =\n \"TIP (advisory): when this task needs code context, search lexical + semantic in \"\n + \"parallel — one `mcp__search__code` call with mode:\\\"lexical\\\" and one with \"\n + \"mode:\\\"semantic\\\", issued in the same turn — before concluding.\"\n\n/** System prompt for the single gpt-5.6-sol scope/goal inference. Steers a SHORT,\n * user-derived (not invented) advisory note grounded in the search results. */\nexport const PROMPT_SCOPE_SYSTEM =\n \"You are a scoping assistant for a coding agent about to act on a user's request. \"\n + \"You are given the user's request and the results of a lexical + semantic code search \"\n + \"over the relevant repository. Produce a SHORT advisory note (<= 120 words), plain text only:\\n\"\n + \"1. SCOPE: one line — is this trivial, focused (one area), or large/cross-cutting — grounded in \"\n + \"what the search surfaced (reference the most relevant file(s) by name).\\n\"\n + \"2. GOAL: restate the user's OWN ask as a single measurable objective, in THEIR terms. Do NOT \"\n + \"invent new requirements or acceptance criteria beyond what they asked.\\n\"\n + \"3. Only if the task is large/cross-cutting, add a final line: \\\"Consider /gh-research first to \"\n + \"saturate understanding, then /gh-orchestrate to compose a floor-raising workflow.\\\" Omit it for a \"\n + \"focused or trivial task.\\n\"\n + \"This is advisory — the agent decides whether to follow it. Be concrete and concise; no preamble.\"\n\n/** Injected IO for V2, all best-effort. Each network call returns its text and\n * never throws to the orchestrator (the orchestrator wraps the enrichment in a\n * timeout + try/catch and falls open to the regex goal).\n *\n * INVARIANT for live callers: each IO call MUST self-timeout (the live wiring\n * passes a per-call timeoutMs to callMcpTool / callInference). The orchestrator's\n * outer `timeoutMs` race only bounds how long it WAITS for a result before\n * failing open — it does NOT abort the in-flight call. A non-self-timing-out IO\n * would leave a socket open until the hook process's terminal `process.exit(0)`,\n * which is the hard backstop but not a substitute for the per-call bound. */\nexport interface PromptSubmitV2IO {\n /** One `mcp__search__code` call; returns the raw result text (\"\" on failure).\n * Receives the orchestrator's AbortSignal so a timed-out enrichment cancels\n * the in-flight request (live callers thread it into the HTTP fetch). */\n searchCode: (query: string, mode: \"lexical\" | \"semantic\", signal?: AbortSignal) => Promise<string>\n /** One gpt-5.6-sol `/v1/responses` inference; returns assistant text (\"\" on failure).\n * Receives the orchestrator's AbortSignal (see `searchCode`). */\n infer: (system: string, user: string, signal?: AbortSignal) => Promise<string>\n /** Pending advisory findings from the prior turn's background review. */\n readFindings: (sessionId: string) => Promise<string | null>\n clearFindings: (sessionId: string) => Promise<void>\n /** Stash this prompt so the next Stop review can judge the diff against it. */\n storePrompt: (sessionId: string, prompt: string) => Promise<void>\n /** Wall-clock budget for the substantive-prompt enrichment (search+infer);\n * on overrun the decision falls open to the regex goal. Default 22s. */\n timeoutMs?: number\n}\n\n/** Max chars of each search-result blob fed into the scope inference. */\nconst SEARCH_CONTEXT_CAP = 6 * 1024\n\n/** Wrap the prior-turn review findings in an explicitly NON-AUTHORITATIVE frame. */\nfunction framePendingFindings(findings: string): string {\n return (\n \"ADVISORY — independent review of your PREVIOUS change (NON-AUTHORITATIVE): an independent \"\n + \"gpt-5.6-sol reviewer flagged the following. Evaluate each on its merits — fix the real ones, and \"\n + \"ignore any wrong one with a one-line reason. You are NOT obligated to act on these.\\n\"\n + findings.trim()\n )\n}\n\nfunction joinSections(sections: Array<string>): string {\n return sections\n .map((s) => s.trim())\n .filter((s) => s.length > 0)\n .join(\"\\n\\n\")\n}\n\n/**\n * V2 decision: budget reset (via resetSession) + a grounded, user-derived scope\n * note + surfaced prior-turn findings. ASYNC and IO-driven, but every IO is\n * best-effort and the substantive enrichment is timeout-bounded with a fail-open\n * to the v1 regex goal — so this never blocks and never wedges the prompt.\n *\n * - subagent/teammate -> empty (top-level only, like v1).\n * - findings -> always surfaced (+ cleared) regardless of triviality.\n * - trivial prompt -> findings only (no search tip, no model call).\n * - substantive prompt -> static search tip + parallel lexical+semantic search -> ONE gpt-5.6-sol call\n * -> grounded scope/goal note. Fail-open to PROMPT_STEER_GOAL.\n * - steerEnabled=false -> findings only (no goal/tip).\n */\nexport async function decidePromptSubmitV2(input: {\n stdin: string\n steerEnabled: boolean\n io: PromptSubmitV2IO\n}): Promise<PromptSubmitDecision> {\n let payload: { session_id?: unknown; prompt?: unknown; agent_type?: unknown; agent_id?: unknown } = {}\n try {\n const p: unknown = JSON.parse(input.stdin)\n if (p && typeof p === \"object\") payload = p as typeof payload\n } catch {\n return { inject: \"\" }\n }\n if (isSubagentContext(payload)) return { inject: \"\" }\n\n const decision: PromptSubmitDecision = { inject: \"\" }\n const sessionId = typeof payload.session_id === \"string\" && payload.session_id.length > 0 ? payload.session_id : \"\"\n if (sessionId) decision.resetSession = sessionId\n const prompt = typeof payload.prompt === \"string\" ? payload.prompt : \"\"\n\n // Stash the prompt for the next Stop review (best-effort, fire-and-await so a\n // slow disk doesn't matter — it's a tiny local write).\n if (sessionId) await input.io.storePrompt(sessionId, prompt).catch(() => {})\n\n // Read + clear any pending findings from the prior turn's background review.\n let findingsBlock = \"\"\n if (sessionId) {\n const pending = await input.io.readFindings(sessionId).catch(() => null)\n if (pending && pending.trim().length > 0) {\n findingsBlock = framePendingFindings(pending)\n await input.io.clearFindings(sessionId).catch(() => {})\n }\n }\n\n // steer disabled -> findings only.\n if (!input.steerEnabled) {\n decision.inject = findingsBlock\n return decision\n }\n\n // Trivial prompt -> no search tip and no model call; surface findings only.\n if (!isNonTrivialPrompt(prompt)) {\n decision.inject = findingsBlock\n return decision\n }\n\n // Substantive prompt -> static search tip + grounded enrichment, timeout-bounded + fail-open.\n const timeoutMs = input.io.timeoutMs ?? 22_000\n let goal = PROMPT_STEER_GOAL // fail-open default.\n let timer: ReturnType<typeof setTimeout> | undefined\n // One controller cancels BOTH search calls + the inference when the race ends\n // (timeout OR success), so a lost-race fetch never keeps the short-lived hook\n // process alive past its decision. Aborting after success is a harmless no-op.\n const controller = new AbortController()\n try {\n const enrich = (async (): Promise<string> => {\n const [lexical, semantic] = await Promise.all([\n input.io.searchCode(prompt, \"lexical\", controller.signal).catch(() => \"\"),\n input.io.searchCode(prompt, \"semantic\", controller.signal).catch(() => \"\"),\n ])\n const searchContext =\n `Lexical search results:\\n${lexical.slice(0, SEARCH_CONTEXT_CAP)}\\n\\n`\n + `Semantic search results:\\n${semantic.slice(0, SEARCH_CONTEXT_CAP)}`\n const note = await input.io.infer(\n PROMPT_SCOPE_SYSTEM,\n `USER REQUEST:\\n${prompt}\\n\\n${searchContext}`,\n controller.signal,\n )\n return note.trim()\n })()\n // Mark `enrich` handled so a post-race rejection (e.g. the inference aborting\n // after the timeout already won) can't surface as an unhandled rejection.\n enrich.catch(() => {})\n // The timer is cleared in `finally` (so when `enrich` wins, no live timer\n // lingers for the rest of `timeoutMs`).\n const raced = await Promise.race<string | \"__timeout__\">([\n enrich,\n new Promise<\"__timeout__\">((resolve) => {\n timer = setTimeout(() => resolve(\"__timeout__\"), timeoutMs)\n }),\n ])\n if (raced !== \"__timeout__\" && raced.length > 0) goal = raced\n } catch {\n // keep the fail-open regex goal.\n } finally {\n if (timer) clearTimeout(timer)\n controller.abort()\n }\n\n decision.inject = joinSections([PROMPT_SEARCH_TIP, goal, findingsBlock])\n return decision\n}\n\n/** Build the shell command Claude Code runs for the `UserPromptSubmit` hook. */\nexport function buildPromptSubmitHookCommand(invocation: SelfInvocation): string {\n return buildSelfCommand(invocation, \"internal-prompt-submit\")\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
- import { D as TOOLBELT_TOOLS, Dt as extractTarGzMember, E as vscodeRipgrepPath, Kt as withInstallLock, Nt as collapsePathKeys, O as assetFor, Ot as extractZipMember, T as toolbeltSkipSet, k as satisfiesMinVersion, w as toolbeltEnabled } from "./peer-mcp-personas-DRL_xT4h.js";
2
- import { t as PATHS } from "./paths-j2B7b0DZ.js";
1
+ import { A as satisfiesMinVersion, Bt as collapsePathKeys, D as vscodeRipgrepPath, E as toolbeltSkipSet, Nt as extractTarGzMember, O as TOOLBELT_TOOLS, Pt as extractZipMember, T as toolbeltEnabled, an as withInstallLock, k as assetFor } from "./peer-mcp-personas-V6stFvpq.js";
2
+ import { t as PATHS } from "./paths-wLC0InjX.js";
3
3
  import { a as resolveExecutable, o as runCommandCapture, r as parseBoolEnv, t as killChildProcessTree } from "./exec-y8C_MU8A.js";
4
- import { a as sweepRegistry } from "./lifecycle-LA4rAuAL.js";
4
+ import { a as sweepRegistry } from "./lifecycle-B7CHqKlF.js";
5
5
  import consola from "consola";
6
6
  import { existsSync } from "node:fs";
7
7
  import path from "node:path";
@@ -678,4 +678,4 @@ async function exposedCommands(binDir) {
678
678
  //#endregion
679
679
  export { STRIPPED_AUTH_ROUTING_ENV_KEYS as a, launchChild as i, buildCodexProviderConfigFlags as n, getCodexVersion as r, provisionToolbelt as t };
680
680
 
681
- //# sourceMappingURL=provision-HzZ547dW.js.map
681
+ //# sourceMappingURL=provision-BpL6gZIt.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"provision-HzZ547dW.js","names":[],"sources":["../src/lib/process-guard/index.ts","../src/lib/stripped-env-keys.ts","../src/lib/launch.ts","../src/lib/toolbelt/provision.ts"],"sourcesContent":["/**\n * Crash-safe process guard (POSIX only).\n *\n * If the proxy dies WITHOUT running its own shutdown handlers (hard crash,\n * SIGKILL, OOM), nothing in `cleanup()` runs to reap the launched CLI's\n * process tree. This guard is the last-resort net for that case.\n *\n * Platform split:\n * - **Windows: no guard needed.** Node assigns each child to a Job\n * Object with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`, so when the proxy\n * dies (any cause) the OS tears down the whole descendant tree —\n * verified: force-killing a Node parent kills child AND grandchild.\n * `startProcessGuard` is a no-op on win32.\n * - **POSIX: a detached `node -e` reaper.** There is no job-object\n * equivalent (a killed parent's children reparent to init), so we\n * spawn a detached watchdog that learns of proxy death via an inherited\n * stdin PIPE EOF (the proxy holds the write end; the OS closes it on\n * proxy death) and reaps the CLI's process group.\n *\n * Never kill the WRONG process: if the child's PID/PGID was recycled after\n * the child exited, a blind `kill(-pid)` could hit an innocent group. The\n * reaper snapshots the child's process start-time at startup and, on proxy\n * death, kills ONLY if the live PID's start-time still matches. A mismatch\n * / unreadable probe → skip (fail-safe). We never trade an orphan-leak for\n * killing the user's work.\n *\n * The reaper is detached + unref'd so it survives the proxy's SIGKILL, and\n * self-exits once it has acted (or after a 24h cap) so it can never become\n * the orphan it exists to prevent. Opt out with\n * `GH_ROUTER_DISABLE_PROCESS_GUARD=1`.\n */\n\nimport { spawn, type ChildProcess } from \"node:child_process\"\nimport process from \"node:process\"\n\nimport { parseBoolEnv } from \"../exec\"\n\n/**\n * Live reaper children, held so the GC can't collect the `ChildProcess`\n * (and with it the stdin write-end that is the proxy-death signal) while\n * the proxy runs. Entries self-remove on the reaper's exit. We do NOT\n * proactively kill these on graceful shutdown: when the proxy exits the OS\n * closes the stdin pipe, the reaper hits EOF, re-verifies the child's\n * identity, and reaps any survivor that ignored the graceful SIGTERM —\n * exactly the backstop we want.\n */\nconst _activeReapers = new Set<ChildProcess>()\n\n/** Guard is on by default; `GH_ROUTER_DISABLE_PROCESS_GUARD=1` opts out. */\nexport function processGuardEnabled(): boolean {\n return parseBoolEnv(process.env.GH_ROUTER_DISABLE_PROCESS_GUARD) !== true\n}\n\n/**\n * Build the start-time-verified node reaper script (POSIX). PURE — `pid`\n * is our own integer. `detachedGroup` selects `kill(-pid)` (process group,\n * the detached CLI) vs `kill(pid)` on POSIX.\n *\n * Kept dependency-free (require'd builtins only) so it runs under `node -e`\n * with no module resolution against the dist bundle.\n */\nexport function buildNodeReaperScript(\n pid: number,\n detachedGroup: boolean,\n): string {\n const target = detachedGroup ? \"-PID\" : \"PID\"\n return `\nconst cp = require(\"node:child_process\");\nconst fs = require(\"node:fs\");\nconst PID = ${pid >>> 0};\nfunction startTime() {\n try {\n if (process.platform === \"linux\") {\n const stat = fs.readFileSync(\"/proc/\" + PID + \"/stat\", \"utf8\");\n const post = stat.slice(stat.lastIndexOf(\")\") + 1).trim().split(/\\\\s+/);\n return post[19] || null; // field 22 (starttime) = index 19 after comm\n }\n const out = cp.execFileSync(\"ps\", [\"-o\",\"lstart=\",\"-p\",String(PID)],\n { stdio: [\"ignore\",\"pipe\",\"ignore\"], timeout: 2000 }).toString().trim();\n return out || null;\n } catch { return null; }\n}\nfunction alive() { try { process.kill(PID, 0); return true; } catch { return false; } }\nfunction treeKill() {\n try { process.kill(${target}, \"SIGTERM\"); } catch {}\n // NOT unref'd: this must keep the loop alive to deliver the escalated\n // SIGKILL (the onDeath exit timer below stays alive past 500ms too).\n setTimeout(() => { try { process.kill(${target}, \"SIGKILL\"); } catch {} }, 500);\n}\nconst snap = (() => {\n // The child is definitely ours and alive at startup, so a null here is a\n // transient probe failure (e.g. a momentary 'ps' hiccup), NOT a real\n // identity loss. Retry a few times so a one-off failure can't silently\n // disable the guard for the rest of the run.\n for (let i = 0; i < 3; i++) { const s = startTime(); if (s !== null) return s; }\n return null;\n})();\nlet done = false;\nfunction onDeath() {\n if (done) return; done = true;\n // Re-verify identity: kill ONLY if the live PID is still our child.\n if (snap !== null && alive() && startTime() === snap) treeKill();\n // REF'd (NOT unref'd): stdin has closed, so an unref'd timer would let\n // the loop empty and the process exit immediately — dropping the 500ms\n // SIGKILL escalation. Keep the loop alive to deliver it, then exit.\n setTimeout(() => process.exit(0), 1500);\n}\nprocess.stdin.resume();\nprocess.stdin.on(\"end\", onDeath);\nprocess.stdin.on(\"close\", onDeath);\nprocess.stdin.on(\"error\", onDeath);\nconst cap = setTimeout(() => process.exit(0), 24*3600*1000); if (cap.unref) cap.unref();\n`.trim()\n}\n\n/**\n * Spawn the detached node reaper holding the stdin death-pipe. Always\n * `detached` so it outlives a force-kill of the proxy, and `unref`'d so it\n * never holds the proxy's event loop open. Returns null on spawn failure.\n */\nfunction spawnNodeReaper(pid: number, detachedGroup: boolean): ChildProcess | null {\n try {\n const child = spawn(\n process.execPath,\n [\"-e\", buildNodeReaperScript(pid, detachedGroup)],\n {\n stdio: [\"pipe\", \"ignore\", \"ignore\"],\n detached: true,\n windowsHide: true,\n shell: false,\n },\n )\n child.on(\"error\", () => {})\n child.stdin?.on(\"error\", () => {})\n // Hold a reference so the GC can't drop the ChildProcess (and its stdin\n // write-end, the proxy-death signal) mid-run; drop it on reaper exit.\n _activeReapers.add(child)\n child.once(\"exit\", () => _activeReapers.delete(child))\n child.unref()\n return child\n } catch {\n return null\n }\n}\n\n/**\n * Start the crash-safe guard for a launched CLI child. No-op on Windows\n * (Node's Job Object already reaps the tree on proxy death) and when\n * disabled / unspawnable. Fire-and-forget; never throws.\n */\nexport function startProcessGuard(child: ChildProcess): void {\n if (!processGuardEnabled()) return\n const pid = child.pid\n if (!pid) return\n // Windows: the runtime's KILL_ON_JOB_CLOSE job object is the crash net.\n if (process.platform === \"win32\") return\n // POSIX: the CLI is spawned detached (own group), so reap the group.\n spawnNodeReaper(pid, true)\n}\n","/**\n * Auth / routing / remote / wire-fingerprint environment keys that must never\n * reach a github-router-spawned Claude or Codex child — through ANY channel.\n *\n * Two channels carry these keys to a spawned agent, and both must be defended:\n * 1. The PARENT PROCESS ENV — a user's shell exports. Stripped by\n * `sanitizeParentEnv` (`src/lib/launch.ts`) before the proxy's overrides.\n * 2. The serve mirror `settings.json` `env` BLOCK — a user's own\n * `~/.claude/settings.json`, snapshot-mirrored and applied to the\n * SDK-spawned claude via `settingSources`. Stripped by\n * `sanitizeServeSettingsEnv` (`src/lib/mcp-permissions-settings.ts`).\n *\n * If any of these survived, a Bedrock/Vertex/gateway user's config would\n * re-route the session OFF the github-router proxy, inject real auth that wins\n * over the synthetic `.credentials.json`, or activate Claude Code's\n * unimplemented Bridge/remote code path. `CLAUDE_CODE_COORDINATOR_MODE` (a\n * toolset-stripper) is NOT here — it's serve-specific, added to the serve list\n * directly. `ANTHROPIC_SMALL_FAST_MODEL` is deliberately NOT stripped (users\n * legitimately rely on it; `resolveModel` translates unrecognized values).\n */\nexport const STRIPPED_AUTH_ROUTING_ENV_KEYS = [\n // Claude Code auth surface\n \"ANTHROPIC_API_KEY\",\n \"ANTHROPIC_AUTH_TOKEN\",\n \"ANTHROPIC_BASE_URL\",\n \"ANTHROPIC_CUSTOM_HEADERS\",\n \"ANTHROPIC_MODEL\",\n \"CLAUDE_CODE_OAUTH_TOKEN\",\n // Alternate OAuth source (loads from an open file descriptor); stripping\n // prevents a third auth source alongside the synthetic .credentials.json.\n \"CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR\",\n \"CLAUDE_CODE_USE_BEDROCK\",\n \"CLAUDE_CODE_USE_VERTEX\",\n \"CLAUDE_CODE_USE_FOUNDRY\",\n // Defense-in-depth: a parent- or settings-set CLAUDE_CONFIG_DIR would\n // redirect the session's config dir away from the router-owned mirror.\n \"CLAUDE_CONFIG_DIR\",\n // Claude Code Bridge / IDE remote-session surface — activates a remote code\n // path (POST /v1/code/sessions, /v1/environments/bridge, …) the proxy does\n // not implement. Stripping forces a local-only session.\n \"CLAUDE_BRIDGE_OAUTH_TOKEN\",\n \"CLAUDE_BRIDGE_BASE_URL\",\n \"CLAUDE_BRIDGE_SESSION_INGRESS_URL\",\n \"SESSION_INGRESS_URL\",\n \"CLAUDE_CODE_REMOTE\",\n \"CLAUDE_CODE_CONTAINER_ID\",\n \"CLAUDE_CODE_REMOTE_SESSION_ID\",\n \"CLAUDE_CODE_SESSION_ID\",\n // Emits `x-anthropic-additional-protection` — pure wire-fingerprint noise\n // that breaks the VS Code stealth posture.\n \"CLAUDE_CODE_ADDITIONAL_PROTECTION\",\n // Codex CLI auth surface\n \"OPENAI_API_KEY\",\n \"OPENAI_BASE_URL\",\n \"CODEX_HOME\",\n // ai-or-die per-tab session-bind / artifact-review surface — must not leak\n // into a nested launch and hijack the parent tab's sidecar / bearer token.\n \"AIORDIE_CLAUDE_BIND\",\n \"AIORDIE_TOKEN\",\n \"AIORDIE_INSECURE_TLS\",\n] as const\n","import { execFileSync, spawn, type ChildProcess } from \"node:child_process\"\nimport { existsSync } from \"node:fs\"\nimport path from \"node:path\"\nimport process from \"node:process\"\n\nimport consola from \"consola\"\n\nimport type { Server } from \"srvx\"\n\nimport { resolveExecutable, killChildProcessTree } from \"./exec\"\nimport { DEFAULT_CODEX_MODEL } from \"./port\"\nimport { startProcessGuard } from \"./process-guard\"\nimport { STRIPPED_AUTH_ROUTING_ENV_KEYS } from \"./stripped-env-keys\"\nimport { collapsePathKeys } from \"./toolbelt/path-inject\"\nimport { sweepRegistry } from \"./worker-agent/lifecycle\"\n\n/**\n * Auth-related env keys we strip from the parent before spawning the child CLI.\n * The proxy provides its own values for everything we care about, so the child\n * behaves as if the user had no parent-env auth at all. The canonical list lives\n * in `./stripped-env-keys` (shared with serve's settings.json `env` strip).\n *\n * Why strip rather than override-with-empty-string: Claude Code emits \"Auth\n * conflict\" warnings whenever both ANTHROPIC_AUTH_TOKEN and ANTHROPIC_API_KEY\n * are present (any value); stripping suppresses it AND prevents an inherited\n * real key from leaking. Cloud-provider toggles + OAuth tokens are simpler\n * dropped than overridden — a missing var is unambiguously absent everywhere.\n */\nconst STRIPPED_PARENT_ENV_KEYS = STRIPPED_AUTH_ROUTING_ENV_KEYS\n\n/**\n * Strip auth-related keys from a parent-process env object. The result\n * is suitable to spread into a spawned child's env BEFORE the proxy's\n * explicit overrides, so the proxy is the only source of truth for\n * auth — and stale shell exports can't leak through.\n */\nexport function sanitizeParentEnv(\n parent: NodeJS.ProcessEnv,\n): NodeJS.ProcessEnv {\n const sanitized: NodeJS.ProcessEnv = { ...parent }\n for (const key of STRIPPED_PARENT_ENV_KEYS) {\n delete sanitized[key]\n }\n return sanitized\n}\n\nfunction commandExists(name: string): boolean {\n try {\n execFileSync(process.platform === \"win32\" ? \"where.exe\" : \"which\", [name], {\n stdio: \"ignore\",\n })\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Whether the launcher can execute `executable`.\n *\n * `buildLaunchCommand` resolves the CLI to an ABSOLUTE path (anti-shadow).\n * `where.exe` (and POSIX `which`) reject a full path argument — `where`\n * returns \"Could not find files for the given pattern(s)\" for an absolute\n * path even when the file exists — so the where/which probe is only valid\n * for bare command names. For an absolute path (already resolved against\n * PATH and existence-checked by `resolveExecutable`), check the\n * filesystem directly. Without this split, every launch where the CLI is\n * installed fails with a spurious \"not found on PATH\".\n */\nexport function isExecutableAvailable(executable: string): boolean {\n if (path.isAbsolute(executable)) return existsSync(executable)\n return commandExists(executable)\n}\n\n/**\n * Provider-config flags (`-c model_providers.github_router=...`) that\n * point Codex at our proxy. Extracted from `buildCodexCmd` so the new\n * `codex mcp-server` MCP-config builder can reuse the exact same\n * provider definition — drift between the two paths would silently\n * break the MCP wiring.\n */\nexport function buildCodexProviderConfigFlags(serverUrl: string): Array<string> {\n return [\n \"-c\",\n `model_providers.github_router={name=\"github-router\",base_url=\"${serverUrl}/v1\",wire_api=\"responses\",env_key=\"OPENAI_API_KEY\"}`,\n \"-c\",\n \"model_provider=github_router\",\n ]\n}\n\n/**\n * Inspect the installed `codex` binary. Used by the codex-MCP wiring\n * in `claude.ts` to gate `--codex-cli`. Codex 0.129.0 introduced the\n * `mcp-server` subcommand; older versions don't expose it, so we\n * downgrade to the HTTP backend with a warning.\n */\nexport function getCodexVersion(): { ok: boolean; version?: string } {\n if (!commandExists(\"codex\")) return { ok: false }\n let raw: string\n try {\n raw = execFileSync(\"codex\", [\"--version\"], {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim()\n } catch {\n return { ok: false }\n }\n // Output examples: \"codex-cli 0.129.0\", \"codex 0.130.1-dev\"\n const m = /(\\d+)\\.(\\d+)\\.(\\d+)/.exec(raw)\n if (!m) return { ok: false, version: raw }\n const major = Number.parseInt(m[1], 10)\n const minor = Number.parseInt(m[2], 10)\n const version = `${m[1]}.${m[2]}.${m[3]}`\n // mcp-server requires codex >= 0.129.0\n const ok = major > 0 || (major === 0 && minor >= 129)\n return { ok, version }\n}\n\nexport interface LaunchTarget {\n kind: \"claude-code\" | \"codex\"\n envVars: Record<string, string>\n extraArgs: string[]\n model?: string\n /**\n * Proxy URL the spawned child should target. Required for Codex 0.129+\n * which stopped honoring OPENAI_BASE_URL and now needs an explicit\n * `-c model_providers.<name>.base_url=...` argument. Set by the codex\n * subcommand from the same `serverUrl` it computed for env vars.\n */\n serverUrl?: string\n}\n\n/**\n * Codex 0.129.0 broke two things the launcher had been relying on:\n * (1) `--full-auto` was removed in favor of `--sandbox` + `--ask-for-approval`;\n * passing it now exits the child immediately with\n * `error: unexpected argument '--full-auto' found`.\n * (2) `OPENAI_BASE_URL` is silently ignored — Codex hardcodes\n * `https://api.openai.com/v1/responses` and 401s out without an\n * explicit `-c model_providers.<name>.base_url` override.\n *\n * `buildCodexCmd` builds the launch argv that works on Codex 0.129+ while\n * still being compatible with older versions that accept the same flags.\n */\nfunction buildCodexCmd(target: LaunchTarget): string[] {\n const cmd: string[] = [\"codex\"]\n if (target.serverUrl) {\n cmd.push(...buildCodexProviderConfigFlags(target.serverUrl))\n }\n cmd.push(\n \"--sandbox\",\n \"workspace-write\",\n \"--ask-for-approval\",\n \"on-request\",\n \"-m\",\n target.model ?? DEFAULT_CODEX_MODEL,\n ...target.extraArgs,\n )\n return cmd\n}\n\nexport function buildLaunchCommand(target: LaunchTarget): {\n cmd: string[]\n env: Record<string, string | undefined>\n} {\n // F10: don't force `--dangerously-skip-permissions` when the caller already\n // requested an explicit `--permission-mode` (e.g. ai-or-die's claude-bridge\n // appends one for a fleet create_session with permissionMode). The two flags\n // conflict — claude rejects them together — and the explicit mode must win.\n const wantsPermissionMode =\n target.kind === \"claude-code\" &&\n target.extraArgs.some((arg) => arg === \"--permission-mode\" || arg.startsWith(\"--permission-mode=\"))\n const cmd: string[] =\n target.kind === \"claude-code\"\n ? wantsPermissionMode\n ? [\"claude\", ...target.extraArgs]\n : [\"claude\", \"--dangerously-skip-permissions\", ...target.extraArgs]\n : buildCodexCmd(target)\n\n // Anti-shadow: resolve the top-level CLI to an ABSOLUTE path against\n // the clean parent PATH (excluding the cwd). The spawned child's env\n // prepends the toolbelt bin dir to PATH; without this, a stray\n // `claude.cmd`/`codex.cmd` in that dir — or in an untrusted repo's cwd\n // (Windows resolves cwd before PATH under shell:true) — could shadow\n // the real CLI. Resolving here means the toolbelt PATH only affects\n // the agent's OWN tool lookups, never which CLI we launch.\n const resolved = resolveExecutable(cmd[0], { env: process.env })\n if (resolved) cmd[0] = resolved\n\n const env = collapsePathKeys({\n ...sanitizeParentEnv(process.env),\n ...target.envVars,\n })\n return { cmd, env }\n}\n\n/**\n * Whether a resolved Windows executable must be launched through cmd.exe\n * (`shell:true`). Only batch shims (`.cmd`/`.bat`) need it — and even then\n * cmd.exe stays alive as the CLI's parent, so `taskkill /T` reaps the\n * whole tree. A real `.exe` (e.g. the native-installer `claude.exe`) is\n * spawned DIRECTLY so the CLI is the direct child, with no cmd.exe\n * intermediary to orphan its node/MCP grandchildren on a kill.\n */\nexport function windowsLaunchNeedsShell(executable: string): boolean {\n const ext = path.extname(executable).toLowerCase()\n return ext === \".cmd\" || ext === \".bat\"\n}\n\nexport function launchChild(\n target: LaunchTarget,\n server: Server,\n options: { onShutdown?: () => Promise<void> | void } = {},\n): void {\n const { cmd, env } = buildLaunchCommand(target)\n\n const executable = cmd[0]\n if (!isExecutableAvailable(executable)) {\n const msg = `\"${executable}\" not found on PATH. Install it first, then try again.`\n consola.error(msg)\n process.stderr.write(msg + \"\\n\")\n process.exit(1)\n }\n\n let child: ChildProcess\n try {\n if (process.platform === \"win32\") {\n if (windowsLaunchNeedsShell(cmd[0])) {\n // A batch shim genuinely needs cmd.exe. cmd.exe stays alive as the\n // CLI's parent for the shim's lifetime, so the tree-kill (taskkill\n // /T) and the crash guard both reap the whole tree through it. Use\n // the full command as a single string to avoid the DEP0190\n // deprecation warning about shell + args.\n const quoted = cmd.map((a) => (a.includes(\" \") ? `\"${a}\"` : a)).join(\" \")\n child = spawn(quoted, [], {\n env,\n stdio: \"inherit\",\n shell: true,\n })\n } else {\n // A real .exe (e.g. the native-installer claude.exe) needs no\n // shell. Spawning it directly makes the CLI the DIRECT child, so\n // child.kill()/taskkill and the crash guard target the real\n // process instead of a cmd.exe wrapper that would orphan its\n // node/MCP grandchildren.\n child = spawn(cmd[0], cmd.slice(1), {\n env,\n stdio: \"inherit\",\n })\n }\n } else {\n // detached:true → the CLI leads its OWN process group, so cleanup()\n // can kill(-pgid) the whole tree (grandchildren included). We still\n // await its exit, so we do NOT unref(). Terminal Ctrl-C no longer\n // reaches the child directly — the signal handlers below forward it.\n child = spawn(cmd[0], cmd.slice(1), {\n env,\n stdio: \"inherit\",\n detached: true,\n })\n }\n } catch (error) {\n const msg = `Failed to launch ${executable}: ${error instanceof Error ? error.message : String(error)}`\n consola.error(msg)\n process.stderr.write(msg + \"\\n\")\n server.close(true).catch(() => {})\n if (options.onShutdown) {\n void Promise.resolve(options.onShutdown()).catch(() => {})\n }\n process.exit(1)\n }\n\n // Crash-safe net: if the proxy dies WITHOUT running cleanup() (hard\n // crash, SIGKILL/taskkill, OOM). On Windows this is the OS's job —\n // Node's KILL_ON_JOB_CLOSE job object reaps the tree — so this is a\n // no-op there. On POSIX (no such mechanism) it spawns a detached reaper\n // that sees the proxy's stdin-pipe EOF, re-verifies the child's\n // identity, and reaps the group (also backstopping a graceful shutdown\n // where the child ignores SIGTERM and the proxy exits before the\n // escalation lands). Fire-and-forget; self-acts and self-exits.\n startProcessGuard(child)\n\n let cleaned = false\n let exiting = false\n async function cleanup(): Promise<void> {\n if (cleaned) return\n cleaned = true\n\n // Arm the hard fail-safe FIRST, before any (potentially blocking) kill\n // or async teardown, so a wedged taskkill / server.close can't hang\n // shutdown indefinitely.\n const timeout = setTimeout(() => process.exit(1), 5000)\n\n // Tree-kill the whole CLI subtree, not just the direct child. On\n // Windows taskkill /T reaps grandchildren; on POSIX kill(-pgid) reaps\n // the detached process group. (Replaces a plain child.kill() that\n // orphaned node/MCP grandchildren on Windows.) A SIGTERM-ignoring\n // survivor that outlives our exit is caught by the crash guard above.\n try {\n killChildProcessTree(child, {\n detachedGroup: process.platform !== \"win32\",\n })\n } catch {\n // Already exited / best-effort.\n }\n\n try {\n await server.close(true)\n } catch {\n // Server already closed\n }\n if (options.onShutdown) {\n try {\n await options.onShutdown()\n } catch {\n // Best-effort cleanup; shutdown must not be blocked by it.\n }\n }\n clearTimeout(timeout)\n }\n\n function exit(code: number): void {\n if (exiting) return\n exiting = true\n process.exit(code)\n }\n\n // On POSIX the CLI is in its own process group (detached), so terminal\n // Ctrl-C reaches only the proxy. Forward the signal to the child group\n // so the CLI's own interactive handler runs (e.g. Claude's \"press\n // Ctrl-C again to exit\"); let its natural exit drive shutdown and\n // escalate to a hard tree-kill only if it ignores us past a grace.\n let forwardGrace: NodeJS.Timeout | null = null\n const lastForwardAt: Record<\"SIGINT\" | \"SIGTERM\", number> = {\n SIGINT: 0,\n SIGTERM: 0,\n }\n const onSignal = (sig: \"SIGINT\" | \"SIGTERM\") => {\n if (process.platform !== \"win32\" && child.pid && !cleaned) {\n // Debounce PER SIGNAL a sub-second burst into ONE forward. The other\n // subsystem signal handlers (keep-awake/colbert/worker/browser)\n // re-raise the SAME signal to restore default-terminate, which\n // re-invokes us several times within milliseconds — without this the\n // child would receive a volley and Claude's \"press Ctrl-C again\"\n // guard would never see a single press. Per-signal so a SIGTERM right\n // after a SIGINT still forwards. A genuine second human Ctrl-C\n // (seconds later) falls outside the window and forwards again.\n const now = Date.now()\n if (now - lastForwardAt[sig] > 250) {\n lastForwardAt[sig] = now\n try {\n process.kill(-child.pid, sig)\n } catch {\n // group already gone — fall through to the grace escalation\n }\n }\n const graceMs = sig === \"SIGINT\" ? 10000 : 3000\n if (!forwardGrace) {\n forwardGrace = setTimeout(() => {\n cleanup().then(() => exit(130)).catch(() => exit(1))\n }, graceMs)\n forwardGrace.unref?.()\n }\n return\n }\n // Windows: the console already delivered Ctrl-C to the direct child;\n // cleanup()'s taskkill /T is the escalation hammer.\n cleanup().then(() => exit(130)).catch(() => exit(1))\n }\n process.on(\"SIGINT\", () => onSignal(\"SIGINT\"))\n process.on(\"SIGTERM\", () => onSignal(\"SIGTERM\"))\n\n child.on(\"exit\", (exitCode, signal) => {\n // When the spawned CLI exits we may be holding worker-agent\n // worktrees that the proxy will never get a chance to clean up\n // through the per-call finally (the proxy itself is about to\n // shut down). Drain them synchronously here — same sweep the\n // SIGINT/SIGTERM handlers use. No-op if no worker tools were\n // ever invoked (registry is null).\n try {\n sweepRegistry()\n } catch {\n // best-effort; don't let cleanup failures override the exit code\n }\n // When killed by a signal, exitCode is null — derive from signal number\n const code = exitCode ?? (signal ? 128 : 1)\n cleanup().then(() => exit(code)).catch(() => exit(1))\n })\n child.on(\"error\", () => {\n cleanup().then(() => exit(1)).catch(() => exit(1))\n })\n}\n","/**\n * Toolbelt provisioner: materialize curated CLI tools into the durable\n * router `bin/` dir so the spawned agent can call them on PATH.\n *\n * Properties (all load-bearing):\n * - **Best-effort.** Every tool is isolated in try/catch; a failure\n * skips that one tool and never throws to the launcher.\n * - **Gap-fill.** A tool already on the user's PATH is NOT\n * materialized (and a stale copy is removed) so we never shadow the\n * user's own `jq`/`yq`/etc.\n * - **Verified.** The downloaded archive's SHA256 must match the\n * hardcoded manifest digest BEFORE extraction.\n * - **Safe extraction.** Only the expected regular-file binary is\n * pulled out; symlink/hardlink/device entries are rejected.\n * - **Concurrency-safe.** A cross-process lock serializes writers;\n * unique temp names + atomic rename avoid half-written binaries.\n * - **Integrity-pruned.** Unexpected files in `bin/` are removed each\n * launch so a prompt-injected agent can't plant a `git.cmd` that\n * shadows real tools in future sessions.\n */\n\nimport { randomBytes, createHash } from \"node:crypto\"\nimport { existsSync } from \"node:fs\"\nimport {\n chmod,\n copyFile,\n link,\n mkdir,\n readdir,\n readFile,\n rename,\n rm,\n writeFile,\n} from \"node:fs/promises\"\nimport path from \"node:path\"\nimport process from \"node:process\"\n\nimport consola from \"consola\"\n\nimport { resolveExecutable, runCommandCapture } from \"../exec\"\nimport { PATHS } from \"../paths\"\nimport { withInstallLock } from \"../update-lock\"\n\nimport { extractTarGzMember, extractZipMember } from \"./extract\"\nimport { toolbeltEnabled, toolbeltSkipSet, vscodeRipgrepPath } from \"./index\"\nimport { assetFor, satisfiesMinVersion, TOOLBELT_TOOLS, type ToolAsset, type ToolSpec } from \"./manifest\"\n\n/** Per-download cap (bytes) — these binaries are a few MB at most. */\nconst MAX_DOWNLOAD_BYTES = 64 * 1024 * 1024\nconst DOWNLOAD_TIMEOUT_MS = 30_000\n\nconst EXE_EXT = process.platform === \"win32\" ? \".exe\" : \"\"\n\n/**\n * Materialize the toolbelt. Returns the list of command names exposed\n * in `bin/` after provisioning. Best-effort; never throws.\n */\nexport async function provisionToolbelt(): Promise<string[]> {\n if (!toolbeltEnabled()) return []\n const binDir = PATHS.TOOLBELT_BIN_DIR\n try {\n await mkdir(binDir, { recursive: true })\n } catch (err) {\n consola.debug(\"toolbelt: could not create bin dir:\", err)\n return []\n }\n\n const skip = toolbeltSkipSet()\n\n // Serialize across concurrent proxies. If another process holds the\n // lock it is provisioning; we skip and just report current state.\n await withInstallLock(\"toolbelt.lock\", async () => {\n await pruneUnexpected(binDir)\n await provisionRg(binDir, skip).catch((err) =>\n consola.debug(\"toolbelt: rg skipped:\", err),\n )\n await Promise.all(\n TOOLBELT_TOOLS.map((spec) =>\n provisionTool(spec, binDir, skip).catch((err) =>\n consola.debug(`toolbelt: ${spec.command} skipped:`, err),\n ),\n ),\n )\n })\n\n return exposedCommands(binDir)\n}\n\n/** Names allowed to live in `bin/` (managed binaries + their sidecars). */\nfunction expectedFileNames(): Set<string> {\n const names = new Set<string>()\n const add = (base: string) => {\n names.add(base + EXE_EXT)\n names.add(`${base}${EXE_EXT}.sha256`)\n }\n add(\"rg\")\n for (const spec of TOOLBELT_TOOLS) {\n add(spec.binBasename)\n for (const a of spec.aliases ?? []) add(a)\n }\n return names\n}\n\n/** Remove any file in `bin/` that isn't a managed binary or sidecar. */\nasync function pruneUnexpected(binDir: string): Promise<void> {\n const expected = expectedFileNames()\n let entries: string[]\n try {\n entries = await readdir(binDir)\n } catch {\n return\n }\n for (const name of entries) {\n if (name.endsWith(\".tmp\")) continue // in-flight temp from a peer\n if (!expected.has(name)) {\n await rm(path.join(binDir, name), { force: true }).catch(() => {})\n }\n }\n}\n\nasync function provisionRg(binDir: string, skip: Set<string>): Promise<void> {\n const dest = path.join(binDir, \"rg\" + EXE_EXT)\n if (skip.has(\"rg\") || resolveExecutable(\"rg\")) {\n // User has rg (or skipped) — don't shadow it. code_search uses its\n // own resolver regardless.\n await removeBin(dest)\n return\n }\n if (existsSync(dest)) return // already materialized\n const src = vscodeRipgrepPath()\n if (!src) return\n\n const tmp = tempName(dest)\n try {\n await link(src, tmp) // hardlink (same-volume fast path)\n } catch {\n await copyFile(src, tmp) // cross-volume (EXDEV) or other → copy\n }\n if (process.platform !== \"win32\") await chmod(tmp, 0o755).catch(() => {})\n await commit(tmp, dest)\n}\n\n/**\n * Does the user's own copy on PATH satisfy `spec.minVersion`?\n *\n * Returns true when there is nothing to check (no minVersion), and — this\n * is the load-bearing part — ALSO returns true when the probe fails or the\n * output cannot be parsed. Gap-fill exists so we never shadow a user's\n * pinned or wrapper install, and an unreadable `--version` is not evidence\n * that their tool is inadequate. We only step in front of it on positive\n * proof that it is too old.\n */\nasync function pathCopyIsAdequate(spec: ToolSpec, resolved: string): Promise<boolean> {\n if (!spec.minVersion) return true\n try {\n // `runCommandCapture`, NOT `runManagedExeCapture`: on Windows a tool\n // installed via npm resolves to a `.cmd` shim, which cannot be spawned\n // with `shell:false` (it throws EINVAL). That is exactly how the\n // ast-grep case presented — the probe threw, we treated the version as\n // unknown, and yielded to the outdated copy anyway. runCommandCapture\n // goes through buildExecInvocation, which routes shims via cmd.exe.\n const res = await runCommandCapture([resolved, \"--version\"], {\n timeoutMs: VERSION_PROBE_TIMEOUT_MS,\n })\n const verdict = satisfiesMinVersion(`${res.stdout} ${res.stderr}`, spec.minVersion)\n if (verdict === undefined) return true\n if (!verdict) {\n consola.debug(\n `[toolbelt] ${spec.command} on PATH is older than ${spec.minVersion}; using the router-pinned build instead`,\n )\n }\n return verdict\n } catch {\n return true\n }\n}\n\n/** A `--version` call should be instant; this only bounds a hung binary. */\nconst VERSION_PROBE_TIMEOUT_MS = 5_000\n\nasync function provisionTool(\n spec: ToolSpec,\n binDir: string,\n skip: Set<string>,\n): Promise<void> {\n const dest = path.join(binDir, spec.binBasename + EXE_EXT)\n const sidecar = `${dest}.sha256`\n const asset = assetFor(spec)\n\n if (skip.has(spec.command) || !asset) {\n await removeTool(spec, binDir)\n return\n }\n // Gap-fill: user already has it on PATH → remove ours, don't shadow.\n // Exception: a tool carrying `minVersion` is one this project's own code\n // depends on, so an older PATH copy is not \"good enough\" — it degrades\n // us silently. In that case we materialize ours anyway; the toolbelt bin\n // is prepended to the spawned agent's PATH and code_search checks the\n // router-owned paths first, so ours wins where it matters while the\n // user's shell keeps theirs.\n const onPath = resolveExecutable(spec.command)\n if (onPath && (await pathCopyIsAdequate(spec, onPath))) {\n await removeTool(spec, binDir)\n return\n }\n // Idempotent: present + sidecar matches → just ensure aliases exist.\n if (existsSync(dest) && (await sidecarMatches(sidecar, asset.sha256))) {\n await ensureAliases(spec, binDir, dest)\n return\n }\n\n const bytes = await downloadAndExtract(spec, asset)\n await atomicInstall(dest, bytes)\n await writeFile(sidecar, asset.sha256).catch(() => {})\n await ensureAliases(spec, binDir, dest)\n}\n\nasync function downloadAndExtract(\n spec: ToolSpec,\n asset: ToolAsset,\n): Promise<Buffer> {\n const data = await download(asset.url)\n const digest = createHash(\"sha256\").update(data).digest(\"hex\")\n if (digest !== asset.sha256) {\n throw new Error(\n `checksum mismatch for ${spec.command} (${asset.url}): expected ${asset.sha256}, got ${digest}`,\n )\n }\n if (asset.archive === \"raw\") return data\n const member =\n asset.archive === \"zip\"\n ? extractZipMember(data, spec.binBasename)\n : extractTarGzMember(data, spec.binBasename)\n if (!member) {\n throw new Error(`binary \"${spec.binBasename}\" not found in ${asset.url}`)\n }\n return member\n}\n\nasync function download(url: string): Promise<Buffer> {\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS)\n try {\n const res = await fetch(url, {\n signal: controller.signal,\n redirect: \"follow\",\n headers: { \"User-Agent\": \"github-router-toolbelt\" },\n })\n if (!res.ok) throw new Error(`download ${url}: HTTP ${res.status}`)\n const buf = Buffer.from(await res.arrayBuffer())\n if (buf.length > MAX_DOWNLOAD_BYTES) {\n throw new Error(`download ${url}: exceeds ${MAX_DOWNLOAD_BYTES} bytes`)\n }\n return buf\n } finally {\n clearTimeout(timer)\n }\n}\n\n/** Write to a unique temp then atomically rename into place. */\nasync function atomicInstall(dest: string, bytes: Buffer): Promise<void> {\n const tmp = tempName(dest)\n await writeFile(tmp, bytes)\n if (process.platform !== \"win32\") await chmod(tmp, 0o755).catch(() => {})\n await commit(tmp, dest)\n}\n\n/** Rename tmp→dest, handling Windows replace-existing / in-use locks. */\nasync function commit(tmp: string, dest: string): Promise<void> {\n try {\n await rename(tmp, dest)\n } catch {\n // Windows: rename won't replace an existing/locked dest. Remove and\n // retry once; if that fails (in-use .exe), leave the existing copy.\n try {\n await rm(dest, { force: true })\n await rename(tmp, dest)\n } catch (err) {\n await rm(tmp, { force: true }).catch(() => {})\n throw err\n }\n }\n}\n\nasync function ensureAliases(\n spec: ToolSpec,\n binDir: string,\n dest: string,\n): Promise<void> {\n for (const alias of spec.aliases ?? []) {\n const ap = path.join(binDir, alias + EXE_EXT)\n if (existsSync(ap)) continue\n const tmp = tempName(ap)\n try {\n await copyFile(dest, tmp)\n if (process.platform !== \"win32\") await chmod(tmp, 0o755).catch(() => {})\n await commit(tmp, ap)\n } catch (err) {\n consola.debug(`toolbelt: alias ${alias} skipped:`, err)\n }\n }\n}\n\nasync function removeTool(spec: ToolSpec, binDir: string): Promise<void> {\n const dest = path.join(binDir, spec.binBasename + EXE_EXT)\n await removeBin(dest)\n for (const alias of spec.aliases ?? []) {\n await removeBin(path.join(binDir, alias + EXE_EXT))\n }\n}\n\nasync function removeBin(dest: string): Promise<void> {\n await rm(dest, { force: true }).catch(() => {})\n await rm(`${dest}.sha256`, { force: true }).catch(() => {})\n}\n\nasync function sidecarMatches(sidecar: string, sha256: string): Promise<boolean> {\n try {\n return (await readFile(sidecar, \"utf8\")).trim() === sha256\n } catch {\n return false\n }\n}\n\nfunction tempName(dest: string): string {\n return `${dest}.${process.pid}.${randomBytes(4).toString(\"hex\")}.tmp`\n}\n\n/** The command names currently exposed in `bin/`. */\nasync function exposedCommands(binDir: string): Promise<string[]> {\n let files: Set<string>\n try {\n files = new Set(await readdir(binDir))\n } catch {\n return []\n }\n const present = (base: string) => files.has(base + EXE_EXT)\n const out: string[] = []\n if (present(\"rg\")) out.push(\"rg\")\n for (const spec of TOOLBELT_TOOLS) {\n if (present(spec.binBasename)) out.push(spec.command)\n }\n return out\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"provision-BpL6gZIt.js","names":[],"sources":["../src/lib/process-guard/index.ts","../src/lib/stripped-env-keys.ts","../src/lib/launch.ts","../src/lib/toolbelt/provision.ts"],"sourcesContent":["/**\n * Crash-safe process guard (POSIX only).\n *\n * If the proxy dies WITHOUT running its own shutdown handlers (hard crash,\n * SIGKILL, OOM), nothing in `cleanup()` runs to reap the launched CLI's\n * process tree. This guard is the last-resort net for that case.\n *\n * Platform split:\n * - **Windows: no guard needed.** Node assigns each child to a Job\n * Object with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`, so when the proxy\n * dies (any cause) the OS tears down the whole descendant tree —\n * verified: force-killing a Node parent kills child AND grandchild.\n * `startProcessGuard` is a no-op on win32.\n * - **POSIX: a detached `node -e` reaper.** There is no job-object\n * equivalent (a killed parent's children reparent to init), so we\n * spawn a detached watchdog that learns of proxy death via an inherited\n * stdin PIPE EOF (the proxy holds the write end; the OS closes it on\n * proxy death) and reaps the CLI's process group.\n *\n * Never kill the WRONG process: if the child's PID/PGID was recycled after\n * the child exited, a blind `kill(-pid)` could hit an innocent group. The\n * reaper snapshots the child's process start-time at startup and, on proxy\n * death, kills ONLY if the live PID's start-time still matches. A mismatch\n * / unreadable probe → skip (fail-safe). We never trade an orphan-leak for\n * killing the user's work.\n *\n * The reaper is detached + unref'd so it survives the proxy's SIGKILL, and\n * self-exits once it has acted (or after a 24h cap) so it can never become\n * the orphan it exists to prevent. Opt out with\n * `GH_ROUTER_DISABLE_PROCESS_GUARD=1`.\n */\n\nimport { spawn, type ChildProcess } from \"node:child_process\"\nimport process from \"node:process\"\n\nimport { parseBoolEnv } from \"../exec\"\n\n/**\n * Live reaper children, held so the GC can't collect the `ChildProcess`\n * (and with it the stdin write-end that is the proxy-death signal) while\n * the proxy runs. Entries self-remove on the reaper's exit. We do NOT\n * proactively kill these on graceful shutdown: when the proxy exits the OS\n * closes the stdin pipe, the reaper hits EOF, re-verifies the child's\n * identity, and reaps any survivor that ignored the graceful SIGTERM —\n * exactly the backstop we want.\n */\nconst _activeReapers = new Set<ChildProcess>()\n\n/** Guard is on by default; `GH_ROUTER_DISABLE_PROCESS_GUARD=1` opts out. */\nexport function processGuardEnabled(): boolean {\n return parseBoolEnv(process.env.GH_ROUTER_DISABLE_PROCESS_GUARD) !== true\n}\n\n/**\n * Build the start-time-verified node reaper script (POSIX). PURE — `pid`\n * is our own integer. `detachedGroup` selects `kill(-pid)` (process group,\n * the detached CLI) vs `kill(pid)` on POSIX.\n *\n * Kept dependency-free (require'd builtins only) so it runs under `node -e`\n * with no module resolution against the dist bundle.\n */\nexport function buildNodeReaperScript(\n pid: number,\n detachedGroup: boolean,\n): string {\n const target = detachedGroup ? \"-PID\" : \"PID\"\n return `\nconst cp = require(\"node:child_process\");\nconst fs = require(\"node:fs\");\nconst PID = ${pid >>> 0};\nfunction startTime() {\n try {\n if (process.platform === \"linux\") {\n const stat = fs.readFileSync(\"/proc/\" + PID + \"/stat\", \"utf8\");\n const post = stat.slice(stat.lastIndexOf(\")\") + 1).trim().split(/\\\\s+/);\n return post[19] || null; // field 22 (starttime) = index 19 after comm\n }\n const out = cp.execFileSync(\"ps\", [\"-o\",\"lstart=\",\"-p\",String(PID)],\n { stdio: [\"ignore\",\"pipe\",\"ignore\"], timeout: 2000 }).toString().trim();\n return out || null;\n } catch { return null; }\n}\nfunction alive() { try { process.kill(PID, 0); return true; } catch { return false; } }\nfunction treeKill() {\n try { process.kill(${target}, \"SIGTERM\"); } catch {}\n // NOT unref'd: this must keep the loop alive to deliver the escalated\n // SIGKILL (the onDeath exit timer below stays alive past 500ms too).\n setTimeout(() => { try { process.kill(${target}, \"SIGKILL\"); } catch {} }, 500);\n}\nconst snap = (() => {\n // The child is definitely ours and alive at startup, so a null here is a\n // transient probe failure (e.g. a momentary 'ps' hiccup), NOT a real\n // identity loss. Retry a few times so a one-off failure can't silently\n // disable the guard for the rest of the run.\n for (let i = 0; i < 3; i++) { const s = startTime(); if (s !== null) return s; }\n return null;\n})();\nlet done = false;\nfunction onDeath() {\n if (done) return; done = true;\n // Re-verify identity: kill ONLY if the live PID is still our child.\n if (snap !== null && alive() && startTime() === snap) treeKill();\n // REF'd (NOT unref'd): stdin has closed, so an unref'd timer would let\n // the loop empty and the process exit immediately — dropping the 500ms\n // SIGKILL escalation. Keep the loop alive to deliver it, then exit.\n setTimeout(() => process.exit(0), 1500);\n}\nprocess.stdin.resume();\nprocess.stdin.on(\"end\", onDeath);\nprocess.stdin.on(\"close\", onDeath);\nprocess.stdin.on(\"error\", onDeath);\nconst cap = setTimeout(() => process.exit(0), 24*3600*1000); if (cap.unref) cap.unref();\n`.trim()\n}\n\n/**\n * Spawn the detached node reaper holding the stdin death-pipe. Always\n * `detached` so it outlives a force-kill of the proxy, and `unref`'d so it\n * never holds the proxy's event loop open. Returns null on spawn failure.\n */\nfunction spawnNodeReaper(pid: number, detachedGroup: boolean): ChildProcess | null {\n try {\n const child = spawn(\n process.execPath,\n [\"-e\", buildNodeReaperScript(pid, detachedGroup)],\n {\n stdio: [\"pipe\", \"ignore\", \"ignore\"],\n detached: true,\n windowsHide: true,\n shell: false,\n },\n )\n child.on(\"error\", () => {})\n child.stdin?.on(\"error\", () => {})\n // Hold a reference so the GC can't drop the ChildProcess (and its stdin\n // write-end, the proxy-death signal) mid-run; drop it on reaper exit.\n _activeReapers.add(child)\n child.once(\"exit\", () => _activeReapers.delete(child))\n child.unref()\n return child\n } catch {\n return null\n }\n}\n\n/**\n * Start the crash-safe guard for a launched CLI child. No-op on Windows\n * (Node's Job Object already reaps the tree on proxy death) and when\n * disabled / unspawnable. Fire-and-forget; never throws.\n */\nexport function startProcessGuard(child: ChildProcess): void {\n if (!processGuardEnabled()) return\n const pid = child.pid\n if (!pid) return\n // Windows: the runtime's KILL_ON_JOB_CLOSE job object is the crash net.\n if (process.platform === \"win32\") return\n // POSIX: the CLI is spawned detached (own group), so reap the group.\n spawnNodeReaper(pid, true)\n}\n","/**\n * Auth / routing / remote / wire-fingerprint environment keys that must never\n * reach a github-router-spawned Claude or Codex child — through ANY channel.\n *\n * Two channels carry these keys to a spawned agent, and both must be defended:\n * 1. The PARENT PROCESS ENV — a user's shell exports. Stripped by\n * `sanitizeParentEnv` (`src/lib/launch.ts`) before the proxy's overrides.\n * 2. The serve mirror `settings.json` `env` BLOCK — a user's own\n * `~/.claude/settings.json`, snapshot-mirrored and applied to the\n * SDK-spawned claude via `settingSources`. Stripped by\n * `sanitizeServeSettingsEnv` (`src/lib/mcp-permissions-settings.ts`).\n *\n * If any of these survived, a Bedrock/Vertex/gateway user's config would\n * re-route the session OFF the github-router proxy, inject real auth that wins\n * over the synthetic `.credentials.json`, or activate Claude Code's\n * unimplemented Bridge/remote code path. `CLAUDE_CODE_COORDINATOR_MODE` (a\n * toolset-stripper) is NOT here — it's serve-specific, added to the serve list\n * directly. `ANTHROPIC_SMALL_FAST_MODEL` is deliberately NOT stripped (users\n * legitimately rely on it; `resolveModel` translates unrecognized values).\n */\nexport const STRIPPED_AUTH_ROUTING_ENV_KEYS = [\n // Claude Code auth surface\n \"ANTHROPIC_API_KEY\",\n \"ANTHROPIC_AUTH_TOKEN\",\n \"ANTHROPIC_BASE_URL\",\n \"ANTHROPIC_CUSTOM_HEADERS\",\n \"ANTHROPIC_MODEL\",\n \"CLAUDE_CODE_OAUTH_TOKEN\",\n // Alternate OAuth source (loads from an open file descriptor); stripping\n // prevents a third auth source alongside the synthetic .credentials.json.\n \"CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR\",\n \"CLAUDE_CODE_USE_BEDROCK\",\n \"CLAUDE_CODE_USE_VERTEX\",\n \"CLAUDE_CODE_USE_FOUNDRY\",\n // Defense-in-depth: a parent- or settings-set CLAUDE_CONFIG_DIR would\n // redirect the session's config dir away from the router-owned mirror.\n \"CLAUDE_CONFIG_DIR\",\n // Claude Code Bridge / IDE remote-session surface — activates a remote code\n // path (POST /v1/code/sessions, /v1/environments/bridge, …) the proxy does\n // not implement. Stripping forces a local-only session.\n \"CLAUDE_BRIDGE_OAUTH_TOKEN\",\n \"CLAUDE_BRIDGE_BASE_URL\",\n \"CLAUDE_BRIDGE_SESSION_INGRESS_URL\",\n \"SESSION_INGRESS_URL\",\n \"CLAUDE_CODE_REMOTE\",\n \"CLAUDE_CODE_CONTAINER_ID\",\n \"CLAUDE_CODE_REMOTE_SESSION_ID\",\n \"CLAUDE_CODE_SESSION_ID\",\n // Emits `x-anthropic-additional-protection` — pure wire-fingerprint noise\n // that breaks the VS Code stealth posture.\n \"CLAUDE_CODE_ADDITIONAL_PROTECTION\",\n // Codex CLI auth surface\n \"OPENAI_API_KEY\",\n \"OPENAI_BASE_URL\",\n \"CODEX_HOME\",\n // ai-or-die per-tab session-bind / artifact-review surface — must not leak\n // into a nested launch and hijack the parent tab's sidecar / bearer token.\n \"AIORDIE_CLAUDE_BIND\",\n \"AIORDIE_TOKEN\",\n \"AIORDIE_INSECURE_TLS\",\n] as const\n","import { execFileSync, spawn, type ChildProcess } from \"node:child_process\"\nimport { existsSync } from \"node:fs\"\nimport path from \"node:path\"\nimport process from \"node:process\"\n\nimport consola from \"consola\"\n\nimport type { Server } from \"srvx\"\n\nimport { resolveExecutable, killChildProcessTree } from \"./exec\"\nimport { DEFAULT_CODEX_MODEL } from \"./port\"\nimport { startProcessGuard } from \"./process-guard\"\nimport { STRIPPED_AUTH_ROUTING_ENV_KEYS } from \"./stripped-env-keys\"\nimport { collapsePathKeys } from \"./toolbelt/path-inject\"\nimport { sweepRegistry } from \"./worker-agent/lifecycle\"\n\n/**\n * Auth-related env keys we strip from the parent before spawning the child CLI.\n * The proxy provides its own values for everything we care about, so the child\n * behaves as if the user had no parent-env auth at all. The canonical list lives\n * in `./stripped-env-keys` (shared with serve's settings.json `env` strip).\n *\n * Why strip rather than override-with-empty-string: Claude Code emits \"Auth\n * conflict\" warnings whenever both ANTHROPIC_AUTH_TOKEN and ANTHROPIC_API_KEY\n * are present (any value); stripping suppresses it AND prevents an inherited\n * real key from leaking. Cloud-provider toggles + OAuth tokens are simpler\n * dropped than overridden — a missing var is unambiguously absent everywhere.\n */\nconst STRIPPED_PARENT_ENV_KEYS = STRIPPED_AUTH_ROUTING_ENV_KEYS\n\n/**\n * Strip auth-related keys from a parent-process env object. The result\n * is suitable to spread into a spawned child's env BEFORE the proxy's\n * explicit overrides, so the proxy is the only source of truth for\n * auth — and stale shell exports can't leak through.\n */\nexport function sanitizeParentEnv(\n parent: NodeJS.ProcessEnv,\n): NodeJS.ProcessEnv {\n const sanitized: NodeJS.ProcessEnv = { ...parent }\n for (const key of STRIPPED_PARENT_ENV_KEYS) {\n delete sanitized[key]\n }\n return sanitized\n}\n\nfunction commandExists(name: string): boolean {\n try {\n execFileSync(process.platform === \"win32\" ? \"where.exe\" : \"which\", [name], {\n stdio: \"ignore\",\n })\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Whether the launcher can execute `executable`.\n *\n * `buildLaunchCommand` resolves the CLI to an ABSOLUTE path (anti-shadow).\n * `where.exe` (and POSIX `which`) reject a full path argument — `where`\n * returns \"Could not find files for the given pattern(s)\" for an absolute\n * path even when the file exists — so the where/which probe is only valid\n * for bare command names. For an absolute path (already resolved against\n * PATH and existence-checked by `resolveExecutable`), check the\n * filesystem directly. Without this split, every launch where the CLI is\n * installed fails with a spurious \"not found on PATH\".\n */\nexport function isExecutableAvailable(executable: string): boolean {\n if (path.isAbsolute(executable)) return existsSync(executable)\n return commandExists(executable)\n}\n\n/**\n * Provider-config flags (`-c model_providers.github_router=...`) that\n * point Codex at our proxy. Extracted from `buildCodexCmd` so the new\n * `codex mcp-server` MCP-config builder can reuse the exact same\n * provider definition — drift between the two paths would silently\n * break the MCP wiring.\n */\nexport function buildCodexProviderConfigFlags(serverUrl: string): Array<string> {\n return [\n \"-c\",\n `model_providers.github_router={name=\"github-router\",base_url=\"${serverUrl}/v1\",wire_api=\"responses\",env_key=\"OPENAI_API_KEY\"}`,\n \"-c\",\n \"model_provider=github_router\",\n ]\n}\n\n/**\n * Inspect the installed `codex` binary. Used by the codex-MCP wiring\n * in `claude.ts` to gate `--codex-cli`. Codex 0.129.0 introduced the\n * `mcp-server` subcommand; older versions don't expose it, so we\n * downgrade to the HTTP backend with a warning.\n */\nexport function getCodexVersion(): { ok: boolean; version?: string } {\n if (!commandExists(\"codex\")) return { ok: false }\n let raw: string\n try {\n raw = execFileSync(\"codex\", [\"--version\"], {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim()\n } catch {\n return { ok: false }\n }\n // Output examples: \"codex-cli 0.129.0\", \"codex 0.130.1-dev\"\n const m = /(\\d+)\\.(\\d+)\\.(\\d+)/.exec(raw)\n if (!m) return { ok: false, version: raw }\n const major = Number.parseInt(m[1], 10)\n const minor = Number.parseInt(m[2], 10)\n const version = `${m[1]}.${m[2]}.${m[3]}`\n // mcp-server requires codex >= 0.129.0\n const ok = major > 0 || (major === 0 && minor >= 129)\n return { ok, version }\n}\n\nexport interface LaunchTarget {\n kind: \"claude-code\" | \"codex\"\n envVars: Record<string, string>\n extraArgs: string[]\n model?: string\n /**\n * Proxy URL the spawned child should target. Required for Codex 0.129+\n * which stopped honoring OPENAI_BASE_URL and now needs an explicit\n * `-c model_providers.<name>.base_url=...` argument. Set by the codex\n * subcommand from the same `serverUrl` it computed for env vars.\n */\n serverUrl?: string\n}\n\n/**\n * Codex 0.129.0 broke two things the launcher had been relying on:\n * (1) `--full-auto` was removed in favor of `--sandbox` + `--ask-for-approval`;\n * passing it now exits the child immediately with\n * `error: unexpected argument '--full-auto' found`.\n * (2) `OPENAI_BASE_URL` is silently ignored — Codex hardcodes\n * `https://api.openai.com/v1/responses` and 401s out without an\n * explicit `-c model_providers.<name>.base_url` override.\n *\n * `buildCodexCmd` builds the launch argv that works on Codex 0.129+ while\n * still being compatible with older versions that accept the same flags.\n */\nfunction buildCodexCmd(target: LaunchTarget): string[] {\n const cmd: string[] = [\"codex\"]\n if (target.serverUrl) {\n cmd.push(...buildCodexProviderConfigFlags(target.serverUrl))\n }\n cmd.push(\n \"--sandbox\",\n \"workspace-write\",\n \"--ask-for-approval\",\n \"on-request\",\n \"-m\",\n target.model ?? DEFAULT_CODEX_MODEL,\n ...target.extraArgs,\n )\n return cmd\n}\n\nexport function buildLaunchCommand(target: LaunchTarget): {\n cmd: string[]\n env: Record<string, string | undefined>\n} {\n // F10: don't force `--dangerously-skip-permissions` when the caller already\n // requested an explicit `--permission-mode` (e.g. ai-or-die's claude-bridge\n // appends one for a fleet create_session with permissionMode). The two flags\n // conflict — claude rejects them together — and the explicit mode must win.\n const wantsPermissionMode =\n target.kind === \"claude-code\" &&\n target.extraArgs.some((arg) => arg === \"--permission-mode\" || arg.startsWith(\"--permission-mode=\"))\n const cmd: string[] =\n target.kind === \"claude-code\"\n ? wantsPermissionMode\n ? [\"claude\", ...target.extraArgs]\n : [\"claude\", \"--dangerously-skip-permissions\", ...target.extraArgs]\n : buildCodexCmd(target)\n\n // Anti-shadow: resolve the top-level CLI to an ABSOLUTE path against\n // the clean parent PATH (excluding the cwd). The spawned child's env\n // prepends the toolbelt bin dir to PATH; without this, a stray\n // `claude.cmd`/`codex.cmd` in that dir — or in an untrusted repo's cwd\n // (Windows resolves cwd before PATH under shell:true) — could shadow\n // the real CLI. Resolving here means the toolbelt PATH only affects\n // the agent's OWN tool lookups, never which CLI we launch.\n const resolved = resolveExecutable(cmd[0], { env: process.env })\n if (resolved) cmd[0] = resolved\n\n const env = collapsePathKeys({\n ...sanitizeParentEnv(process.env),\n ...target.envVars,\n })\n return { cmd, env }\n}\n\n/**\n * Whether a resolved Windows executable must be launched through cmd.exe\n * (`shell:true`). Only batch shims (`.cmd`/`.bat`) need it — and even then\n * cmd.exe stays alive as the CLI's parent, so `taskkill /T` reaps the\n * whole tree. A real `.exe` (e.g. the native-installer `claude.exe`) is\n * spawned DIRECTLY so the CLI is the direct child, with no cmd.exe\n * intermediary to orphan its node/MCP grandchildren on a kill.\n */\nexport function windowsLaunchNeedsShell(executable: string): boolean {\n const ext = path.extname(executable).toLowerCase()\n return ext === \".cmd\" || ext === \".bat\"\n}\n\nexport function launchChild(\n target: LaunchTarget,\n server: Server,\n options: { onShutdown?: () => Promise<void> | void } = {},\n): void {\n const { cmd, env } = buildLaunchCommand(target)\n\n const executable = cmd[0]\n if (!isExecutableAvailable(executable)) {\n const msg = `\"${executable}\" not found on PATH. Install it first, then try again.`\n consola.error(msg)\n process.stderr.write(msg + \"\\n\")\n process.exit(1)\n }\n\n let child: ChildProcess\n try {\n if (process.platform === \"win32\") {\n if (windowsLaunchNeedsShell(cmd[0])) {\n // A batch shim genuinely needs cmd.exe. cmd.exe stays alive as the\n // CLI's parent for the shim's lifetime, so the tree-kill (taskkill\n // /T) and the crash guard both reap the whole tree through it. Use\n // the full command as a single string to avoid the DEP0190\n // deprecation warning about shell + args.\n const quoted = cmd.map((a) => (a.includes(\" \") ? `\"${a}\"` : a)).join(\" \")\n child = spawn(quoted, [], {\n env,\n stdio: \"inherit\",\n shell: true,\n })\n } else {\n // A real .exe (e.g. the native-installer claude.exe) needs no\n // shell. Spawning it directly makes the CLI the DIRECT child, so\n // child.kill()/taskkill and the crash guard target the real\n // process instead of a cmd.exe wrapper that would orphan its\n // node/MCP grandchildren.\n child = spawn(cmd[0], cmd.slice(1), {\n env,\n stdio: \"inherit\",\n })\n }\n } else {\n // detached:true → the CLI leads its OWN process group, so cleanup()\n // can kill(-pgid) the whole tree (grandchildren included). We still\n // await its exit, so we do NOT unref(). Terminal Ctrl-C no longer\n // reaches the child directly — the signal handlers below forward it.\n child = spawn(cmd[0], cmd.slice(1), {\n env,\n stdio: \"inherit\",\n detached: true,\n })\n }\n } catch (error) {\n const msg = `Failed to launch ${executable}: ${error instanceof Error ? error.message : String(error)}`\n consola.error(msg)\n process.stderr.write(msg + \"\\n\")\n server.close(true).catch(() => {})\n if (options.onShutdown) {\n void Promise.resolve(options.onShutdown()).catch(() => {})\n }\n process.exit(1)\n }\n\n // Crash-safe net: if the proxy dies WITHOUT running cleanup() (hard\n // crash, SIGKILL/taskkill, OOM). On Windows this is the OS's job —\n // Node's KILL_ON_JOB_CLOSE job object reaps the tree — so this is a\n // no-op there. On POSIX (no such mechanism) it spawns a detached reaper\n // that sees the proxy's stdin-pipe EOF, re-verifies the child's\n // identity, and reaps the group (also backstopping a graceful shutdown\n // where the child ignores SIGTERM and the proxy exits before the\n // escalation lands). Fire-and-forget; self-acts and self-exits.\n startProcessGuard(child)\n\n let cleaned = false\n let exiting = false\n async function cleanup(): Promise<void> {\n if (cleaned) return\n cleaned = true\n\n // Arm the hard fail-safe FIRST, before any (potentially blocking) kill\n // or async teardown, so a wedged taskkill / server.close can't hang\n // shutdown indefinitely.\n const timeout = setTimeout(() => process.exit(1), 5000)\n\n // Tree-kill the whole CLI subtree, not just the direct child. On\n // Windows taskkill /T reaps grandchildren; on POSIX kill(-pgid) reaps\n // the detached process group. (Replaces a plain child.kill() that\n // orphaned node/MCP grandchildren on Windows.) A SIGTERM-ignoring\n // survivor that outlives our exit is caught by the crash guard above.\n try {\n killChildProcessTree(child, {\n detachedGroup: process.platform !== \"win32\",\n })\n } catch {\n // Already exited / best-effort.\n }\n\n try {\n await server.close(true)\n } catch {\n // Server already closed\n }\n if (options.onShutdown) {\n try {\n await options.onShutdown()\n } catch {\n // Best-effort cleanup; shutdown must not be blocked by it.\n }\n }\n clearTimeout(timeout)\n }\n\n function exit(code: number): void {\n if (exiting) return\n exiting = true\n process.exit(code)\n }\n\n // On POSIX the CLI is in its own process group (detached), so terminal\n // Ctrl-C reaches only the proxy. Forward the signal to the child group\n // so the CLI's own interactive handler runs (e.g. Claude's \"press\n // Ctrl-C again to exit\"); let its natural exit drive shutdown and\n // escalate to a hard tree-kill only if it ignores us past a grace.\n let forwardGrace: NodeJS.Timeout | null = null\n const lastForwardAt: Record<\"SIGINT\" | \"SIGTERM\", number> = {\n SIGINT: 0,\n SIGTERM: 0,\n }\n const onSignal = (sig: \"SIGINT\" | \"SIGTERM\") => {\n if (process.platform !== \"win32\" && child.pid && !cleaned) {\n // Debounce PER SIGNAL a sub-second burst into ONE forward. The other\n // subsystem signal handlers (keep-awake/colbert/worker/browser)\n // re-raise the SAME signal to restore default-terminate, which\n // re-invokes us several times within milliseconds — without this the\n // child would receive a volley and Claude's \"press Ctrl-C again\"\n // guard would never see a single press. Per-signal so a SIGTERM right\n // after a SIGINT still forwards. A genuine second human Ctrl-C\n // (seconds later) falls outside the window and forwards again.\n const now = Date.now()\n if (now - lastForwardAt[sig] > 250) {\n lastForwardAt[sig] = now\n try {\n process.kill(-child.pid, sig)\n } catch {\n // group already gone — fall through to the grace escalation\n }\n }\n const graceMs = sig === \"SIGINT\" ? 10000 : 3000\n if (!forwardGrace) {\n forwardGrace = setTimeout(() => {\n cleanup().then(() => exit(130)).catch(() => exit(1))\n }, graceMs)\n forwardGrace.unref?.()\n }\n return\n }\n // Windows: the console already delivered Ctrl-C to the direct child;\n // cleanup()'s taskkill /T is the escalation hammer.\n cleanup().then(() => exit(130)).catch(() => exit(1))\n }\n process.on(\"SIGINT\", () => onSignal(\"SIGINT\"))\n process.on(\"SIGTERM\", () => onSignal(\"SIGTERM\"))\n\n child.on(\"exit\", (exitCode, signal) => {\n // When the spawned CLI exits we may be holding worker-agent\n // worktrees that the proxy will never get a chance to clean up\n // through the per-call finally (the proxy itself is about to\n // shut down). Drain them synchronously here — same sweep the\n // SIGINT/SIGTERM handlers use. No-op if no worker tools were\n // ever invoked (registry is null).\n try {\n sweepRegistry()\n } catch {\n // best-effort; don't let cleanup failures override the exit code\n }\n // When killed by a signal, exitCode is null — derive from signal number\n const code = exitCode ?? (signal ? 128 : 1)\n cleanup().then(() => exit(code)).catch(() => exit(1))\n })\n child.on(\"error\", () => {\n cleanup().then(() => exit(1)).catch(() => exit(1))\n })\n}\n","/**\n * Toolbelt provisioner: materialize curated CLI tools into the durable\n * router `bin/` dir so the spawned agent can call them on PATH.\n *\n * Properties (all load-bearing):\n * - **Best-effort.** Every tool is isolated in try/catch; a failure\n * skips that one tool and never throws to the launcher.\n * - **Gap-fill.** A tool already on the user's PATH is NOT\n * materialized (and a stale copy is removed) so we never shadow the\n * user's own `jq`/`yq`/etc.\n * - **Verified.** The downloaded archive's SHA256 must match the\n * hardcoded manifest digest BEFORE extraction.\n * - **Safe extraction.** Only the expected regular-file binary is\n * pulled out; symlink/hardlink/device entries are rejected.\n * - **Concurrency-safe.** A cross-process lock serializes writers;\n * unique temp names + atomic rename avoid half-written binaries.\n * - **Integrity-pruned.** Unexpected files in `bin/` are removed each\n * launch so a prompt-injected agent can't plant a `git.cmd` that\n * shadows real tools in future sessions.\n */\n\nimport { randomBytes, createHash } from \"node:crypto\"\nimport { existsSync } from \"node:fs\"\nimport {\n chmod,\n copyFile,\n link,\n mkdir,\n readdir,\n readFile,\n rename,\n rm,\n writeFile,\n} from \"node:fs/promises\"\nimport path from \"node:path\"\nimport process from \"node:process\"\n\nimport consola from \"consola\"\n\nimport { resolveExecutable, runCommandCapture } from \"../exec\"\nimport { PATHS } from \"../paths\"\nimport { withInstallLock } from \"../update-lock\"\n\nimport { extractTarGzMember, extractZipMember } from \"./extract\"\nimport { toolbeltEnabled, toolbeltSkipSet, vscodeRipgrepPath } from \"./index\"\nimport { assetFor, satisfiesMinVersion, TOOLBELT_TOOLS, type ToolAsset, type ToolSpec } from \"./manifest\"\n\n/** Per-download cap (bytes) — these binaries are a few MB at most. */\nconst MAX_DOWNLOAD_BYTES = 64 * 1024 * 1024\nconst DOWNLOAD_TIMEOUT_MS = 30_000\n\nconst EXE_EXT = process.platform === \"win32\" ? \".exe\" : \"\"\n\n/**\n * Materialize the toolbelt. Returns the list of command names exposed\n * in `bin/` after provisioning. Best-effort; never throws.\n */\nexport async function provisionToolbelt(): Promise<string[]> {\n if (!toolbeltEnabled()) return []\n const binDir = PATHS.TOOLBELT_BIN_DIR\n try {\n await mkdir(binDir, { recursive: true })\n } catch (err) {\n consola.debug(\"toolbelt: could not create bin dir:\", err)\n return []\n }\n\n const skip = toolbeltSkipSet()\n\n // Serialize across concurrent proxies. If another process holds the\n // lock it is provisioning; we skip and just report current state.\n await withInstallLock(\"toolbelt.lock\", async () => {\n await pruneUnexpected(binDir)\n await provisionRg(binDir, skip).catch((err) =>\n consola.debug(\"toolbelt: rg skipped:\", err),\n )\n await Promise.all(\n TOOLBELT_TOOLS.map((spec) =>\n provisionTool(spec, binDir, skip).catch((err) =>\n consola.debug(`toolbelt: ${spec.command} skipped:`, err),\n ),\n ),\n )\n })\n\n return exposedCommands(binDir)\n}\n\n/** Names allowed to live in `bin/` (managed binaries + their sidecars). */\nfunction expectedFileNames(): Set<string> {\n const names = new Set<string>()\n const add = (base: string) => {\n names.add(base + EXE_EXT)\n names.add(`${base}${EXE_EXT}.sha256`)\n }\n add(\"rg\")\n for (const spec of TOOLBELT_TOOLS) {\n add(spec.binBasename)\n for (const a of spec.aliases ?? []) add(a)\n }\n return names\n}\n\n/** Remove any file in `bin/` that isn't a managed binary or sidecar. */\nasync function pruneUnexpected(binDir: string): Promise<void> {\n const expected = expectedFileNames()\n let entries: string[]\n try {\n entries = await readdir(binDir)\n } catch {\n return\n }\n for (const name of entries) {\n if (name.endsWith(\".tmp\")) continue // in-flight temp from a peer\n if (!expected.has(name)) {\n await rm(path.join(binDir, name), { force: true }).catch(() => {})\n }\n }\n}\n\nasync function provisionRg(binDir: string, skip: Set<string>): Promise<void> {\n const dest = path.join(binDir, \"rg\" + EXE_EXT)\n if (skip.has(\"rg\") || resolveExecutable(\"rg\")) {\n // User has rg (or skipped) — don't shadow it. code_search uses its\n // own resolver regardless.\n await removeBin(dest)\n return\n }\n if (existsSync(dest)) return // already materialized\n const src = vscodeRipgrepPath()\n if (!src) return\n\n const tmp = tempName(dest)\n try {\n await link(src, tmp) // hardlink (same-volume fast path)\n } catch {\n await copyFile(src, tmp) // cross-volume (EXDEV) or other → copy\n }\n if (process.platform !== \"win32\") await chmod(tmp, 0o755).catch(() => {})\n await commit(tmp, dest)\n}\n\n/**\n * Does the user's own copy on PATH satisfy `spec.minVersion`?\n *\n * Returns true when there is nothing to check (no minVersion), and — this\n * is the load-bearing part — ALSO returns true when the probe fails or the\n * output cannot be parsed. Gap-fill exists so we never shadow a user's\n * pinned or wrapper install, and an unreadable `--version` is not evidence\n * that their tool is inadequate. We only step in front of it on positive\n * proof that it is too old.\n */\nasync function pathCopyIsAdequate(spec: ToolSpec, resolved: string): Promise<boolean> {\n if (!spec.minVersion) return true\n try {\n // `runCommandCapture`, NOT `runManagedExeCapture`: on Windows a tool\n // installed via npm resolves to a `.cmd` shim, which cannot be spawned\n // with `shell:false` (it throws EINVAL). That is exactly how the\n // ast-grep case presented — the probe threw, we treated the version as\n // unknown, and yielded to the outdated copy anyway. runCommandCapture\n // goes through buildExecInvocation, which routes shims via cmd.exe.\n const res = await runCommandCapture([resolved, \"--version\"], {\n timeoutMs: VERSION_PROBE_TIMEOUT_MS,\n })\n const verdict = satisfiesMinVersion(`${res.stdout} ${res.stderr}`, spec.minVersion)\n if (verdict === undefined) return true\n if (!verdict) {\n consola.debug(\n `[toolbelt] ${spec.command} on PATH is older than ${spec.minVersion}; using the router-pinned build instead`,\n )\n }\n return verdict\n } catch {\n return true\n }\n}\n\n/** A `--version` call should be instant; this only bounds a hung binary. */\nconst VERSION_PROBE_TIMEOUT_MS = 5_000\n\nasync function provisionTool(\n spec: ToolSpec,\n binDir: string,\n skip: Set<string>,\n): Promise<void> {\n const dest = path.join(binDir, spec.binBasename + EXE_EXT)\n const sidecar = `${dest}.sha256`\n const asset = assetFor(spec)\n\n if (skip.has(spec.command) || !asset) {\n await removeTool(spec, binDir)\n return\n }\n // Gap-fill: user already has it on PATH → remove ours, don't shadow.\n // Exception: a tool carrying `minVersion` is one this project's own code\n // depends on, so an older PATH copy is not \"good enough\" — it degrades\n // us silently. In that case we materialize ours anyway; the toolbelt bin\n // is prepended to the spawned agent's PATH and code_search checks the\n // router-owned paths first, so ours wins where it matters while the\n // user's shell keeps theirs.\n const onPath = resolveExecutable(spec.command)\n if (onPath && (await pathCopyIsAdequate(spec, onPath))) {\n await removeTool(spec, binDir)\n return\n }\n // Idempotent: present + sidecar matches → just ensure aliases exist.\n if (existsSync(dest) && (await sidecarMatches(sidecar, asset.sha256))) {\n await ensureAliases(spec, binDir, dest)\n return\n }\n\n const bytes = await downloadAndExtract(spec, asset)\n await atomicInstall(dest, bytes)\n await writeFile(sidecar, asset.sha256).catch(() => {})\n await ensureAliases(spec, binDir, dest)\n}\n\nasync function downloadAndExtract(\n spec: ToolSpec,\n asset: ToolAsset,\n): Promise<Buffer> {\n const data = await download(asset.url)\n const digest = createHash(\"sha256\").update(data).digest(\"hex\")\n if (digest !== asset.sha256) {\n throw new Error(\n `checksum mismatch for ${spec.command} (${asset.url}): expected ${asset.sha256}, got ${digest}`,\n )\n }\n if (asset.archive === \"raw\") return data\n const member =\n asset.archive === \"zip\"\n ? extractZipMember(data, spec.binBasename)\n : extractTarGzMember(data, spec.binBasename)\n if (!member) {\n throw new Error(`binary \"${spec.binBasename}\" not found in ${asset.url}`)\n }\n return member\n}\n\nasync function download(url: string): Promise<Buffer> {\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS)\n try {\n const res = await fetch(url, {\n signal: controller.signal,\n redirect: \"follow\",\n headers: { \"User-Agent\": \"github-router-toolbelt\" },\n })\n if (!res.ok) throw new Error(`download ${url}: HTTP ${res.status}`)\n const buf = Buffer.from(await res.arrayBuffer())\n if (buf.length > MAX_DOWNLOAD_BYTES) {\n throw new Error(`download ${url}: exceeds ${MAX_DOWNLOAD_BYTES} bytes`)\n }\n return buf\n } finally {\n clearTimeout(timer)\n }\n}\n\n/** Write to a unique temp then atomically rename into place. */\nasync function atomicInstall(dest: string, bytes: Buffer): Promise<void> {\n const tmp = tempName(dest)\n await writeFile(tmp, bytes)\n if (process.platform !== \"win32\") await chmod(tmp, 0o755).catch(() => {})\n await commit(tmp, dest)\n}\n\n/** Rename tmp→dest, handling Windows replace-existing / in-use locks. */\nasync function commit(tmp: string, dest: string): Promise<void> {\n try {\n await rename(tmp, dest)\n } catch {\n // Windows: rename won't replace an existing/locked dest. Remove and\n // retry once; if that fails (in-use .exe), leave the existing copy.\n try {\n await rm(dest, { force: true })\n await rename(tmp, dest)\n } catch (err) {\n await rm(tmp, { force: true }).catch(() => {})\n throw err\n }\n }\n}\n\nasync function ensureAliases(\n spec: ToolSpec,\n binDir: string,\n dest: string,\n): Promise<void> {\n for (const alias of spec.aliases ?? []) {\n const ap = path.join(binDir, alias + EXE_EXT)\n if (existsSync(ap)) continue\n const tmp = tempName(ap)\n try {\n await copyFile(dest, tmp)\n if (process.platform !== \"win32\") await chmod(tmp, 0o755).catch(() => {})\n await commit(tmp, ap)\n } catch (err) {\n consola.debug(`toolbelt: alias ${alias} skipped:`, err)\n }\n }\n}\n\nasync function removeTool(spec: ToolSpec, binDir: string): Promise<void> {\n const dest = path.join(binDir, spec.binBasename + EXE_EXT)\n await removeBin(dest)\n for (const alias of spec.aliases ?? []) {\n await removeBin(path.join(binDir, alias + EXE_EXT))\n }\n}\n\nasync function removeBin(dest: string): Promise<void> {\n await rm(dest, { force: true }).catch(() => {})\n await rm(`${dest}.sha256`, { force: true }).catch(() => {})\n}\n\nasync function sidecarMatches(sidecar: string, sha256: string): Promise<boolean> {\n try {\n return (await readFile(sidecar, \"utf8\")).trim() === sha256\n } catch {\n return false\n }\n}\n\nfunction tempName(dest: string): string {\n return `${dest}.${process.pid}.${randomBytes(4).toString(\"hex\")}.tmp`\n}\n\n/** The command names currently exposed in `bin/`. */\nasync function exposedCommands(binDir: string): Promise<string[]> {\n let files: Set<string>\n try {\n files = new Set(await readdir(binDir))\n } catch {\n return []\n }\n const present = (base: string) => files.has(base + EXE_EXT)\n const out: string[] = []\n if (present(\"rg\")) out.push(\"rg\")\n for (const spec of TOOLBELT_TOOLS) {\n if (present(spec.binBasename)) out.push(spec.command)\n }\n return out\n}\n"],"mappings":"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"}