@gajae-code/coding-agent 0.18.7 → 0.18.9

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 (344) hide show
  1. package/CHANGELOG.md +377 -1
  2. package/dist/types/async/endpoint-id.d.ts +16 -0
  3. package/dist/types/async/job-manager.d.ts +1 -1
  4. package/dist/types/async/support.d.ts +0 -14
  5. package/dist/types/capability/mcp.d.ts +2 -0
  6. package/dist/types/capability/skill.d.ts +2 -0
  7. package/dist/types/capability/types.d.ts +2 -0
  8. package/dist/types/cli/public-command-errors.d.ts +3 -1
  9. package/dist/types/cli/setup-cli.d.ts +1 -1
  10. package/dist/types/commands/deep-interview.d.ts +3 -0
  11. package/dist/types/commands/mcp-serve.d.ts +4 -0
  12. package/dist/types/config/autorouting-tier-map.d.ts +348 -9
  13. package/dist/types/config/file-lock.d.ts +1 -0
  14. package/dist/types/config/model-bindings-applier.d.ts +2 -0
  15. package/dist/types/config/model-profile-activation.d.ts +11 -3
  16. package/dist/types/config/model-registry.d.ts +4 -9
  17. package/dist/types/config/skill-settings-defaults.d.ts +2 -0
  18. package/dist/types/cursor-delete-path.d.ts +1 -0
  19. package/dist/types/cursor-mcp-name.d.ts +8 -0
  20. package/dist/types/customization/inventory.d.ts +2 -0
  21. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  22. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  23. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  24. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  25. package/dist/types/discovery/helpers.d.ts +6 -1
  26. package/dist/types/discovery/mcp-json.d.ts +1 -0
  27. package/dist/types/eval/py/kernel.d.ts +18 -0
  28. package/dist/types/export/html/css-color.d.ts +2 -0
  29. package/dist/types/export/html/html-export-destination.d.ts +2 -0
  30. package/dist/types/export/html/template.generated.d.ts +1 -1
  31. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +5 -0
  32. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  33. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  34. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  35. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  36. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  37. package/dist/types/extensibility/skill-management.d.ts +4 -1
  38. package/dist/types/extensibility/skills.d.ts +2 -0
  39. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  40. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  41. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  42. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -3
  43. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  44. package/dist/types/gjc-runtime/sidecar-bootstrap-env.d.ts +6 -0
  45. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  46. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  47. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  48. package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
  49. package/dist/types/gjc-runtime/tmux-common.d.ts +8 -5
  50. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  51. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  52. package/dist/types/lsp/config.d.ts +8 -5
  53. package/dist/types/lsp/workspace-path.d.ts +1 -0
  54. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  55. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  56. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  57. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  58. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  59. package/dist/types/modes/interactive-mode.d.ts +1 -0
  60. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  61. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  62. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  63. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  64. package/dist/types/progress/progress-contract.d.ts +154 -0
  65. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  66. package/dist/types/progress/project-progress.d.ts +141 -0
  67. package/dist/types/progress/render-progress.d.ts +28 -0
  68. package/dist/types/runtime-mcp/manager.d.ts +2 -1
  69. package/dist/types/runtime-mcp/mcp-redirect.d.ts +9 -0
  70. package/dist/types/runtime-mcp/oauth-public-url.d.ts +3 -0
  71. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  72. package/dist/types/runtime-mcp/project-host-values.d.ts +27 -0
  73. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  74. package/dist/types/runtime-mcp/types.d.ts +6 -0
  75. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  76. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  77. package/dist/types/sdk/broker/broker.d.ts +24 -2
  78. package/dist/types/sdk/broker/doctor-restart.d.ts +5 -0
  79. package/dist/types/sdk/broker/ensure.d.ts +14 -1
  80. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  81. package/dist/types/sdk/bus/index.d.ts +12 -1
  82. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  83. package/dist/types/sdk/client/client.d.ts +2 -0
  84. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  85. package/dist/types/sdk/index.d.ts +1 -0
  86. package/dist/types/sdk/router/session-router.d.ts +2 -0
  87. package/dist/types/sdk/session-list.d.ts +1 -1
  88. package/dist/types/sdk/session.d.ts +8 -0
  89. package/dist/types/session/agent-session.d.ts +38 -9
  90. package/dist/types/session/artifacts.d.ts +2 -2
  91. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  92. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  93. package/dist/types/session/internal/managed-session-storage.d.ts +26 -1
  94. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  95. package/dist/types/session/internal/task-artifact-owner-access.d.ts +2 -0
  96. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +9 -1
  97. package/dist/types/session/session-manager.d.ts +31 -5
  98. package/dist/types/session/session-retirement.d.ts +31 -7
  99. package/dist/types/session/session-storage.d.ts +69 -2
  100. package/dist/types/session/task-artifact-owner-codec.d.ts +2 -0
  101. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  102. package/dist/types/setup/credential-import.d.ts +6 -4
  103. package/dist/types/skill-state/active-state.d.ts +38 -0
  104. package/dist/types/skill-state/workflow-mutation-guard.d.ts +3 -0
  105. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  106. package/dist/types/task/executor.d.ts +3 -0
  107. package/dist/types/task/isolation-dir.d.ts +26 -0
  108. package/dist/types/tools/ast-edit-write-path.d.ts +13 -0
  109. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  110. package/dist/types/tools/browser/cdp-url.d.ts +2 -0
  111. package/dist/types/tools/github-side-effect.d.ts +22 -0
  112. package/dist/types/tools/index.d.ts +3 -0
  113. package/dist/types/tools/monitor-mutation.d.ts +9 -0
  114. package/dist/types/tools/python.d.ts +9 -1
  115. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  116. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  117. package/dist/types/tools/todo-write.d.ts +6 -0
  118. package/dist/types/utils/tools-checksum.d.ts +7 -0
  119. package/package.json +7 -7
  120. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  121. package/scripts/verify-managed-empty-stop.ts +28 -0
  122. package/src/async/endpoint-id.ts +26 -0
  123. package/src/async/job-manager.ts +247 -132
  124. package/src/async/support.ts +0 -25
  125. package/src/capability/index.ts +15 -0
  126. package/src/capability/mcp.ts +2 -0
  127. package/src/capability/skill.ts +2 -0
  128. package/src/capability/types.ts +2 -0
  129. package/src/cli/auth-broker-cli.ts +1 -1
  130. package/src/cli/customize-doctor.ts +15 -2
  131. package/src/cli/doctor/repairs.ts +4 -1
  132. package/src/cli/doctor-supervisor.ts +3 -0
  133. package/src/cli/public-command-errors.ts +21 -1
  134. package/src/cli/public-command-registry.ts +28 -6
  135. package/src/cli/setup-cli.ts +4 -4
  136. package/src/cli/skills-cli.ts +5 -0
  137. package/src/commands/deep-interview.ts +47 -8
  138. package/src/commands/mcp-serve.ts +7 -0
  139. package/src/commands/sdk.ts +455 -397
  140. package/src/config/autorouting-tier-map.ts +117 -7
  141. package/src/config/file-lock.ts +316 -79
  142. package/src/config/mcp-schema.json +4 -0
  143. package/src/config/model-bindings-applier.ts +20 -0
  144. package/src/config/model-profile-activation.ts +240 -46
  145. package/src/config/model-registry.ts +47 -153
  146. package/src/config/skill-settings-defaults.ts +7 -0
  147. package/src/coordinator-mcp/policy.ts +3 -2
  148. package/src/coordinator-mcp/server.ts +2 -0
  149. package/src/cursor-delete-path.ts +10 -0
  150. package/src/cursor-mcp-name.ts +15 -0
  151. package/src/cursor.ts +13 -2
  152. package/src/customization/inventory.ts +4 -13
  153. package/src/dap/config.ts +2 -2
  154. package/src/debug/report-bundle-parent.ts +22 -0
  155. package/src/debug/report-bundle.ts +3 -0
  156. package/src/debug/system-info.ts +13 -1
  157. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +10 -6
  158. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  159. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  160. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  161. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  162. package/src/defaults/gjc-defaults.test.ts +18 -1
  163. package/src/defaults/gjc-defaults.ts +3 -0
  164. package/src/defaults/gjc-skills.generated.ts +58 -36
  165. package/src/discovery/agents.ts +6 -1
  166. package/src/discovery/builtin.ts +13 -1
  167. package/src/discovery/helpers.ts +171 -29
  168. package/src/discovery/mcp-json.ts +26 -12
  169. package/src/discovery/ssh.ts +1 -1
  170. package/src/discovery/substitute-plugin-root.ts +3 -1
  171. package/src/edit/file-read-cache.ts +1 -1
  172. package/src/eval/py/executor.ts +796 -154
  173. package/src/eval/py/kernel.ts +79 -13
  174. package/src/eval/py/tool-bridge.ts +4 -0
  175. package/src/export/html/css-color.ts +9 -0
  176. package/src/export/html/html-export-destination.ts +16 -0
  177. package/src/export/html/index.ts +7 -10
  178. package/src/export/html/template.generated.ts +1 -1
  179. package/src/extensibility/extensions/function-hooks-internal.ts +27 -0
  180. package/src/extensibility/extensions/runner.ts +15 -1
  181. package/src/extensibility/extensions/types.ts +7 -0
  182. package/src/extensibility/gjc-plugins/mcp-policy.ts +2 -1
  183. package/src/extensibility/gjc-plugins/runtime-adapters.ts +85 -8
  184. package/src/extensibility/plugins/manager.ts +3 -2
  185. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  186. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  187. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  188. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  189. package/src/extensibility/skill-management.ts +9 -2
  190. package/src/extensibility/skills.ts +6 -1
  191. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  192. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  193. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  194. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  195. package/src/gjc-runtime/gc-runtime.ts +206 -9
  196. package/src/gjc-runtime/launch-worktree.ts +70 -9
  197. package/src/gjc-runtime/psmux-detect.ts +28 -4
  198. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  199. package/src/gjc-runtime/session-state-sidecar.ts +3 -2
  200. package/src/gjc-runtime/sidecar-bootstrap-env.ts +19 -0
  201. package/src/gjc-runtime/state-argv.ts +2 -0
  202. package/src/gjc-runtime/state-migrations.ts +91 -29
  203. package/src/gjc-runtime/state-runtime.ts +3716 -135
  204. package/src/gjc-runtime/state-writer.ts +105 -16
  205. package/src/gjc-runtime/tmux-common.ts +8 -5
  206. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  207. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  208. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  209. package/src/harness-control-plane/operate.ts +12 -4
  210. package/src/harness-control-plane/owner.ts +5 -1
  211. package/src/hashline/parser.ts +48 -9
  212. package/src/hashline/recovery.ts +13 -4
  213. package/src/internal-urls/docs-index.generated.ts +20 -19
  214. package/src/internal-urls/skill-path.ts +19 -0
  215. package/src/internal-urls/skill-protocol.ts +2 -0
  216. package/src/lsp/config.ts +12 -63
  217. package/src/lsp/edits.ts +6 -0
  218. package/src/lsp/workspace-path.ts +51 -0
  219. package/src/main.ts +195 -31
  220. package/src/memories/index.ts +88 -7
  221. package/src/modes/acp/acp-agent.ts +88 -4
  222. package/src/modes/components/custom-editor.ts +14 -0
  223. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  224. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  225. package/src/modes/components/extensions/state-manager.ts +15 -5
  226. package/src/modes/components/hook-editor.ts +1 -1
  227. package/src/modes/components/hook-selector.ts +1 -1
  228. package/src/modes/components/model-selector.ts +1 -4
  229. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  230. package/src/modes/components/status-line/segments.ts +4 -0
  231. package/src/modes/controllers/event-controller.ts +11 -4
  232. package/src/modes/controllers/input-controller.ts +76 -31
  233. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -16
  234. package/src/modes/controllers/selector-controller.ts +24 -15
  235. package/src/modes/controllers/todo-command-controller.ts +1 -1
  236. package/src/modes/interactive-mode.ts +5 -1
  237. package/src/modes/runtime-init.ts +4 -0
  238. package/src/modes/utils/abort-message.ts +3 -1
  239. package/src/modes/utils/interrupt-ownership.ts +12 -0
  240. package/src/modes/utils/keybinding-matchers.ts +4 -0
  241. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  242. package/src/progress/collect-project-progress.ts +153 -0
  243. package/src/progress/progress-contract.ts +172 -0
  244. package/src/progress/progress-snapshot.ts +140 -0
  245. package/src/progress/project-progress.ts +446 -0
  246. package/src/progress/render-progress.ts +209 -0
  247. package/src/prompts/system/master-mode.md +1 -0
  248. package/src/prompts/tools/skill-discovery.md +1 -1
  249. package/src/runtime-mcp/client.ts +4 -2
  250. package/src/runtime-mcp/config-writer.ts +10 -6
  251. package/src/runtime-mcp/config.ts +1 -0
  252. package/src/runtime-mcp/json-rpc.ts +2 -1
  253. package/src/runtime-mcp/manager.ts +29 -11
  254. package/src/runtime-mcp/mcp-redirect.ts +73 -0
  255. package/src/runtime-mcp/oauth-flow.ts +14 -2
  256. package/src/runtime-mcp/oauth-public-url.ts +14 -0
  257. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  258. package/src/runtime-mcp/pool.ts +6 -0
  259. package/src/runtime-mcp/project-host-values.ts +46 -0
  260. package/src/runtime-mcp/smithery-registry.ts +22 -3
  261. package/src/runtime-mcp/transports/http.ts +4 -3
  262. package/src/runtime-mcp/types.ts +6 -0
  263. package/src/sdk/acp/adapter.ts +95 -8
  264. package/src/sdk/broker/broker-exit.ts +52 -23
  265. package/src/sdk/broker/broker.ts +81 -6
  266. package/src/sdk/broker/doctor-restart.ts +4 -0
  267. package/src/sdk/broker/ensure.ts +31 -2
  268. package/src/sdk/broker/lifecycle.ts +711 -66
  269. package/src/sdk/broker/session-index.ts +191 -81
  270. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  271. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  272. package/src/sdk/bus/index.ts +238 -43
  273. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  274. package/src/sdk/cli/session-cli.ts +17 -3
  275. package/src/sdk/client/client.ts +28 -8
  276. package/src/sdk/host/query/handlers.ts +9 -1
  277. package/src/sdk/host/session-runtime.test.ts +2705 -303
  278. package/src/sdk/host/session-runtime.ts +490 -143
  279. package/src/sdk/host/surface-policy.ts +1 -0
  280. package/src/sdk/index.ts +8 -0
  281. package/src/sdk/lifecycle-session.ts +3 -1
  282. package/src/sdk/prompt-deadline-manager.ts +15 -0
  283. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  284. package/src/sdk/protocol/operation-registry.ts +9 -2
  285. package/src/sdk/router/session-router.ts +82 -18
  286. package/src/sdk/session-list.ts +1 -0
  287. package/src/sdk/session.ts +69 -45
  288. package/src/session/agent-session.ts +679 -238
  289. package/src/session/artifacts.ts +176 -71
  290. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  291. package/src/session/internal/managed-session-scope.ts +3779 -462
  292. package/src/session/internal/managed-session-storage.ts +501 -78
  293. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  294. package/src/session/internal/task-artifact-owner-access.ts +19 -3
  295. package/src/session/internal/task-artifact-owner-transcript.ts +547 -1
  296. package/src/session/session-manager.ts +951 -133
  297. package/src/session/session-retirement.ts +573 -14
  298. package/src/session/session-storage.ts +447 -15
  299. package/src/session/task-artifact-owner-codec.ts +71 -8
  300. package/src/session/task-artifact-owner-retirement.ts +21 -19
  301. package/src/setup/credential-auto-import.ts +12 -3
  302. package/src/setup/credential-import.ts +176 -5
  303. package/src/setup/paseo/announce.ts +6 -1
  304. package/src/skill-state/active-state.ts +145 -2
  305. package/src/skill-state/workflow-mutation-guard.ts +102 -10
  306. package/src/slash-commands/builtin-registry.ts +39 -9
  307. package/src/slash-commands/helpers/mcp.ts +8 -4
  308. package/src/slash-commands/helpers/progress-report.ts +23 -0
  309. package/src/stt/setup.ts +3 -5
  310. package/src/task/commands.ts +3 -1
  311. package/src/task/executor.ts +4 -0
  312. package/src/task/index.ts +6 -0
  313. package/src/task/isolation-dir.ts +143 -0
  314. package/src/task/worktree.ts +16 -7
  315. package/src/tools/ask.ts +205 -6
  316. package/src/tools/ast-edit-write-path.ts +68 -0
  317. package/src/tools/ast-edit.ts +10 -1
  318. package/src/tools/bash-allowed-prefixes.ts +15 -0
  319. package/src/tools/bash-skill-urls.ts +56 -3
  320. package/src/tools/bash.ts +48 -79
  321. package/src/tools/browser/cdp-url.ts +18 -0
  322. package/src/tools/browser.ts +8 -1
  323. package/src/tools/descriptors.ts +5 -1
  324. package/src/tools/eval.ts +56 -36
  325. package/src/tools/gh.ts +10 -0
  326. package/src/tools/github-side-effect.ts +112 -0
  327. package/src/tools/image-gen.ts +1 -1
  328. package/src/tools/index.ts +3 -0
  329. package/src/tools/monitor-mutation.ts +27 -0
  330. package/src/tools/monitor.ts +7 -0
  331. package/src/tools/python.ts +188 -66
  332. package/src/tools/recipe/index.ts +9 -0
  333. package/src/tools/recipe/recipe-guard.ts +19 -0
  334. package/src/tools/restricted-cwd.ts +10 -0
  335. package/src/tools/skill-discovery.ts +5 -0
  336. package/src/tools/skill.ts +28 -1
  337. package/src/tools/todo-write.ts +10 -1
  338. package/src/tools/tool-catalog.generated.ts +1 -1
  339. package/src/tools/write.ts +1 -0
  340. package/src/utils/external-editor.ts +3 -2
  341. package/src/utils/tools-checksum.ts +34 -0
  342. package/src/utils/tools-manager.ts +24 -6
  343. package/src/web/insane/url-guard.ts +2 -1
  344. package/src/web/search/providers/codex.ts +3 -2
@@ -50,13 +50,17 @@ export async function writeMCPConfigFile(filePath: string, config: MCPConfigFile
50
50
  const dir = path.dirname(filePath);
51
51
  await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
52
52
 
53
- // Write to temp file first (atomic write)
54
- const tmpPath = `${filePath}.tmp`;
53
+ // A predictable `${file}.tmp` can already be a symlink planted in the project.
54
+ // Write a unique sibling and fail closed if that path exists.
55
+ const tmpPath = `${filePath}.${crypto.randomUUID()}.tmp`;
55
56
  const content = JSON.stringify(withSchema(config), null, 2);
56
- await fs.promises.writeFile(tmpPath, content, { encoding: "utf-8", mode: 0o600 });
57
-
58
- // Rename to final path (atomic on most systems)
59
- await fs.promises.rename(tmpPath, filePath);
57
+ try {
58
+ await fs.promises.writeFile(tmpPath, content, { encoding: "utf-8", mode: 0o600, flag: "wx" });
59
+ await fs.promises.rename(tmpPath, filePath);
60
+ } catch (error) {
61
+ await fs.promises.rm(tmpPath, { force: true });
62
+ throw error;
63
+ }
60
64
  // Invalidate the capability fs cache so subsequent reads see the new content
61
65
  invalidateFsCache(filePath);
62
66
  }
@@ -103,6 +103,7 @@ function convertToLegacyConfig(server: MCPServer): MCPServerConfig {
103
103
  url: server.url ?? "",
104
104
  };
105
105
  if (server.headers) config.headers = server.headers;
106
+ if (server.publicNetwork === true) config.publicNetwork = true;
106
107
  canonicalizeMCPEndpoint(config.url);
107
108
  return config;
108
109
  }
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { logger } from "@gajae-code/utils";
8
8
  import { cancelMCPStream, MCP_HTTP_TIMEOUT_MS, MCP_MAX_CONTENT_BYTES, readMCPResponseText } from "./content-limits";
9
+ import { fetchMcpRespectingOrigin } from "./mcp-redirect";
9
10
  import { redactMCPDiagnosticValue, redactMCPEndpoint } from "./redaction";
10
11
 
11
12
  /** Parse SSE response format (lines starting with "data: ") */
@@ -70,7 +71,7 @@ export async function callMCP<T = unknown>(
70
71
 
71
72
  const signal = AbortSignal.timeout(MCP_HTTP_TIMEOUT_MS);
72
73
  const response = await translateMCPTimeout(
73
- fetch(url, {
74
+ fetchMcpRespectingOrigin(url, {
74
75
  method: "POST",
75
76
  headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream" },
76
77
  body: JSON.stringify(body),
@@ -34,6 +34,7 @@ import {
34
34
  MCPPoolLeaseObsoleteError,
35
35
  MCPPoolLeaseReleaseError,
36
36
  } from "./pool";
37
+ import { decideUntrustedProjectConfigValue, isUntrustedProjectMcpSource } from "./project-host-values";
37
38
  import type { MCPProtocolObservation } from "./protocol";
38
39
  import { DEFAULT_MCP_STARTUP_WAIT_MS, MAX_MCP_STARTUP_WAIT_MS, MCP_STARTUP_WAIT_GRACE_MS } from "./startup-policy";
39
40
  import type { MCPToolDetails } from "./tool-bridge";
@@ -1313,8 +1314,9 @@ export class MCPManager {
1313
1314
  const connectionAbort = new AbortController();
1314
1315
  this.#pendingConnectionControllers.set(name, connectionAbort);
1315
1316
  // Resolve auth config before connecting, but do so per-server in parallel.
1317
+ const configSource = sources[name] ?? this.#sources.get(name);
1316
1318
  const acquireInitialLease = async (): Promise<MCPPoolLease> => {
1317
- const resolvedConfig = await this.#resolveAuthConfig(config);
1319
+ const resolvedConfig = await this.#resolveAuthConfig(config, false, configSource);
1318
1320
  let lease: MCPPoolLease | undefined;
1319
1321
  try {
1320
1322
  lease = await this.#acquireLease(name, resolvedConfig, config, connectionAbort);
@@ -1371,7 +1373,7 @@ export class MCPManager {
1371
1373
  // Wire auth refresh for HTTP transports, and reconnect for any transport.
1372
1374
  if (connection.transport instanceof HttpTransport && config.auth?.type === "oauth") {
1373
1375
  connection.transport.onAuthError = async () => {
1374
- const refreshed = await this.#resolveAuthConfig(config, true);
1376
+ const refreshed = await this.#resolveAuthConfig(config, true, configSource);
1375
1377
  if (refreshed.type === "http" || refreshed.type === "sse") {
1376
1378
  return refreshed.headers ?? null;
1377
1379
  }
@@ -2152,7 +2154,11 @@ export class MCPManager {
2152
2154
  const config = this.#serverConfigs.get(name);
2153
2155
  if (!config) throw new Error(`MCP server unavailable: ${name}`);
2154
2156
  await this.#closePendingConnectionCleanup(name);
2155
- const resolved = await this.#resolveAuthConfig(config);
2157
+ const resolved = await this.#resolveAuthConfig(
2158
+ config,
2159
+ false,
2160
+ this.#sources.get(name) ?? this.#connections.get(name)?._source,
2161
+ );
2156
2162
  const abort = new AbortController();
2157
2163
  this.#pendingConnectionControllers.set(name, abort);
2158
2164
  let prepared:
@@ -2308,9 +2314,9 @@ export class MCPManager {
2308
2314
  /**
2309
2315
  * Resolve auth and shell-command substitutions in config before connecting.
2310
2316
  */
2311
- async prepareConfig(config: MCPServerConfig): Promise<MCPServerConfig> {
2317
+ async prepareConfig(config: MCPServerConfig, source?: SourceMeta): Promise<MCPServerConfig> {
2312
2318
  this.#assertRawMCPAccessAllowed();
2313
- return this.#resolveAuthConfig(config);
2319
+ return this.#resolveAuthConfig(config, false, source);
2314
2320
  }
2315
2321
 
2316
2322
  /** Acquire a prepared, pool-owned lease for a scoped transient operation. */
@@ -2318,7 +2324,7 @@ export class MCPManager {
2318
2324
  name: string,
2319
2325
  config: MCPServerConfig,
2320
2326
  fn: (lease: MCPPoolLease) => Promise<T> | T,
2321
- options: { signal?: AbortSignal } = {},
2327
+ options: { signal?: AbortSignal; source?: SourceMeta } = {},
2322
2328
  ): Promise<T> {
2323
2329
  this.#assertRawMCPAccessAllowed();
2324
2330
  const lifecycleEpoch = this.#assertScopedAdmission();
@@ -2347,7 +2353,7 @@ export class MCPManager {
2347
2353
  operation: ScopedOperation,
2348
2354
  config: MCPServerConfig,
2349
2355
  fn: (lease: MCPPoolLease) => Promise<T> | T,
2350
- options: { signal?: AbortSignal },
2356
+ options: { signal?: AbortSignal; source?: SourceMeta },
2351
2357
  ): Promise<T> {
2352
2358
  const callerSignal = options.signal;
2353
2359
  const onAbort = () =>
@@ -2376,7 +2382,11 @@ export class MCPManager {
2376
2382
  let failed = false;
2377
2383
  let primaryError: unknown;
2378
2384
  try {
2379
- const resolvedConfigPromise = this.#resolveAuthConfig(config);
2385
+ const resolvedConfigPromise = this.#resolveAuthConfig(
2386
+ config,
2387
+ false,
2388
+ options.source ?? this.#sources.get(operation.name),
2389
+ );
2380
2390
  void resolvedConfigPromise.catch(() => {});
2381
2391
  const resolvedConfig = await Promise.race([resolvedConfigPromise, abortPromise]);
2382
2392
  if (operation.lifecycleEpoch !== this.#scopedLifecycleEpoch || this.#scopedLifecycle !== "open") {
@@ -2857,7 +2867,7 @@ export class MCPManager {
2857
2867
  throw new MCPManagerLifecycleError(this.#scopedLifecycle === "disconnecting" ? "disconnect" : "reconnect");
2858
2868
  };
2859
2869
  assertLifecycle();
2860
- const resolvedConfig = await this.#resolveAuthConfig(config);
2870
+ const resolvedConfig = await this.#resolveAuthConfig(config, false, source);
2861
2871
  assertLifecycle();
2862
2872
  const connectionAbort = new AbortController();
2863
2873
  this.#pendingConnectionControllers.set(name, connectionAbort);
@@ -2909,7 +2919,7 @@ export class MCPManager {
2909
2919
  // Wire auth refresh for HTTP transports, and reconnect for any transport.
2910
2920
  if (connection.transport instanceof HttpTransport && config.auth?.type === "oauth") {
2911
2921
  connection.transport.onAuthError = async () => {
2912
- const refreshed = await this.#resolveAuthConfig(config, true);
2922
+ const refreshed = await this.#resolveAuthConfig(config, true, source);
2913
2923
  if (refreshed.type === "http" || refreshed.type === "sse") {
2914
2924
  return refreshed.headers ?? null;
2915
2925
  }
@@ -3174,7 +3184,11 @@ export class MCPManager {
3174
3184
  /**
3175
3185
  * Resolve OAuth credentials and shell commands in config.
3176
3186
  */
3177
- async #resolveAuthConfig(config: MCPServerConfig, forceRefresh = false): Promise<MCPServerConfig> {
3187
+ async #resolveAuthConfig(
3188
+ config: MCPServerConfig,
3189
+ forceRefresh = false,
3190
+ source?: SourceMeta,
3191
+ ): Promise<MCPServerConfig> {
3178
3192
  let resolved: MCPServerConfig = { ...config };
3179
3193
 
3180
3194
  const auth = config.auth;
@@ -3240,7 +3254,11 @@ export class MCPManager {
3240
3254
  resolved = { ...resolved, noInheritEnv: true };
3241
3255
  }
3242
3256
 
3257
+ const untrustedProject = isUntrustedProjectMcpSource(source, this.#toolsOnly);
3243
3258
  const resolveValue = async (value: string): Promise<string | undefined> => {
3259
+ const decision = decideUntrustedProjectConfigValue(value, untrustedProject);
3260
+ if (decision.action === "drop") return undefined;
3261
+ if (decision.action === "literal") return decision.value;
3244
3262
  try {
3245
3263
  const resolvedValue = await configValue.resolveConfigValue(value);
3246
3264
  if (this.#toolsOnly && !resolvedValue) throw new MCPExpectedFailure();
@@ -0,0 +1,73 @@
1
+ import { cancelMCPStream } from "./content-limits";
2
+
3
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
4
+ const MAX_REDIRECTS = 20;
5
+ const REQUEST_BODY_HEADERS = [
6
+ "content-encoding",
7
+ "content-language",
8
+ "content-length",
9
+ "content-location",
10
+ "content-type",
11
+ ] as const;
12
+
13
+ function rewritesToGet(status: number, method: string): boolean {
14
+ return (
15
+ ((status === 301 || status === 302) && method === "POST") ||
16
+ (status === 303 && method !== "GET" && method !== "HEAD")
17
+ );
18
+ }
19
+
20
+ /**
21
+ * Follow same-origin MCP redirects only.
22
+ *
23
+ * A cross-origin 307/308 would carry custom credential headers, `Mcp-Session-Id`,
24
+ * and the JSON-RPC body. The first request uses `rawUrl` unchanged so a caller
25
+ * that cannot be parsed (diagnostic redaction) still reaches fetch, but with
26
+ * `redirect: "manual"` so that hop is not followed either.
27
+ */
28
+ export async function fetchMcpRespectingOrigin(rawUrl: string, init: BunFetchRequestInit): Promise<Response> {
29
+ let currentUrl: URL;
30
+ try {
31
+ currentUrl = new URL(rawUrl);
32
+ } catch {
33
+ return fetch(rawUrl, { ...init, redirect: "manual" });
34
+ }
35
+
36
+ let requestTarget = rawUrl;
37
+ let currentInit: BunFetchRequestInit = { ...init, redirect: "manual" };
38
+ for (let redirectCount = 0; ; redirectCount++) {
39
+ const response = await fetch(requestTarget, currentInit);
40
+ if (!REDIRECT_STATUSES.has(response.status)) return response;
41
+
42
+ const location = response.headers.get("location");
43
+ if (!location) return response;
44
+ if (redirectCount >= MAX_REDIRECTS) {
45
+ cancelMCPStream(response.body);
46
+ throw new Error("MCP redirect limit exceeded");
47
+ }
48
+
49
+ let nextUrl: URL;
50
+ try {
51
+ nextUrl = new URL(location, currentUrl);
52
+ } catch (error) {
53
+ cancelMCPStream(response.body);
54
+ throw error;
55
+ }
56
+ if (nextUrl.origin !== currentUrl.origin) {
57
+ cancelMCPStream(response.body);
58
+ throw new Error("cross-origin redirects are not allowed");
59
+ }
60
+
61
+ const headers = new Headers(currentInit.headers);
62
+ const method = (currentInit.method ?? "GET").toUpperCase();
63
+ if (rewritesToGet(response.status, method)) {
64
+ for (const name of REQUEST_BODY_HEADERS) headers.delete(name);
65
+ currentInit = { ...currentInit, method: "GET", headers, body: undefined, redirect: "manual" };
66
+ } else {
67
+ currentInit = { ...currentInit, headers, redirect: "manual" };
68
+ }
69
+ currentUrl = nextUrl;
70
+ requestTarget = nextUrl.toString();
71
+ cancelMCPStream(response.body);
72
+ }
73
+ }
@@ -8,6 +8,8 @@
8
8
  import type { OAuthCallbackFlowOptions } from "@gajae-code/ai/utils/oauth/callback-server";
9
9
  import { OAuthCallbackFlow } from "@gajae-code/ai/utils/oauth/callback-server";
10
10
  import type { OAuthController, OAuthCredentials } from "@gajae-code/ai/utils/oauth/types";
11
+ import { guardedPublicFetch } from "../web/insane/url-guard";
12
+ import { assertPublicOAuthUrl } from "./oauth-public-url";
11
13
 
12
14
  const DEFAULT_PORT = 3000;
13
15
  const CALLBACK_PATH = "/callback";
@@ -261,8 +263,10 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
261
263
  params.set("client_secret", clientSecret);
262
264
  }
263
265
 
266
+ await assertPublicOAuthUrl(this.config.tokenUrl, { signal: this.ctrl.signal });
264
267
  const response = await fetch(this.config.tokenUrl, {
265
268
  method: "POST",
269
+ redirect: "error",
266
270
  headers: {
267
271
  "Content-Type": "application/x-www-form-urlencoded",
268
272
  },
@@ -340,8 +344,10 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
340
344
  if (!registrationEndpoint) return;
341
345
 
342
346
  try {
347
+ await assertPublicOAuthUrl(registrationEndpoint, { signal });
343
348
  const response = await fetch(registrationEndpoint, {
344
349
  method: "POST",
350
+ redirect: "error",
345
351
  headers: {
346
352
  "Content-Type": "application/json",
347
353
  Accept: "application/json",
@@ -380,8 +386,10 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
380
386
  try {
381
387
  const authorizationEndpoint = new URL(this.config.authorizationUrl);
382
388
  const metadataUrl = new URL("/.well-known/oauth-authorization-server", authorizationEndpoint.origin);
389
+ await assertPublicOAuthUrl(metadataUrl.toString(), { signal });
383
390
  const response = await fetch(metadataUrl.toString(), {
384
391
  method: "GET",
392
+ redirect: "error",
385
393
  headers: { Accept: "application/json" },
386
394
  signal,
387
395
  });
@@ -401,12 +409,16 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
401
409
 
402
410
  async #assertClientIdNotRequired(authorizationUrl: string, signal?: AbortSignal): Promise<void> {
403
411
  try {
404
- const response = await fetch(authorizationUrl, {
412
+ // Pin the GET to the addresses that passed validation. A plain fetch would
413
+ // resolve the hostname again. Skip the probe when that route cannot be
414
+ // established; the flow signal cancels the DNS lookup.
415
+ const probed = await guardedPublicFetch(authorizationUrl, {
405
416
  method: "GET",
406
- redirect: "manual",
407
417
  headers: { Accept: "text/plain,text/html,application/json" },
408
418
  signal,
409
419
  });
420
+ if (!probed.ok) return;
421
+ const response = probed.response;
410
422
  if (response.status < 400) return;
411
423
  const body = await response.text();
412
424
  if (/client[_-]?id/i.test(body) && /(required|missing|invalid)/i.test(body)) {
@@ -0,0 +1,14 @@
1
+ import { validatePublicHttpUrl } from "../web/insane/url-guard";
2
+
3
+ export async function assertPublicOAuthUrl(url: string, options: { signal?: AbortSignal } = {}): Promise<void> {
4
+ if (options.signal?.aborted) throw abortReason(options.signal);
5
+ const checked = await validatePublicHttpUrl(url, { signal: options.signal });
6
+ if (options.signal?.aborted) throw abortReason(options.signal);
7
+ if (!checked.ok) {
8
+ throw new Error(`Refusing non-public OAuth endpoint: ${checked.reason ?? url}`);
9
+ }
10
+ }
11
+
12
+ function abortReason(signal: AbortSignal): Error {
13
+ return signal.reason instanceof Error ? signal.reason : new Error("OAuth URL validation aborted");
14
+ }
@@ -19,6 +19,10 @@ export function bindPluginMcpToPublicNetwork<T extends MCPHttpServerConfig | MCP
19
19
  }
20
20
 
21
21
  /** The marker is an internal symbol, so persisted user MCP configs cannot opt into or forge this path. */
22
+ export function usesPublicNetworkMcpFetch(config: MCPHttpServerConfig | MCPSseServerConfig): boolean {
23
+ return ("publicNetwork" in config && config.publicNetwork === true) || isPluginMcpPublicNetworkBound(config);
24
+ }
25
+
22
26
  export function isPluginMcpPublicNetworkBound(config: MCPHttpServerConfig | MCPSseServerConfig): boolean {
23
27
  return (
24
28
  (config as MCPHttpServerConfig & { [PLUGIN_MCP_PUBLIC_NETWORK_BOUNDARY]?: boolean })[
@@ -307,6 +307,12 @@ class MCPPoolLeaseImpl implements MCPPoolLease {
307
307
  notify: (method, params) => lease.notify(method, params),
308
308
  close: () => lease.release(),
309
309
  closeBeforeReconnect: physical.transport.closeBeforeReconnect,
310
+ get onRequest() {
311
+ return physical.transport.onRequest;
312
+ },
313
+ set onRequest(handler) {
314
+ physical.transport.onRequest = handler;
315
+ },
310
316
  };
311
317
  const facade = { ...physical, transport };
312
318
  for (const property of ["tools", "resources", "resourceTemplates", "prompts"] as const) {
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Host-value policy for MCP config that comes from an untrusted project file.
3
+ *
4
+ * User mcp.json and an explicit `--mcp-config` file may still substitute
5
+ * secret-named environment variables and run `!` shell commands. A project
6
+ * file (native `.gjc/mcp.json` or a repo-root `mcp.json`) uses the same
7
+ * sensitive-name rule as project `ssh.json`: do not copy those host values
8
+ * into server env or headers, and do not execute `!`.
9
+ */
10
+
11
+ import type { SourceMeta } from "../capability/types";
12
+ import { isSensitiveEnvName } from "../discovery/helpers";
13
+
14
+ const UNTRUSTED_PROJECT_MCP_PROVIDERS = new Set(["native", "mcp-json"]);
15
+
16
+ export function nativeMcpSource(level: "user" | "project", filePath: string): SourceMeta {
17
+ return {
18
+ provider: "native",
19
+ providerName: "GJC",
20
+ path: filePath,
21
+ level,
22
+ };
23
+ }
24
+
25
+ /** True when this server was loaded from untrusted project MCP config. */
26
+ export function isUntrustedProjectMcpSource(source: SourceMeta | undefined, toolsOnly: boolean): boolean {
27
+ if (toolsOnly || !source || source.level !== "project") return false;
28
+ return UNTRUSTED_PROJECT_MCP_PROVIDERS.has(source.provider);
29
+ }
30
+
31
+ export type ProjectHostValueDecision =
32
+ | { action: "resolve" }
33
+ | { action: "drop" }
34
+ | { action: "literal"; value: string };
35
+
36
+ /**
37
+ * Decide how to interpret one env or header value.
38
+ * `drop` means "do not run the shell, and omit the value".
39
+ * `literal` means "do not read process.env".
40
+ */
41
+ export function decideUntrustedProjectConfigValue(value: string, untrusted: boolean): ProjectHostValueDecision {
42
+ if (!untrusted) return { action: "resolve" };
43
+ if (value.startsWith("!")) return { action: "drop" };
44
+ if (isSensitiveEnvName(value)) return { action: "literal", value };
45
+ return { action: "resolve" };
46
+ }
@@ -1,4 +1,5 @@
1
1
  import { logger } from "@gajae-code/utils";
2
+ import { type AddressResolver, validatePublicHttpUrl } from "../web/insane/url-guard";
2
3
  import type { MCPServerConfig } from "./types";
3
4
 
4
5
  const SMITHERY_REGISTRY_BASE_URL = "https://registry.smithery.ai";
@@ -289,6 +290,20 @@ function chooseConnection(
289
290
  return null;
290
291
  }
291
292
 
293
+ export async function resolveSmitheryServerConfig(
294
+ qualifiedName: string,
295
+ selected: { connection: SmitheryConnection; useDirectHttp: boolean },
296
+ options?: { resolver?: AddressResolver },
297
+ ): Promise<MCPServerConfig | null> {
298
+ let useDirectHttp = selected.useDirectHttp;
299
+ const deploymentUrl = selected.connection.deploymentUrl;
300
+ if (useDirectHttp && selected.connection.type === "http" && deploymentUrl) {
301
+ const checked = await validatePublicHttpUrl(deploymentUrl, { resolver: options?.resolver });
302
+ if (!checked.ok) useDirectHttp = false;
303
+ }
304
+ return createConfig(qualifiedName, { connection: selected.connection, useDirectHttp });
305
+ }
306
+
292
307
  function createConfig(
293
308
  qualifiedName: string,
294
309
  selected: { connection: SmitheryConnection; useDirectHttp: boolean },
@@ -297,6 +312,7 @@ function createConfig(
297
312
  return {
298
313
  type: "http",
299
314
  url: selected.connection.deploymentUrl,
315
+ publicNetwork: true,
300
316
  };
301
317
  }
302
318
 
@@ -335,7 +351,10 @@ async function fetchServerDetailsFromEntry(
335
351
  return null;
336
352
  }
337
353
 
338
- function toSearchResult(entry: SmitherySearchEntry, details: SmitheryServerDetails): SmitherySearchResult | null {
354
+ async function toSearchResult(
355
+ entry: SmitherySearchEntry,
356
+ details: SmitheryServerDetails,
357
+ ): Promise<SmitherySearchResult | null> {
339
358
  if (!entry.id) return null;
340
359
  const qualifiedName = normalizeQualifiedName(
341
360
  details.qualifiedName ?? entry.qualifiedName ?? `${entry.namespace}/${entry.slug}`,
@@ -343,7 +362,7 @@ function toSearchResult(entry: SmitherySearchEntry, details: SmitheryServerDetai
343
362
  const selected = chooseConnection(details);
344
363
  if (!selected) return null;
345
364
 
346
- const config = createConfig(qualifiedName, selected);
365
+ const config = await resolveSmitheryServerConfig(qualifiedName, selected);
347
366
  if (!config) return null;
348
367
 
349
368
  const requiredInputs = getSchemaInputs(selected.connection.configSchema);
@@ -450,7 +469,7 @@ export async function searchSmitheryRegistry(
450
469
  try {
451
470
  const details = await fetchServerDetailsFromEntry(entry, { apiKey: options?.apiKey });
452
471
  if (!details) return null;
453
- return toSearchResult(entry, details);
472
+ return await toSearchResult(entry, details);
454
473
  } catch (error) {
455
474
  detailFailures.push({
456
475
  identity: getEntryIdentityKey(entry) ?? entry.id ?? "unknown",
@@ -28,7 +28,8 @@ import {
28
28
  MCP_MAX_SSE_REQUEST_MESSAGES,
29
29
  readMCPResponseText,
30
30
  } from "../content-limits";
31
- import { fetchPluginMcpRequest, isPluginMcpPublicNetworkBound } from "../plugin-network-boundary";
31
+ import { fetchMcpRespectingOrigin } from "../mcp-redirect";
32
+ import { fetchPluginMcpRequest, usesPublicNetworkMcpFetch } from "../plugin-network-boundary";
32
33
  import { buildModernMcpHeaders, type MCPModernClientContext, type MCPProtocolEra, withModernMeta } from "../protocol";
33
34
 
34
35
  /**
@@ -155,9 +156,9 @@ export class HttpTransport implements MCPTransport {
155
156
  }
156
157
 
157
158
  #fetch(init: BunFetchRequestInit): Promise<Response> {
158
- return isPluginMcpPublicNetworkBound(this.config)
159
+ return usesPublicNetworkMcpFetch(this.config)
159
160
  ? fetchPluginMcpRequest(this.config.url, init)
160
- : fetch(this.config.url, init);
161
+ : fetchMcpRespectingOrigin(this.config.url, init);
161
162
  }
162
163
 
163
164
  #trackReader(promise: Promise<void>, controller?: AbortController): void {
@@ -139,6 +139,12 @@ export interface MCPHttpServerConfig extends MCPServerConfigBase {
139
139
  type: "http";
140
140
  url: string;
141
141
  headers?: Record<string, string>;
142
+ /**
143
+ * When true, every request and redirect for this URL is rechecked against
144
+ * the public-network policy. Persisted so a later process cannot fall back
145
+ * to an unguarded fetch. Ordinary localhost MCP servers leave this unset.
146
+ */
147
+ publicNetwork?: boolean;
142
148
  }
143
149
 
144
150
  /** SSE server configuration (deprecated, use HTTP) */
@@ -1,7 +1,13 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { logger } from "@gajae-code/utils";
3
3
  import { lifecycleRequestTimeoutMs } from "../broker/startup-budget";
4
- import { type SdkClient, SdkClientError } from "../client";
4
+ import {
5
+ DEFAULT_SDK_REQUEST_TIMEOUT_MS,
6
+ type SdkClient,
7
+ SdkClientError,
8
+ type SdkDispatchContext,
9
+ type SdkSentRecord,
10
+ } from "../client";
5
11
  import type { AbortScope } from "../host/control/operations";
6
12
  import { assertReverseResponseFrame, ReverseLeaseError } from "../host/reverse-leases";
7
13
  import {
@@ -143,6 +149,9 @@ const ACP_MCP_PRESERVED_LAUNCH_CODES = new Set([
143
149
  "readiness_timeout",
144
150
  "spawn_failed",
145
151
  "worktree_in_use",
152
+ "uncertain_after_send",
153
+ // Attach-only refusal: raised by ensureBroker before any lifecycle request or MCP server.
154
+ "broker_unavailable",
146
155
  ]);
147
156
 
148
157
  /**
@@ -531,7 +540,11 @@ export class AcpSdkAdapter {
531
540
  return envelope?.result ?? response;
532
541
  }
533
542
 
534
- async #requestSession(frame: JsonObject, raw = false, options?: { timeoutMs: number }): Promise<unknown> {
543
+ async #requestSession(
544
+ frame: JsonObject,
545
+ raw = false,
546
+ options?: { timeoutMs: number; deadline?: number },
547
+ ): Promise<unknown> {
535
548
  const router = this.#router;
536
549
  if (!router)
537
550
  throw new AcpSdkAdapterError(
@@ -603,11 +616,68 @@ export class AcpSdkAdapter {
603
616
  // request that named no readiness budget is queued for the default one, so it
604
617
  // needs the same extension rather than the client's generic request deadline.
605
618
  const timeoutMs = lifecycleRequestTimeoutMs(operation, input);
606
- const response = await this.#client.global(operation, input, {
619
+ const budgetMs = timeoutMs ?? DEFAULT_SDK_REQUEST_TIMEOUT_MS;
620
+ const deadline = Date.now() + budgetMs;
621
+ let sent = false;
622
+ let sentRecord: SdkSentRecord | undefined;
623
+ let uncertainty: SdkClientError | undefined;
624
+ const deadlineFailure = () =>
625
+ uncertainty ??
626
+ (sent
627
+ ? new SdkClientError(
628
+ "uncertain_after_send",
629
+ "ACP lifecycle deadline elapsed after dispatch.",
630
+ sentRecord ?? {
631
+ operation,
632
+ idempotencyKey,
633
+ },
634
+ )
635
+ : new SdkClientError("timeout", "ACP lifecycle deadline elapsed before dispatch.", undefined, undefined, {
636
+ transport: true,
637
+ }));
638
+ const options = {
607
639
  idempotencyKey,
608
- ...(timeoutMs === undefined ? {} : { timeoutMs }),
609
- });
610
- return response;
640
+ deadline,
641
+ // SdkClient reads this after connecting, so reconnect and replay spend the same allowance.
642
+ get timeoutMs() {
643
+ return Math.max(0, deadline - Date.now());
644
+ },
645
+ beforeDispatch: () => {
646
+ if (Date.now() >= deadline) throw deadlineFailure();
647
+ },
648
+ onDispatch: (context: SdkDispatchContext) => {
649
+ sent = true;
650
+ if (typeof context.frame.id === "string") sentRecord = client.getSentRecord(context.frame.id);
651
+ },
652
+ };
653
+ const expired = Promise.withResolvers<never>();
654
+ const timer = setTimeout(() => expired.reject(deadlineFailure()), budgetMs);
655
+ const recover = async () => {
656
+ try {
657
+ const result = await client.global(operation, input, options);
658
+ if (Date.now() >= deadline) throw deadlineFailure();
659
+ return result;
660
+ } catch (firstError) {
661
+ if (!(firstError instanceof SdkClientError) || firstError.code !== "uncertain_after_send") throw firstError;
662
+ uncertainty = firstError;
663
+ // Same-key replay joins the original durable claim, within the original deadline.
664
+ // Failure to recover does not prove that the committed operation failed.
665
+ if (Date.now() >= deadline) throw firstError;
666
+ try {
667
+ const result = await client.global(operation, input, options);
668
+ if (Date.now() >= deadline) throw firstError;
669
+ return result;
670
+ } catch (replayError) {
671
+ if (replayError !== firstError) Object.assign(firstError, { recovery: replayError });
672
+ throw firstError;
673
+ }
674
+ }
675
+ };
676
+ try {
677
+ return await Promise.race([recover(), expired.promise]);
678
+ } finally {
679
+ clearTimeout(timer);
680
+ }
611
681
  }
612
682
 
613
683
  async sdkControl(params: { operation: string; input?: JsonObject }): Promise<unknown> {
@@ -646,7 +716,7 @@ export class AcpSdkAdapter {
646
716
  throw new AcpSdkAdapterError("method_not_found", `Unsupported ACP SDK method: ${method}`);
647
717
  }
648
718
 
649
- async registerProvider(provider: AcpProviderRegistration): Promise<void> {
719
+ async registerProvider(provider: AcpProviderRegistration, timeoutMs?: number, deadline?: number): Promise<void> {
650
720
  if (!this.#router)
651
721
  throw new AcpSdkAdapterError(
652
722
  "operation_prohibited",
@@ -665,10 +735,15 @@ export class AcpSdkAdapter {
665
735
  ...(previousLeaseId ? { expectedLeaseId: previousLeaseId } : {}),
666
736
  },
667
737
  true,
738
+ timeoutMs === undefined ? undefined : { timeoutMs, ...(deadline === undefined ? {} : { deadline }) },
668
739
  );
669
740
  const result = object(object(response)?.result) ?? object(response) ?? {};
670
741
  if (typeof result.leaseId !== "string")
671
742
  throw new AcpSdkAdapterError("invalid_reverse_frame", "Provider registration omitted leaseId.");
743
+ if (this.#closed)
744
+ throw new AcpSdkAdapterError("connection_closed", "ACP provider activation closed before acknowledgement.");
745
+ if (deadline !== undefined && Date.now() >= deadline)
746
+ throw new AcpSdkAdapterError("provider_activation_exhausted", "ACP provider activation deadline elapsed.");
672
747
  this.#leases.set(provider.capability, result.leaseId);
673
748
  if (previousLeaseId && previousLeaseId !== result.leaseId) this.#abortReverseForCapability(provider.capability);
674
749
  }
@@ -694,9 +769,20 @@ export class AcpSdkAdapter {
694
769
  const connectionId = attachment?.connectionId;
695
770
  try {
696
771
  for (const provider of this.#providers) {
772
+ const providerRemainingMs = PROVIDER_ACTIVATION_BUDGET_MS - (Date.now() - startedAt);
773
+ if (providerRemainingMs <= 0) throw this.#providerActivationExhausted(attempt, startedAt);
697
774
  try {
698
- await this.registerProvider(provider);
775
+ await this.registerProvider(
776
+ provider,
777
+ Math.max(1, providerRemainingMs),
778
+ startedAt + PROVIDER_ACTIVATION_BUDGET_MS,
779
+ );
780
+ if (Date.now() - startedAt >= PROVIDER_ACTIVATION_BUDGET_MS)
781
+ throw this.#providerActivationExhausted(attempt, startedAt);
699
782
  } catch (error) {
783
+ if (providerErrorCode(error) === "uncertain_after_send") throw error;
784
+ if (Date.now() - startedAt >= PROVIDER_ACTIVATION_BUDGET_MS)
785
+ throw this.#providerActivationExhausted(attempt, startedAt);
700
786
  if (providerErrorCode(error) === "provider_lease_conflict") {
701
787
  this.#leases.delete(provider.capability);
702
788
  this.#abortReverseForCapability(provider.capability);
@@ -725,6 +811,7 @@ export class AcpSdkAdapter {
725
811
  this.#providersActivated = true;
726
812
  return;
727
813
  } catch (error) {
814
+ if (providerErrorCode(error) === "uncertain_after_send") throw error;
728
815
  if (this.#router && attachment !== this.#attachment && !this.#closed) {
729
816
  this.#providersActivated = false;
730
817
  continue;