@cursor/july 0.1.96 → 0.1.97

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 (321) hide show
  1. package/README.md +3 -2
  2. package/dist/bin/agent-serve.js +14 -3
  3. package/dist/channels/origin/api.d.ts +15 -4
  4. package/dist/channels/origin/api.d.ts.map +1 -1
  5. package/dist/channels/origin/api.js +19 -3
  6. package/dist/channels/origin/binding.d.ts +11 -0
  7. package/dist/channels/origin/binding.d.ts.map +1 -1
  8. package/dist/channels/origin/binding.js +41 -25
  9. package/dist/channels/origin/index.d.ts +1 -0
  10. package/dist/channels/origin/index.d.ts.map +1 -1
  11. package/dist/channels/origin/index.js +1 -0
  12. package/dist/channels/origin/origin-channel.d.ts.map +1 -1
  13. package/dist/channels/origin/origin-channel.js +3 -0
  14. package/dist/channels/origin/reactions.d.ts +56 -0
  15. package/dist/channels/origin/reactions.d.ts.map +1 -0
  16. package/dist/channels/origin/reactions.js +67 -0
  17. package/dist/channels/origin/types.d.ts +11 -0
  18. package/dist/channels/origin/types.d.ts.map +1 -1
  19. package/dist/channels/slack/approvals.d.ts +16 -0
  20. package/dist/channels/slack/approvals.d.ts.map +1 -1
  21. package/dist/channels/slack/approvals.js +86 -0
  22. package/dist/channels/slack/cli-project.d.ts +19 -0
  23. package/dist/channels/slack/cli-project.d.ts.map +1 -0
  24. package/dist/channels/slack/cli-project.js +234 -0
  25. package/dist/channels/slack/defaults.d.ts.map +1 -1
  26. package/dist/channels/slack/defaults.js +97 -39
  27. package/dist/channels/slack/init.d.ts +3 -2
  28. package/dist/channels/slack/init.d.ts.map +1 -1
  29. package/dist/channels/slack/init.js +5 -5
  30. package/dist/channels/slack/interactive.d.ts +2 -1
  31. package/dist/channels/slack/interactive.d.ts.map +1 -1
  32. package/dist/channels/slack/interactive.js +15 -4
  33. package/dist/channels/slack/manifest.d.ts +1 -1
  34. package/dist/channels/slack/manifest.js +1 -1
  35. package/dist/channels/slack/setup.d.ts +1 -1
  36. package/dist/channels/slack/setup.d.ts.map +1 -1
  37. package/dist/channels/slack/setup.js +10 -6
  38. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  39. package/dist/channels/slack/slack-channel.js +1 -0
  40. package/dist/channels/slack/tool-approval-events.d.ts.map +1 -1
  41. package/dist/channels/slack/tool-approval-events.js +91 -1
  42. package/dist/docs/404.html +2 -2
  43. package/dist/docs/ab.html +4 -4
  44. package/dist/docs/assets/{app.jXleCmOc.js → app.B3rWNYE1.js} +1 -1
  45. package/dist/docs/assets/chunks/@localSearchIndexroot.BU9nHdnQ.js +1 -0
  46. package/dist/docs/assets/chunks/{VPLocalSearchBox.8LhyglIT.js → VPLocalSearchBox.C1lhqJJR.js} +1 -1
  47. package/dist/docs/assets/chunks/{theme.9ged-pr2.js → theme.B40_SXuv.js} +2 -2
  48. package/dist/docs/assets/{deployment.md.D9msOFOW.js → deployment.md.MFPKqsqI.js} +2 -2
  49. package/dist/docs/assets/{guides_mcp-oauth.md.DPYmBCbV.js → guides_mcp-oauth.md.CN-6YmTJ.js} +4 -4
  50. package/dist/docs/assets/{guides_mcp-oauth.md.DPYmBCbV.lean.js → guides_mcp-oauth.md.CN-6YmTJ.lean.js} +1 -1
  51. package/dist/docs/assets/guides_slack.md.VDXQV3ja.js +22 -0
  52. package/dist/docs/assets/guides_slack.md.VDXQV3ja.lean.js +1 -0
  53. package/dist/docs/assets/index.md.CVeRUOeZ.js +5 -0
  54. package/dist/docs/assets/{reference_agent-config.md.Cp_x38Nl.js → reference_agent-config.md.BHeYMgme.js} +1 -1
  55. package/dist/docs/assets/{reference_cli.md.CCnG-nJx.js → reference_cli.md.RyZf5OTE.js} +9 -6
  56. package/dist/docs/assets/{reference_cli.md.CCnG-nJx.lean.js → reference_cli.md.RyZf5OTE.lean.js} +1 -1
  57. package/dist/docs/assets/{reference_connections.md.BH8Oc0D0.js → reference_connections.md.DxldvyIB.js} +4 -3
  58. package/dist/docs/assets/{reference_http-api.md.D89k1mdm.js → reference_http-api.md.5zOAbV86.js} +1 -1
  59. package/dist/docs/assets/reference_sessions.md.NxFP_CAC.js +1 -0
  60. package/dist/docs/assets/{reference_tools.md.BHeXn2id.js → reference_tools.md.B84gw9Ii.js} +1 -1
  61. package/dist/docs/assets/templates_agents-md.md.CXSMZljE.js +2 -0
  62. package/dist/docs/assets/templates_agents-md.md.CXSMZljE.lean.js +1 -0
  63. package/dist/docs/assets/troubleshooting.md.DCiPBhYs.js +1 -0
  64. package/dist/docs/building-with-agents.html +4 -4
  65. package/dist/docs/concepts.html +4 -4
  66. package/dist/docs/deployment.html +7 -7
  67. package/dist/docs/deployment.md +5 -4
  68. package/dist/docs/evals.html +4 -4
  69. package/dist/docs/guides/agent-to-agent.html +4 -4
  70. package/dist/docs/guides/cloud-runtime.html +4 -4
  71. package/dist/docs/guides/convert-automation.html +4 -4
  72. package/dist/docs/guides/github.html +4 -4
  73. package/dist/docs/guides/human-in-the-loop.html +4 -4
  74. package/dist/docs/guides/mcp-oauth.html +8 -8
  75. package/dist/docs/guides/mcp-oauth.md +26 -8
  76. package/dist/docs/guides/opentelemetry.html +4 -4
  77. package/dist/docs/guides/slack.html +17 -33
  78. package/dist/docs/guides/slack.md +92 -201
  79. package/dist/docs/guides/webhooks.html +4 -4
  80. package/dist/docs/hashmap.json +1 -1
  81. package/dist/docs/hillclimbing.html +4 -4
  82. package/dist/docs/index.html +6 -6
  83. package/dist/docs/index.md +5 -3
  84. package/dist/docs/llms-full.txt +299 -653
  85. package/dist/docs/llms.txt +2 -2
  86. package/dist/docs/quickstart.html +4 -4
  87. package/dist/docs/reference/agent-config.html +6 -6
  88. package/dist/docs/reference/agent-config.md +2 -2
  89. package/dist/docs/reference/artifacts.html +4 -4
  90. package/dist/docs/reference/channels.html +4 -4
  91. package/dist/docs/reference/cli.html +13 -10
  92. package/dist/docs/reference/cli.md +38 -16
  93. package/dist/docs/reference/connections.html +9 -8
  94. package/dist/docs/reference/connections.md +6 -3
  95. package/dist/docs/reference/hooks.html +4 -4
  96. package/dist/docs/reference/http-api.html +6 -6
  97. package/dist/docs/reference/http-api.md +2 -1
  98. package/dist/docs/reference/instructions.html +4 -4
  99. package/dist/docs/reference/playground.html +4 -4
  100. package/dist/docs/reference/project-layout.html +4 -4
  101. package/dist/docs/reference/prompt.html +4 -4
  102. package/dist/docs/reference/schedules.html +4 -4
  103. package/dist/docs/reference/sessions.html +5 -5
  104. package/dist/docs/reference/sessions.md +4 -2
  105. package/dist/docs/reference/skills.html +4 -4
  106. package/dist/docs/reference/subagents.html +4 -4
  107. package/dist/docs/reference/tools.html +6 -6
  108. package/dist/docs/reference/tools.md +9 -2
  109. package/dist/docs/scaffolding-agents.html +4 -4
  110. package/dist/docs/storage.html +4 -4
  111. package/dist/docs/templates/agentic-owners.html +4 -4
  112. package/dist/docs/templates/agents-md.html +28 -0
  113. package/dist/docs/templates/agents-md.md +105 -0
  114. package/dist/docs/templates/code-wiki.html +5 -5
  115. package/dist/docs/templates/demo.html +4 -4
  116. package/dist/docs/templates/pr-autofixer.html +4 -4
  117. package/dist/docs/templates/security-help.html +4 -4
  118. package/dist/docs/templates/security-reviewer.html +5 -5
  119. package/dist/docs/templates/triage.html +4 -4
  120. package/dist/docs/troubleshooting.html +5 -5
  121. package/dist/docs/troubleshooting.md +2 -2
  122. package/dist/evals/run-facts.d.ts.map +1 -1
  123. package/dist/evals/run-facts.js +27 -0
  124. package/dist/internal/advertise-tools.d.ts.map +1 -1
  125. package/dist/internal/advertise-tools.js +15 -9
  126. package/dist/internal/approval-gate.d.ts +12 -13
  127. package/dist/internal/approval-gate.d.ts.map +1 -1
  128. package/dist/internal/approval-gate.js +16 -21
  129. package/dist/internal/chat-client.js +10 -0
  130. package/dist/internal/cli-deploy.d.ts +5 -0
  131. package/dist/internal/cli-deploy.d.ts.map +1 -1
  132. package/dist/internal/cli-deploy.js +50 -17
  133. package/dist/internal/cli-mcp-oauth.js +8 -18
  134. package/dist/internal/cli-slack-manual.d.ts +24 -2
  135. package/dist/internal/cli-slack-manual.d.ts.map +1 -1
  136. package/dist/internal/cli-slack-manual.js +251 -20
  137. package/dist/internal/cli-slack.d.ts +9 -2
  138. package/dist/internal/cli-slack.d.ts.map +1 -1
  139. package/dist/internal/cli-slack.js +29 -82
  140. package/dist/internal/cursor/account-mcp.d.ts +1 -0
  141. package/dist/internal/cursor/account-mcp.d.ts.map +1 -1
  142. package/dist/internal/cursor/account-mcp.js +17 -0
  143. package/dist/internal/cursor-agent-template.d.ts +1 -1
  144. package/dist/internal/cursor-agent-template.d.ts.map +1 -1
  145. package/dist/internal/cursor-agent-template.js +1 -0
  146. package/dist/internal/deploy-client.d.ts +17 -0
  147. package/dist/internal/deploy-client.d.ts.map +1 -1
  148. package/dist/internal/deploy-client.js +31 -10
  149. package/dist/internal/env-file.d.ts +19 -0
  150. package/dist/internal/env-file.d.ts.map +1 -0
  151. package/dist/internal/env-file.js +90 -0
  152. package/dist/internal/framework-file-storage.d.ts +1 -1
  153. package/dist/internal/framework-file-storage.d.ts.map +1 -1
  154. package/dist/internal/framework-file-storage.js +6 -0
  155. package/dist/internal/hosted-delivery-protocol.d.ts +7 -0
  156. package/dist/internal/hosted-delivery-protocol.d.ts.map +1 -1
  157. package/dist/internal/hosted-delivery-protocol.js +7 -0
  158. package/dist/internal/hosted-delivery.d.ts +6 -0
  159. package/dist/internal/hosted-delivery.d.ts.map +1 -1
  160. package/dist/internal/hosted-delivery.js +63 -22
  161. package/dist/internal/hosted-execution-diag.d.ts +48 -0
  162. package/dist/internal/hosted-execution-diag.d.ts.map +1 -0
  163. package/dist/internal/hosted-execution-diag.js +150 -0
  164. package/dist/internal/http-channel.d.ts.map +1 -1
  165. package/dist/internal/http-channel.js +81 -1
  166. package/dist/internal/init-project.d.ts.map +1 -1
  167. package/dist/internal/init-project.js +2 -0
  168. package/dist/internal/logs-client.d.ts +5 -0
  169. package/dist/internal/logs-client.d.ts.map +1 -1
  170. package/dist/internal/logs-client.js +5 -0
  171. package/dist/internal/mcp-endpoint.js +2 -3
  172. package/dist/internal/mcp-host.d.ts +10 -0
  173. package/dist/internal/mcp-host.d.ts.map +1 -1
  174. package/dist/internal/mcp-host.js +57 -8
  175. package/dist/internal/mcp-oauth-on-demand.d.ts +68 -0
  176. package/dist/internal/mcp-oauth-on-demand.d.ts.map +1 -0
  177. package/dist/internal/mcp-oauth-on-demand.js +287 -0
  178. package/dist/internal/mcp-oauth-persist.d.ts +42 -0
  179. package/dist/internal/mcp-oauth-persist.d.ts.map +1 -0
  180. package/dist/internal/mcp-oauth-persist.js +92 -0
  181. package/dist/internal/mcp-oauth.d.ts +1 -0
  182. package/dist/internal/mcp-oauth.d.ts.map +1 -1
  183. package/dist/internal/mcp-oauth.js +10 -2
  184. package/dist/internal/otel/collector.d.ts.map +1 -1
  185. package/dist/internal/otel/collector.js +22 -9
  186. package/dist/internal/server.d.ts +3 -0
  187. package/dist/internal/server.d.ts.map +1 -1
  188. package/dist/internal/server.js +49 -22
  189. package/dist/internal/session-engine.d.ts +88 -3
  190. package/dist/internal/session-engine.d.ts.map +1 -1
  191. package/dist/internal/session-engine.js +515 -43
  192. package/dist/internal/sessions-client.d.ts +9 -7
  193. package/dist/internal/sessions-client.d.ts.map +1 -1
  194. package/dist/internal/sessions-client.js +3 -1
  195. package/dist/internal/storage-coordinator.d.ts +7 -0
  196. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  197. package/dist/internal/storage-coordinator.js +28 -5
  198. package/dist/internal/storage-roles.d.ts +5 -0
  199. package/dist/internal/storage-roles.d.ts.map +1 -1
  200. package/dist/internal/stream-progress.d.ts.map +1 -1
  201. package/dist/internal/stream-progress.js +10 -0
  202. package/dist/internal/tool-policy.d.ts +9 -1
  203. package/dist/internal/tool-policy.d.ts.map +1 -1
  204. package/dist/internal/tool-policy.js +13 -0
  205. package/dist/internal/trajectory.d.ts.map +1 -1
  206. package/dist/internal/trajectory.js +10 -0
  207. package/dist/internal/turn-outbox.d.ts +24 -0
  208. package/dist/internal/turn-outbox.d.ts.map +1 -0
  209. package/dist/internal/turn-outbox.js +80 -0
  210. package/dist/playground/assets/{index-Cx9CclK_.js → index-DrkI6y5O.js} +45 -45
  211. package/dist/playground/assets/index-DzNGwm7q.css +1 -0
  212. package/dist/playground/index.html +2 -2
  213. package/dist/storage-backends/cursor-hosted.d.ts +1 -1
  214. package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
  215. package/dist/storage-backends/cursor-hosted.js +12 -0
  216. package/dist/storage.d.ts +8 -0
  217. package/dist/storage.d.ts.map +1 -1
  218. package/dist/types.d.ts +63 -7
  219. package/dist/types.d.ts.map +1 -1
  220. package/docs/README.md +5 -3
  221. package/docs/deployment.md +5 -4
  222. package/docs/guides/mcp-oauth.md +26 -8
  223. package/docs/guides/slack.md +93 -202
  224. package/docs/reference/agent-config.md +2 -2
  225. package/docs/reference/cli.md +38 -16
  226. package/docs/reference/connections.md +6 -3
  227. package/docs/reference/http-api.md +2 -1
  228. package/docs/reference/sessions.md +4 -2
  229. package/docs/reference/tools.md +9 -2
  230. package/docs/templates/agents-md.md +110 -0
  231. package/docs/troubleshooting.md +2 -2
  232. package/package.json +1 -1
  233. package/skills/create-agent/SKILL.md +2 -1
  234. package/skills/mcp-auth/SKILL.md +6 -3
  235. package/skills/setup-slack/SKILL.md +23 -13
  236. package/src/bin/agent-serve.ts +13 -1
  237. package/src/channels/origin/api.ts +24 -5
  238. package/src/channels/origin/binding.ts +55 -26
  239. package/src/channels/origin/index.ts +1 -0
  240. package/src/channels/origin/origin-channel.ts +4 -0
  241. package/src/channels/origin/reactions.ts +82 -0
  242. package/src/channels/origin/types.ts +11 -0
  243. package/src/channels/slack/approvals.ts +105 -0
  244. package/src/channels/slack/cli-project.ts +267 -0
  245. package/src/channels/slack/defaults.ts +109 -46
  246. package/src/channels/slack/init.ts +10 -5
  247. package/src/channels/slack/interactive.ts +35 -9
  248. package/src/channels/slack/manifest.ts +1 -1
  249. package/src/channels/slack/setup.ts +10 -6
  250. package/src/channels/slack/slack-channel.ts +1 -0
  251. package/src/channels/slack/tool-approval-events.ts +93 -0
  252. package/src/evals/run-facts.ts +32 -0
  253. package/src/internal/advertise-tools.ts +31 -10
  254. package/src/internal/approval-gate.ts +20 -29
  255. package/src/internal/chat-client.ts +19 -0
  256. package/src/internal/cli-deploy.ts +79 -9
  257. package/src/internal/cli-mcp-oauth.ts +7 -21
  258. package/src/internal/cli-slack-manual.ts +350 -18
  259. package/src/internal/cli-slack.ts +40 -84
  260. package/src/internal/cursor/account-mcp.ts +21 -0
  261. package/src/internal/cursor-agent-template.ts +1 -0
  262. package/src/internal/deploy-client.ts +43 -0
  263. package/src/internal/env-file.ts +84 -0
  264. package/src/internal/framework-file-storage.ts +7 -1
  265. package/src/internal/hosted-delivery-protocol.ts +7 -0
  266. package/src/internal/hosted-delivery.ts +100 -27
  267. package/src/internal/hosted-execution-diag.ts +207 -0
  268. package/src/internal/http-channel.ts +99 -0
  269. package/src/internal/init-project.ts +2 -0
  270. package/src/internal/logs-client.ts +5 -0
  271. package/src/internal/mcp-endpoint.ts +13 -4
  272. package/src/internal/mcp-host.ts +77 -11
  273. package/src/internal/mcp-oauth-on-demand.ts +352 -0
  274. package/src/internal/mcp-oauth-persist.ts +127 -0
  275. package/src/internal/mcp-oauth.ts +17 -1
  276. package/src/internal/otel/collector.ts +30 -0
  277. package/src/internal/server.ts +59 -9
  278. package/src/internal/session-engine.ts +659 -56
  279. package/src/internal/sessions-client.ts +17 -8
  280. package/src/internal/storage-coordinator.ts +38 -8
  281. package/src/internal/storage-roles.ts +7 -0
  282. package/src/internal/stream-progress.ts +19 -0
  283. package/src/internal/tool-policy.ts +18 -0
  284. package/src/internal/trajectory.ts +10 -0
  285. package/src/internal/turn-outbox.ts +85 -0
  286. package/src/storage-backends/cursor-hosted.ts +13 -1
  287. package/src/storage.ts +12 -0
  288. package/src/types.ts +68 -8
  289. package/templates/agents-md/README.md +39 -0
  290. package/templates/agents-md/agent/agent.ts +8 -0
  291. package/templates/agents-md/agent/channels/github.ts +10 -0
  292. package/templates/agents-md/agent/channels/queue.ts +28 -0
  293. package/templates/agents-md/agent/channels/slack.ts +9 -0
  294. package/templates/agents-md/agent/instructions.md +39 -0
  295. package/templates/agents-md/agent/lib/collect.ts +439 -0
  296. package/templates/agents-md/agent/lib/config.ts +8 -0
  297. package/templates/agents-md/agent/lib/paths.ts +51 -0
  298. package/templates/agents-md/agent/schedules/weekly.ts +17 -0
  299. package/templates/agents-md/agent/tools/apply_agents.ts +226 -0
  300. package/templates/agents-md/evals/agents.eval.ts +135 -0
  301. package/templates/agents-md/evals/evals.config.ts +5 -0
  302. package/templates/agents-md/init.json +38 -0
  303. package/templates/agents-md/package.json +18 -0
  304. package/templates/agents-md/tsconfig.json +12 -0
  305. package/dist/docs/assets/chunks/@localSearchIndexroot.DFoGuETX.js +0 -1
  306. package/dist/docs/assets/guides_slack.md.C32HsdKk.js +0 -38
  307. package/dist/docs/assets/guides_slack.md.C32HsdKk.lean.js +0 -1
  308. package/dist/docs/assets/index.md.CI93Ltlb.js +0 -5
  309. package/dist/docs/assets/reference_sessions.md.D0mIh4KK.js +0 -1
  310. package/dist/docs/assets/troubleshooting.md.Ctv3T8C2.js +0 -1
  311. package/dist/docs/design/agsh.md +0 -406
  312. package/dist/playground/assets/index-B1DDvBeC.css +0 -1
  313. package/docs/design/agsh.md +0 -406
  314. /package/dist/docs/assets/{deployment.md.D9msOFOW.lean.js → deployment.md.MFPKqsqI.lean.js} +0 -0
  315. /package/dist/docs/assets/{index.md.CI93Ltlb.lean.js → index.md.CVeRUOeZ.lean.js} +0 -0
  316. /package/dist/docs/assets/{reference_agent-config.md.Cp_x38Nl.lean.js → reference_agent-config.md.BHeYMgme.lean.js} +0 -0
  317. /package/dist/docs/assets/{reference_connections.md.BH8Oc0D0.lean.js → reference_connections.md.DxldvyIB.lean.js} +0 -0
  318. /package/dist/docs/assets/{reference_http-api.md.D89k1mdm.lean.js → reference_http-api.md.5zOAbV86.lean.js} +0 -0
  319. /package/dist/docs/assets/{reference_sessions.md.D0mIh4KK.lean.js → reference_sessions.md.NxFP_CAC.lean.js} +0 -0
  320. /package/dist/docs/assets/{reference_tools.md.BHeXn2id.lean.js → reference_tools.md.B84gw9Ii.lean.js} +0 -0
  321. /package/dist/docs/assets/{troubleshooting.md.Ctv3T8C2.lean.js → troubleshooting.md.DCiPBhYs.lean.js} +0 -0
@@ -7,6 +7,7 @@
7
7
  * event handlers and hooks.
8
8
  */
9
9
 
10
+ import { AsyncLocalStorage } from "node:async_hooks";
10
11
  import { mkdir, rm, writeFile } from "node:fs/promises";
11
12
  import { dirname, join } from "node:path";
12
13
  import type { SDKCustomTool, SDKCustomToolResult } from "@cursor/sdk";
@@ -52,12 +53,15 @@ import {
52
53
  type DiscoveredTool,
53
54
  type HookHandler,
54
55
  type HostContext,
56
+ type HostMcpRegistry,
55
57
  type HttpChannelDefinition,
58
+ isCursorAccountTransport,
56
59
  isHostOauthConnectionTransport,
57
60
  isSymbolicConnectionTransport,
58
61
  type JsonValue,
59
62
  type McpToolAnnotations,
60
63
  type PendingApproval,
64
+ type PendingAuth,
61
65
  type ReceiveInput,
62
66
  type ResolvedAgent,
63
67
  type RunnerSubagent,
@@ -79,7 +83,10 @@ import {
79
83
  import { ABCollector } from "./ab-collector.js";
80
84
  import { foldABStream } from "./ab-fold.js";
81
85
  import { type ABSnapshot, buildABSnapshot } from "./ab-snapshot.js";
82
- import { resolveAdvertisedTools } from "./advertise-tools.js";
86
+ import {
87
+ type AdvertiseHostMcp,
88
+ resolveAdvertisedTools,
89
+ } from "./advertise-tools.js";
83
90
  import { ApprovalGate } from "./approval-gate.js";
84
91
  import { ArtifactsStore } from "./artifacts-store.js";
85
92
  import { SessionAuthError, samePrincipal } from "./auth.js";
@@ -125,7 +132,8 @@ import {
125
132
  parseContinuationToken,
126
133
  } from "./ids.js";
127
134
  import { validateToolInput } from "./json-schema.js";
128
- import { McpHost } from "./mcp-host.js";
135
+ import { isMcpNeedsAuthError, McpHost } from "./mcp-host.js";
136
+ import { McpOauthOnDemandHub } from "./mcp-oauth-on-demand.js";
129
137
  import type { OtelCollector } from "./otel/collector.js";
130
138
  import {
131
139
  shouldStartProcessMemoryTelemetry,
@@ -143,7 +151,7 @@ import { syncSkillsToStore } from "./skills-sync.js";
143
151
  import { steeringEnabled } from "./steering.js";
144
152
  import { StorageCoordinator } from "./storage-coordinator.js";
145
153
  import type { FrameworkSessionStore } from "./storage-roles.js";
146
- import { decideToolCall, resolveEffect } from "./tool-policy.js";
154
+ import { decideToolCall, directCallEffect } from "./tool-policy.js";
147
155
  import { normalizeToolResult, toolCallErrorMessage } from "./tool-result.js";
148
156
  import {
149
157
  type EstimateTurnCostFn,
@@ -152,6 +160,7 @@ import {
152
160
  } from "./turn-cost.js";
153
161
  import { TurnGovernor } from "./turn-governor.js";
154
162
  import { TurnLeaseStore } from "./turn-lease-store.js";
163
+ import { TurnOutbox } from "./turn-outbox.js";
155
164
  import {
156
165
  buildAgentsMdContent,
157
166
  buildCloudMemorySection,
@@ -258,6 +267,22 @@ export class ApprovalInterruptedError extends Error {
258
267
  }
259
268
  }
260
269
 
270
+ export class AuthNotFoundError extends Error {
271
+ constructor(sessionId: string, callId: string) {
272
+ super(`No pending MCP auth for session ${sessionId} call ${callId}`);
273
+ this.name = "AuthNotFoundError";
274
+ }
275
+ }
276
+
277
+ export class AuthInterruptedError extends Error {
278
+ constructor(sessionId: string, callId: string) {
279
+ super(
280
+ `MCP auth for session ${sessionId} call ${callId} is no longer live (host may have restarted while the tool was parked)`
281
+ );
282
+ this.name = "AuthInterruptedError";
283
+ }
284
+ }
285
+
261
286
  export interface SessionEngineOptions {
262
287
  project: AgentProject;
263
288
  stateRoot: string;
@@ -312,6 +337,16 @@ interface ActiveToolContext {
312
337
  emit: (payload: SessionEventPayload) => void;
313
338
  }
314
339
 
340
+ type McpAuthParkCtx = {
341
+ sessionId: string;
342
+ turnId: string;
343
+ callId: string;
344
+ toolName: string;
345
+ emit: (payload: SessionEventPayload) => void;
346
+ };
347
+
348
+ const mcpAuthAls = new AsyncLocalStorage<McpAuthParkCtx>();
349
+
315
350
  export interface EngineSendOptions extends SendMessageOptions {
316
351
  mode?: "chat" | "task";
317
352
  /**
@@ -346,6 +381,8 @@ export class SessionEngine {
346
381
  readonly askDedupe: SlackAskDedupeStore;
347
382
  /** Cross-process turn ownership under `<stateRoot>/turn-leases`. */
348
383
  readonly turnLeases: TurnLeaseStore;
384
+ /** Messages for turns another server owns, under `<stateRoot>/outbox`. */
385
+ readonly outbox: TurnOutbox<AdmitTurnOptions>;
349
386
  /**
350
387
  * Runtime for `agent/storage.ts` (`defineStorage`), when
351
388
  * authored. All durable state changes funnel through it (session
@@ -370,6 +407,13 @@ export class SessionEngine {
370
407
  /** Sessions with a turn being admitted (before the run promise exists). */
371
408
  private readonly pendingTurns = new Set<string>();
372
409
  private readonly activeTurns = new Map<string, Promise<TurnOutcome>>();
410
+ /**
411
+ * Sessions whose workspace a turn in this process has materialized. A
412
+ * session is busy from admit — before the governor slot and the turn's
413
+ * own materialize — so a read running alongside a turn consults this,
414
+ * not busy, to know whether the workspace is ready to be left alone.
415
+ */
416
+ private readonly materializedWorkspaces = new Set<string>();
373
417
  /** Context used by the cloud turn's HTTP MCP callbacks. */
374
418
  private readonly activeToolContexts = new Map<string, ActiveToolContext>();
375
419
  /**
@@ -410,6 +454,11 @@ export class SessionEngine {
410
454
  private readonly mcpHost: McpHost;
411
455
  private readonly platforms: HostPlatforms;
412
456
  private readonly approvals = new ApprovalGate();
457
+ private readonly auths = new ApprovalGate<PendingAuth>();
458
+ private readonly oauthHub = new McpOauthOnDemandHub();
459
+ private probeCursorAccount:
460
+ | ((connection: string) => Promise<string | undefined>)
461
+ | undefined;
413
462
  private readonly abCollector: ABCollector;
414
463
  private readonly otel: OtelCollector | undefined;
415
464
  private readonly otelHost: HostOtelSessionApi;
@@ -458,6 +507,7 @@ export class SessionEngine {
458
507
  this.turnLeases = new TurnLeaseStore(
459
508
  join(options.stateRoot, "turn-leases")
460
509
  );
510
+ this.outbox = new TurnOutbox(join(options.stateRoot, "outbox"));
461
511
  this.logs = new EventLogStore(
462
512
  (sessionId) => this.sessions.eventFilePath(sessionId),
463
513
  (sessionId, error) =>
@@ -565,10 +615,25 @@ export class SessionEngine {
565
615
  this.cloudServerToolsConnection = connection;
566
616
  }
567
617
 
618
+ setMcpOauthCallbackUrl(url: string): void {
619
+ this.oauthHub.callbackUrl = url;
620
+ this.mcpHost.setOAuthRedirectUrl(url);
621
+ }
622
+
623
+ setCursorAccountAuthProbe(
624
+ probe: (connection: string) => Promise<string | undefined>
625
+ ): void {
626
+ this.probeCursorAccount = probe;
627
+ }
628
+
629
+ get oauthCallbackHub(): McpOauthOnDemandHub {
630
+ return this.oauthHub;
631
+ }
632
+
568
633
  /** Shared host services (MCP / GitHub / Slack / reminders / evals / kv / files / otel / Slack nudges). */
569
634
  get host(): HostContext {
570
635
  return {
571
- mcp: this.mcpHost,
636
+ mcp: this.wrapMcp(this.mcpHost),
572
637
  github: this.platforms.github,
573
638
  slack: this.platforms.slack,
574
639
  reminders: this.reminderApi,
@@ -963,7 +1028,7 @@ export class SessionEngine {
963
1028
  channelId,
964
1029
  mode: sendOptions.mode ?? "chat",
965
1030
  purpose,
966
- ...(sendOptions.dryRun === true ? { dryRun: true } : {}),
1031
+ dryRun: sendOptions.dryRun === true ? true : undefined,
967
1032
  continuationKey: sendOptions.continuationToken ?? newContinuationKey(),
968
1033
  auth,
969
1034
  title: sendOptions.title,
@@ -981,7 +1046,10 @@ export class SessionEngine {
981
1046
  this.storage?.sessionRecord(record);
982
1047
  await this.appendEvent(record.sessionId, {
983
1048
  type: "session.started",
984
- data: { channelId },
1049
+ data: {
1050
+ channelId,
1051
+ dryRun: record.dryRun === true ? true : undefined,
1052
+ },
985
1053
  });
986
1054
  record = await this.enrollABs(record);
987
1055
  const since = (await this.logs.get(record.sessionId)).count;
@@ -1231,7 +1299,14 @@ export class SessionEngine {
1231
1299
  const prior = this.admissionChains.get(sessionId) ?? Promise.resolve();
1232
1300
  const admitted = prior
1233
1301
  .catch(() => {})
1234
- .then(() => this.admitTurn(sessionId, message, options));
1302
+ .then(async () => {
1303
+ // Another server holds the turn: hand the message to it and coalesce.
1304
+ if (await this.turnLeases.runningElsewhere(sessionId)) {
1305
+ await this.outbox.post(sessionId, { message, options });
1306
+ return true;
1307
+ }
1308
+ return this.admitTurn(sessionId, message, options);
1309
+ });
1235
1310
  this.admissionChains.set(
1236
1311
  sessionId,
1237
1312
  admitted.catch(() => {})
@@ -1239,6 +1314,20 @@ export class SessionEngine {
1239
1314
  return admitted;
1240
1315
  }
1241
1316
 
1317
+ /** Admit what other servers handed to this turn's holder. Never awaited by the lifecycle. */
1318
+ private drainOutbox(sessionId: string): void {
1319
+ void this.outbox
1320
+ .takeAll(sessionId)
1321
+ .then((items) => {
1322
+ for (const item of items) {
1323
+ void this.startTurn(sessionId, item.message, item.options).catch(
1324
+ () => {}
1325
+ );
1326
+ }
1327
+ })
1328
+ .catch(() => {});
1329
+ }
1330
+
1242
1331
  private enqueueCoalesced(sessionId: string, item: CoalescedQueuedTurn): void {
1243
1332
  const queue = this.coalesceQueues.get(sessionId) ?? [];
1244
1333
  if (
@@ -1476,6 +1565,8 @@ export class SessionEngine {
1476
1565
  let lifecycle!: Promise<TurnOutcome>;
1477
1566
  lifecycle = (async (): Promise<TurnOutcome> => {
1478
1567
  let outcome: TurnOutcome;
1568
+ const poll = setInterval(() => this.drainOutbox(sessionId), 1_000);
1569
+ poll.unref();
1479
1570
  try {
1480
1571
  outcome = await this.executeTurn(
1481
1572
  record,
@@ -1489,6 +1580,8 @@ export class SessionEngine {
1489
1580
  status: "error",
1490
1581
  errorMessage: describeError(error),
1491
1582
  };
1583
+ } finally {
1584
+ clearInterval(poll);
1492
1585
  }
1493
1586
  this.lastOutcomes.set(sessionId, outcome);
1494
1587
  const wasCurrent = this.activeTurns.get(sessionId) === lifecycle;
@@ -1518,6 +1611,10 @@ export class SessionEngine {
1518
1611
  // The drain may have nested a successor turn, which took its own
1519
1612
  // reference; the lease outlives this release until that one ends too.
1520
1613
  await lease.release();
1614
+ // Handed off between the last poll and the release: nobody else will.
1615
+ if (!abort.signal.aborted) {
1616
+ this.drainOutbox(sessionId);
1617
+ }
1521
1618
  return outcome;
1522
1619
  })();
1523
1620
  this.activeTurns.set(sessionId, lifecycle);
@@ -1562,6 +1659,7 @@ export class SessionEngine {
1562
1659
  this.coalesceDrains.has(sessionId) ||
1563
1660
  (this.coalesceQueues.get(sessionId)?.length ?? 0) > 0;
1564
1661
  this.clearCoalesced(sessionId);
1662
+ await this.outbox.clear(sessionId);
1565
1663
  const record = await this.sessions.get(sessionId);
1566
1664
  const continuationKey = record?.continuationKey;
1567
1665
  if (continuationKey !== undefined && continuationKey !== null) {
@@ -1795,6 +1893,7 @@ export class SessionEngine {
1795
1893
  seed: isFirstTurn,
1796
1894
  stateRoot: this.stateRoot,
1797
1895
  });
1896
+ this.materializedWorkspaces.add(sessionId);
1798
1897
  // Must precede the carrier: the harness resolves rules, skills,
1799
1898
  // `mcp.json` and hooks from its git root, so a workspace nested in an
1800
1899
  // outer repo would load that repo's hooks instead of the ones written
@@ -1879,11 +1978,14 @@ export class SessionEngine {
1879
1978
  }
1880
1979
  } catch (error) {
1881
1980
  this.approvals.cancelSession(sessionId, "deny");
1981
+ this.auths.cancelSession(sessionId, "deny");
1882
1982
  outcome = { status: "error", errorMessage: describeError(error) };
1883
1983
  } finally {
1884
1984
  permit.release();
1885
1985
  this.approvals.cancelSession(sessionId, "deny");
1986
+ this.auths.cancelSession(sessionId, "deny");
1886
1987
  await this.clearPendingApprovals(sessionId);
1988
+ await this.clearPendingAuths(sessionId);
1887
1989
  // Anything still queued belongs to the next turn's boundary drain, not
1888
1990
  // to this turn's edge detection.
1889
1991
  this.deliveredNudges.delete(sessionId);
@@ -2518,11 +2620,21 @@ export class SessionEngine {
2518
2620
  // named passthroughs resolved fresh on every turn — reminder fires and
2519
2621
  // post-restart follow-ups included — and a listing failure throws,
2520
2622
  // failing the turn closed.
2521
- for (const tool of await resolveAdvertisedTools({
2522
- agent,
2523
- session: this.sessionInfo(record),
2524
- hostMcp: this.mcpHost,
2525
- })) {
2623
+ for (const tool of await mcpAuthAls.run(
2624
+ {
2625
+ sessionId: record.sessionId,
2626
+ turnId,
2627
+ callId: newToolCallId("mcp"),
2628
+ toolName: "mcp",
2629
+ emit,
2630
+ },
2631
+ () =>
2632
+ resolveAdvertisedTools({
2633
+ agent,
2634
+ session: this.sessionInfo(record),
2635
+ hostMcp: this.advertiseMcp(),
2636
+ })
2637
+ )) {
2526
2638
  register(tool);
2527
2639
  }
2528
2640
  return tools;
@@ -2672,7 +2784,16 @@ export class SessionEngine {
2672
2784
  }
2673
2785
  const started = Date.now();
2674
2786
  this.logger(`[session] tool ${args.tool.name} start`);
2675
- const result = await execute(validation.value as never, toolCtx);
2787
+ const result = await mcpAuthAls.run(
2788
+ {
2789
+ sessionId: args.record.sessionId,
2790
+ turnId: args.turnId,
2791
+ callId: toolCallId,
2792
+ toolName: args.tool.name,
2793
+ emit: args.emit,
2794
+ },
2795
+ () => execute(validation.value as never, toolCtx)
2796
+ );
2676
2797
  this.logger(
2677
2798
  `[session] tool ${args.tool.name} done (${Date.now() - started}ms)`
2678
2799
  );
@@ -2708,10 +2829,10 @@ export class SessionEngine {
2708
2829
  args: args.args,
2709
2830
  requestedAt,
2710
2831
  };
2711
- const resolutionPromise = this.approvals.wait({
2712
- sessionId: args.sessionId,
2713
- ...pendingRecord,
2714
- });
2832
+ const resolutionPromise = this.approvals.wait(
2833
+ args.sessionId,
2834
+ pendingRecord
2835
+ );
2715
2836
  await this.updateRecord(args.sessionId, (current) => ({
2716
2837
  ...current,
2717
2838
  pendingApprovals: [
@@ -2831,6 +2952,318 @@ export class SessionEngine {
2831
2952
  }));
2832
2953
  }
2833
2954
 
2955
+ private wrapMcp(inner: McpHost): HostMcpRegistry {
2956
+ return {
2957
+ names: () => inner.names(),
2958
+ listTools: (name) => this.withMcpAuth(name, () => inner.listTools(name)),
2959
+ callTool: (name, tool, args) =>
2960
+ this.withMcpAuth(name, () => inner.callTool(name, tool, args)),
2961
+ };
2962
+ }
2963
+
2964
+ private advertiseMcp(): AdvertiseHostMcp {
2965
+ return {
2966
+ listTools: (name) =>
2967
+ this.withMcpListAuth(name, () => this.mcpHost.listTools(name)),
2968
+ callTool: (name, tool, args) =>
2969
+ this.withMcpAuth(name, () => this.mcpHost.callTool(name, tool, args)),
2970
+ oneOff: (connection) => {
2971
+ const inner = this.mcpHost.oneOff(connection);
2972
+ return {
2973
+ listTools: () =>
2974
+ this.withMcpListAuth(connection.name, () => inner.listTools()),
2975
+ callTool: (tool, args) =>
2976
+ this.withMcpAuth(connection.name, () => inner.callTool(tool, args)),
2977
+ };
2978
+ },
2979
+ };
2980
+ }
2981
+
2982
+ private withMcpListAuth<T>(
2983
+ connection: string,
2984
+ run: () => Promise<T>
2985
+ ): Promise<T> {
2986
+ const ctx = mcpAuthAls.getStore();
2987
+ if (ctx === undefined) {
2988
+ return this.withMcpAuth(connection, run);
2989
+ }
2990
+ const toolName = `mcp:${connection}`;
2991
+ return mcpAuthAls.run(
2992
+ {
2993
+ ...ctx,
2994
+ callId: newToolCallId(toolName),
2995
+ toolName,
2996
+ },
2997
+ () => this.withMcpAuth(connection, run)
2998
+ );
2999
+ }
3000
+
3001
+ private async withMcpAuth<T>(
3002
+ connection: string,
3003
+ run: () => Promise<T>
3004
+ ): Promise<T> {
3005
+ try {
3006
+ return await run();
3007
+ } catch (error) {
3008
+ if (!isMcpNeedsAuthError(error)) {
3009
+ throw error;
3010
+ }
3011
+ return await this.parkAndRetryMcp(connection, run, error);
3012
+ }
3013
+ }
3014
+
3015
+ private async parkAndRetryMcp<T>(
3016
+ connection: string,
3017
+ run: () => Promise<T>,
3018
+ error: unknown
3019
+ ): Promise<T> {
3020
+ const ctx = mcpAuthAls.getStore();
3021
+ if (ctx === undefined) {
3022
+ throw error;
3023
+ }
3024
+ const minted = await this.mintAuthUrl(connection, ctx.sessionId);
3025
+ if (minted === undefined) {
3026
+ throw error;
3027
+ }
3028
+ if ("authorized" in minted) {
3029
+ await this.persistHostedMcpOauth(connection);
3030
+ await this.mcpHost.dropClient(connection);
3031
+ return await this.withMcpAuth(connection, run);
3032
+ }
3033
+ if (minted.state !== "") {
3034
+ const bind = this.oauthHub.bind(minted.state, ctx.sessionId, ctx.callId);
3035
+ if (bind === "already_done") {
3036
+ await this.persistHostedMcpOauth(connection);
3037
+ await this.mcpHost.dropClient(connection);
3038
+ return await this.withMcpAuth(connection, run);
3039
+ }
3040
+ }
3041
+ const decision = await this.parkForAuth({
3042
+ sessionId: ctx.sessionId,
3043
+ turnId: ctx.turnId,
3044
+ callId: ctx.callId,
3045
+ toolName: ctx.toolName,
3046
+ connection,
3047
+ authUrl: minted.authUrl,
3048
+ emit: ctx.emit,
3049
+ });
3050
+ if (decision === "deny") {
3051
+ throw new Error(
3052
+ `MCP connection "${connection}" authorization was denied.`
3053
+ );
3054
+ }
3055
+ await this.persistHostedMcpOauth(connection);
3056
+ await this.mcpHost.dropClient(connection);
3057
+ return await this.withMcpAuth(connection, run);
3058
+ }
3059
+
3060
+ private async persistHostedMcpOauth(connection: string): Promise<void> {
3061
+ const authored = this.authoredConnection(connection);
3062
+ if (
3063
+ authored === undefined ||
3064
+ !isHostOauthConnectionTransport(authored.transport)
3065
+ ) {
3066
+ return;
3067
+ }
3068
+ await this.oauthHub.persistFor(connection, authored.transport.url);
3069
+ }
3070
+
3071
+ private authoredConnection(name: string) {
3072
+ return (
3073
+ this.project.agent.connections.find((entry) => entry.name === name) ??
3074
+ this.project.agent.hostConnections?.find((entry) => entry.name === name)
3075
+ );
3076
+ }
3077
+
3078
+ private async mintAuthUrl(
3079
+ connection: string,
3080
+ sessionId: string
3081
+ ): Promise<
3082
+ { authorized: true } | { authUrl: string; state: string } | undefined
3083
+ > {
3084
+ const authored = this.authoredConnection(connection);
3085
+ if (authored === undefined) {
3086
+ return undefined;
3087
+ }
3088
+ if (isCursorAccountTransport(authored.transport)) {
3089
+ const probe = this.probeCursorAccount;
3090
+ if (probe === undefined) {
3091
+ return undefined;
3092
+ }
3093
+ const authUrl = await probe(connection);
3094
+ if (authUrl === undefined || authUrl === "") {
3095
+ return undefined;
3096
+ }
3097
+ return { authUrl, state: "" };
3098
+ }
3099
+ if (!isHostOauthConnectionTransport(authored.transport)) {
3100
+ return undefined;
3101
+ }
3102
+ return await this.oauthHub.mint({
3103
+ connection,
3104
+ url: authored.transport.url,
3105
+ type: authored.transport.type === "sse" ? "sse" : "http",
3106
+ headers: authored.transport.headers,
3107
+ sessionId,
3108
+ });
3109
+ }
3110
+
3111
+ private async parkForAuth(args: {
3112
+ sessionId: string;
3113
+ turnId: string;
3114
+ callId: string;
3115
+ toolName: string;
3116
+ connection: string;
3117
+ authUrl: string;
3118
+ emit: (payload: SessionEventPayload) => void;
3119
+ }): Promise<ApprovalDecision> {
3120
+ const requestedAt = new Date().toISOString();
3121
+ const pendingRecord: PendingAuth = {
3122
+ callId: args.callId,
3123
+ turnId: args.turnId,
3124
+ toolName: args.toolName,
3125
+ connection: args.connection,
3126
+ authUrl: args.authUrl,
3127
+ requestedAt,
3128
+ };
3129
+ const resolutionPromise = this.auths.wait(args.sessionId, pendingRecord);
3130
+ await this.updateRecord(args.sessionId, (current) => ({
3131
+ ...current,
3132
+ pendingAuths: [
3133
+ ...(current.pendingAuths ?? []).filter(
3134
+ (item) => item.callId !== args.callId
3135
+ ),
3136
+ pendingRecord,
3137
+ ],
3138
+ }));
3139
+ args.emit({
3140
+ type: "action.auth_requested",
3141
+ data: {
3142
+ callId: args.callId,
3143
+ toolName: args.toolName,
3144
+ connection: args.connection,
3145
+ authUrl: args.authUrl,
3146
+ },
3147
+ });
3148
+ this.logger(
3149
+ `[session] tool ${args.toolName} awaiting MCP auth ${args.connection} (${args.callId})`
3150
+ );
3151
+ const { decision, by } = await resolutionPromise;
3152
+ await this.updateRecord(args.sessionId, (current) => ({
3153
+ ...current,
3154
+ pendingAuths: (current.pendingAuths ?? []).filter(
3155
+ (item) => item.callId !== args.callId
3156
+ ),
3157
+ }));
3158
+ args.emit({
3159
+ type: "action.auth_resolved",
3160
+ data: {
3161
+ callId: args.callId,
3162
+ toolName: args.toolName,
3163
+ connection: args.connection,
3164
+ decision,
3165
+ by,
3166
+ },
3167
+ });
3168
+ this.logger(
3169
+ `[session] tool ${args.toolName} MCP auth ${decision} (${args.callId})`
3170
+ );
3171
+ return decision;
3172
+ }
3173
+
3174
+ private async clearPendingAuths(sessionId: string): Promise<void> {
3175
+ await this.oauthHub.abortSession(sessionId);
3176
+ const current = await this.sessions.get(sessionId);
3177
+ if (
3178
+ current === undefined ||
3179
+ current.pendingAuths === undefined ||
3180
+ current.pendingAuths.length === 0
3181
+ ) {
3182
+ return;
3183
+ }
3184
+ await this.updateRecord(sessionId, (record) => ({
3185
+ ...record,
3186
+ pendingAuths: [],
3187
+ }));
3188
+ }
3189
+
3190
+ async listPendingAuths(sessionId: string): Promise<PendingAuth[]> {
3191
+ const record = await this.sessions.get(sessionId);
3192
+ if (record === undefined) {
3193
+ return [];
3194
+ }
3195
+ const live = this.auths.list(sessionId);
3196
+ if (live.length > 0) {
3197
+ return live;
3198
+ }
3199
+ return record.pendingAuths ?? [];
3200
+ }
3201
+
3202
+ async resolveAuth(
3203
+ sessionId: string,
3204
+ callId: string,
3205
+ decision: ApprovalDecision,
3206
+ auth: AuthContext | null,
3207
+ options?: { asSessionOwner?: boolean }
3208
+ ): Promise<PendingAuth> {
3209
+ const record = await this.sessions.get(sessionId);
3210
+ if (record === undefined) {
3211
+ throw new AuthNotFoundError(sessionId, callId);
3212
+ }
3213
+ const ownershipAuth = options?.asSessionOwner === true ? record.auth : auth;
3214
+ if (!samePrincipal(record.auth, ownershipAuth)) {
3215
+ throw new SessionAuthError(sessionId);
3216
+ }
3217
+ const pending =
3218
+ (record.pendingAuths ?? []).find((item) => item.callId === callId) ??
3219
+ this.auths.list(sessionId).find((item) => item.callId === callId);
3220
+ if (pending === undefined) {
3221
+ throw new AuthNotFoundError(sessionId, callId);
3222
+ }
3223
+ const resolvedBy = approvalResolverLabel(auth);
3224
+ if (decision === "deny") {
3225
+ await this.oauthHub.abortCall(sessionId, callId);
3226
+ }
3227
+ if (!this.auths.resolve(sessionId, callId, decision, resolvedBy)) {
3228
+ await this.removePendingAuth(sessionId, callId);
3229
+ throw new AuthInterruptedError(sessionId, callId);
3230
+ }
3231
+ return pending;
3232
+ }
3233
+
3234
+ private async removePendingAuth(
3235
+ sessionId: string,
3236
+ callId: string
3237
+ ): Promise<void> {
3238
+ await this.updateRecord(sessionId, (current) => ({
3239
+ ...current,
3240
+ pendingAuths: (current.pendingAuths ?? []).filter(
3241
+ (item) => item.callId !== callId
3242
+ ),
3243
+ }));
3244
+ }
3245
+
3246
+ async runWithMcpPark<T>(
3247
+ sessionId: string,
3248
+ toolName: string,
3249
+ fn: () => Promise<T>
3250
+ ): Promise<T> {
3251
+ const active = this.activeToolContexts.get(sessionId);
3252
+ if (active === undefined) {
3253
+ return fn();
3254
+ }
3255
+ return mcpAuthAls.run(
3256
+ {
3257
+ sessionId,
3258
+ turnId: active.turnId,
3259
+ callId: newToolCallId(toolName),
3260
+ toolName,
3261
+ emit: active.emit,
3262
+ },
3263
+ fn
3264
+ );
3265
+ }
3266
+
2834
3267
  // ==========================================================================
2835
3268
  // Direct tool calls (deterministic invocation, no model turn)
2836
3269
  // ==========================================================================
@@ -2846,8 +3279,10 @@ export class SessionEngine {
2846
3279
  * and materialized workspace) and the call is recorded on the session's
2847
3280
  * event stream as `actions.requested` / `action.result` under a
2848
3281
  * per-call `turnId` (the call id), so it shows up in trajectories.
2849
- * Rejected with {@link SessionBusyError} while a model turn is running.
2850
- * Without a session, the tool runs against an **ephemeral** scratch
3282
+ * While a model turn is running, a read-effect call runs alongside it
3283
+ * (the turn's workspace is read as-is) and a write-effect call is
3284
+ * rejected with {@link SessionBusyError}. Without a session, the tool
3285
+ * runs against an **ephemeral** scratch
2851
3286
  * workspace under `<stateRoot>/tool-calls/<callId>` (materialized with
2852
3287
  * seed files, removed after the call returns).
2853
3288
  *
@@ -2873,6 +3308,12 @@ export class SessionEngine {
2873
3308
  session: SessionInfo;
2874
3309
  workspaceDir: string;
2875
3310
  scratchDir: string | undefined;
3311
+ /**
3312
+ * A model turn is running or queued on the session. Reported, not
3313
+ * enforced: what it means depends on the call — a write waits, a read
3314
+ * (including the catalog) runs alongside the turn.
3315
+ */
3316
+ busy: boolean;
2876
3317
  }> {
2877
3318
  if (
2878
3319
  options.sessionId !== undefined &&
@@ -2898,9 +3339,9 @@ export class SessionEngine {
2898
3339
  );
2899
3340
  }
2900
3341
  // A token that maps to a live (or restorable) session takes the
2901
- // session path: same owner check, same turn serialization, same
2902
- // event recording as an explicit sessionId. Skipping those just
2903
- // because the caller addressed the session by token would fail open.
3342
+ // session path: same owner check, same busy semantics, same event
3343
+ // recording as an explicit sessionId. Skipping those just because
3344
+ // the caller addressed the session by token would fail open.
2904
3345
  record =
2905
3346
  (await this.sessions.findByContinuation(
2906
3347
  continuation.channelId,
@@ -2915,19 +3356,14 @@ export class SessionEngine {
2915
3356
  }
2916
3357
  }
2917
3358
  if (record !== undefined) {
2918
- // Direct calls respect the same serialization as follow-up sends: a
2919
- // running model turn owns the session workspace and event stream.
2920
- if (
2921
- this.activeTurns.has(record.sessionId) ||
2922
- this.pendingTurns.has(record.sessionId)
2923
- ) {
2924
- throw new SessionBusyError(record.sessionId);
2925
- }
2926
3359
  return {
2927
3360
  record,
2928
3361
  session: this.sessionInfo(record),
2929
3362
  workspaceDir: this.harnessCwd(record),
2930
3363
  scratchDir: undefined,
3364
+ busy:
3365
+ this.activeTurns.has(record.sessionId) ||
3366
+ this.pendingTurns.has(record.sessionId),
2931
3367
  };
2932
3368
  }
2933
3369
  // No session behind the token (or no token at all): a synthetic
@@ -2947,6 +3383,7 @@ export class SessionEngine {
2947
3383
  },
2948
3384
  workspaceDir: scratchDir,
2949
3385
  scratchDir,
3386
+ busy: false,
2950
3387
  };
2951
3388
  }
2952
3389
 
@@ -2989,6 +3426,43 @@ export class SessionEngine {
2989
3426
  };
2990
3427
  }
2991
3428
 
3429
+ /**
3430
+ * One catalog tool by name. Authored tools are local — their schema does
3431
+ * not depend on session identity or connection listings — so a detail
3432
+ * fetch for `ls` must not wait on every advertised MCP server.
3433
+ */
3434
+ async describeCatalogTool(
3435
+ toolName: string,
3436
+ options: Pick<
3437
+ CallToolOptions,
3438
+ "auth" | "sessionId" | "continuationToken"
3439
+ > = {}
3440
+ ): Promise<
3441
+ | { tool: DiscoveredTool }
3442
+ | {
3443
+ tool: undefined;
3444
+ availableTools: string[];
3445
+ connectionErrors: Array<{ connection: string; message: string }>;
3446
+ }
3447
+ > {
3448
+ const authored = this.project.agent.tools.find(
3449
+ (tool) => tool.name === toolName
3450
+ );
3451
+ if (authored !== undefined) {
3452
+ return { tool: authored };
3453
+ }
3454
+ const catalog = await this.listCatalogTools(options);
3455
+ const tool = catalog.tools.find((entry) => entry.name === toolName);
3456
+ if (tool === undefined) {
3457
+ return {
3458
+ tool: undefined,
3459
+ availableTools: catalog.tools.map((entry) => entry.name),
3460
+ connectionErrors: catalog.connectionErrors,
3461
+ };
3462
+ }
3463
+ return { tool };
3464
+ }
3465
+
2992
3466
  async callTool(
2993
3467
  toolName: string,
2994
3468
  input: unknown = {},
@@ -3015,7 +3489,7 @@ export class SessionEngine {
3015
3489
  const advertised = await resolveAdvertisedTools({
3016
3490
  agent: this.project.agent,
3017
3491
  session,
3018
- hostMcp: this.mcpHost,
3492
+ hostMcp: this.advertiseMcp(),
3019
3493
  });
3020
3494
  tool = advertised.find((t) => t.name === toolName);
3021
3495
  }
@@ -3035,6 +3509,19 @@ export class SessionEngine {
3035
3509
  if (!validation.ok) {
3036
3510
  throw new ToolInputError({ toolName, detail: validation.message });
3037
3511
  }
3512
+ // A running model turn owns the session workspace: a direct write would
3513
+ // race the harness's own edits, so it waits (409). A read runs alongside
3514
+ // the turn — the operator inspecting a live session is the point — and
3515
+ // its events stay separable by their per-call turnId. The effect can
3516
+ // depend on the input, hence deciding after validation; an MCP tool
3517
+ // nothing classifies is a write here, fail-closed.
3518
+ if (
3519
+ identity.busy &&
3520
+ record !== undefined &&
3521
+ directCallEffect(tool, validation.value) !== "read"
3522
+ ) {
3523
+ throw new SessionBusyError(record.sessionId);
3524
+ }
3038
3525
  let workspaceUnavailable = false;
3039
3526
  try {
3040
3527
  await mkdir(workspaceDir, { recursive: true });
@@ -3053,35 +3540,59 @@ export class SessionEngine {
3053
3540
  // to write) authored tool may rely on ctx.workspaceDir being the
3054
3541
  // real session workspace, so silently relocating it is not safe —
3055
3542
  // fail typed instead.
3543
+ //
3544
+ // A shared `local.workspaceDir` (change-monitor's phantom /repo) is
3545
+ // that path by design. Relocating it to scratch would flag a false
3546
+ // session-state warning; keep the borrowed cwd and skip the flag.
3056
3547
  const relocatable =
3057
3548
  tool.advertised === true ||
3058
- resolveEffect(tool.definition.effect, validation.value) === "read";
3549
+ directCallEffect(tool, validation.value) === "read";
3059
3550
  if (!relocatable) {
3060
3551
  throw new WorkspaceUnavailableError(toolName, error);
3061
3552
  }
3062
- this.logger(
3063
- `[session] tool ${toolName} workspace unavailable (direct ${callId}); using scratch: ${
3064
- error instanceof Error ? error.message : String(error)
3065
- }`
3066
- );
3067
- scratchDir = join(this.stateRoot, "tool-calls", callId);
3068
- workspaceDir = scratchDir;
3069
- workspaceUnavailable = true;
3070
- await mkdir(workspaceDir, { recursive: true });
3553
+ if (this.project.agent.local?.workspaceDir !== undefined) {
3554
+ this.logger(
3555
+ `[session] tool ${toolName} harness cwd unavailable (direct ${callId}); keeping borrowed workspace`
3556
+ );
3557
+ } else {
3558
+ this.logger(
3559
+ `[session] tool ${toolName} workspace unavailable (direct ${callId}); using scratch: ${
3560
+ error instanceof Error ? error.message : String(error)
3561
+ }`
3562
+ );
3563
+ scratchDir = join(this.stateRoot, "tool-calls", callId);
3564
+ workspaceDir = scratchDir;
3565
+ workspaceUnavailable = true;
3566
+ await mkdir(workspaceDir, { recursive: true });
3567
+ }
3071
3568
  }
3072
3569
  // Match what a model turn would see: session workspaces get the same
3073
3570
  // scaffold as executeTurn (seeds only before the first turn); scratch
3074
- // workspaces are materialized fresh.
3075
- await materializeWorkspace({
3076
- agent: this.project.agent,
3077
- workspaceDir:
3078
- record === undefined || workspaceUnavailable
3079
- ? workspaceDir
3080
- : this.scaffoldDir(record),
3081
- seed:
3082
- record === undefined || workspaceUnavailable || record.turnCount === 0,
3083
- stateRoot: this.stateRoot,
3084
- });
3571
+ // workspaces are materialized fresh. A read running alongside a turn
3572
+ // that has materialized its workspace leaves it alone — re-seeding
3573
+ // would restore files the agent deleted mid-turn. A session is busy
3574
+ // from admit, though, so a read that lands while the first turn is
3575
+ // still queued or scaffolding materializes the workspace itself, seeds
3576
+ // included (turnCount is already 1 for that pending first turn).
3577
+ const turnMaterialized =
3578
+ record !== undefined &&
3579
+ identity.busy &&
3580
+ this.materializedWorkspaces.has(record.sessionId);
3581
+ if (record === undefined || workspaceUnavailable || !turnMaterialized) {
3582
+ await materializeWorkspace({
3583
+ agent: this.project.agent,
3584
+ workspaceDir:
3585
+ record === undefined || workspaceUnavailable
3586
+ ? workspaceDir
3587
+ : this.scaffoldDir(record),
3588
+ seed:
3589
+ record === undefined ||
3590
+ workspaceUnavailable ||
3591
+ record.turnCount === 0 ||
3592
+ (identity.busy && record.turnCount === 1),
3593
+ stateRoot: this.stateRoot,
3594
+ });
3595
+ }
3085
3596
 
3086
3597
  // A per-call turnId keeps these events visible in trajectories (which
3087
3598
  // drop turn-less action events) without attributing them to a model turn.
@@ -3139,8 +3650,8 @@ export class SessionEngine {
3139
3650
  result = normalizeToolResult(decision.result);
3140
3651
  isError = false;
3141
3652
  } else {
3142
- result = normalizeToolResult(
3143
- await execute(validation.value as never, {
3653
+ const run = () =>
3654
+ execute(validation.value as never, {
3144
3655
  toolCallId: callId,
3145
3656
  session,
3146
3657
  workspaceDir,
@@ -3165,7 +3676,20 @@ export class SessionEngine {
3165
3676
  getSession: (channelId, sessionId) =>
3166
3677
  this.getChannelSession(channelId, sessionId),
3167
3678
  emit,
3168
- })
3679
+ });
3680
+ result = normalizeToolResult(
3681
+ record !== undefined && emit !== undefined
3682
+ ? await mcpAuthAls.run(
3683
+ {
3684
+ sessionId: record.sessionId,
3685
+ turnId: callId,
3686
+ callId,
3687
+ toolName,
3688
+ emit,
3689
+ },
3690
+ run
3691
+ )
3692
+ : await run()
3169
3693
  );
3170
3694
  isError = isErrorEnvelope(result);
3171
3695
  }
@@ -3585,6 +4109,75 @@ export class SessionEngine {
3585
4109
  };
3586
4110
  }
3587
4111
 
4112
+ /**
4113
+ * The single newest session visible to `caller` (`GET /v1/sessions?latest=1`).
4114
+ *
4115
+ * Short-circuits the full listing: the local store already holds every
4116
+ * session this host created plus the startup bulk restore, so the common
4117
+ * case answers from memory without a sink round trip. Only when the local
4118
+ * store has nothing for the caller (fresh host, `restore: "off"`) does it
4119
+ * ask the sink — for a bounded newest-written window when the backend
4120
+ * can order by write time, else the full list. A session written by
4121
+ * another replica moments ago can lose to a local one; callers that need
4122
+ * the exact newest row should use the full listing.
4123
+ */
4124
+ async latestSessionWithSource(
4125
+ caller: AuthContext | null,
4126
+ options?: { includeAll?: boolean }
4127
+ ): Promise<{ sessions: SessionSummary[]; source: SessionListSource }> {
4128
+ const includeAll = options?.includeAll ?? false;
4129
+ const newestVisible = (
4130
+ records: readonly SessionRecord[]
4131
+ ): SessionRecord | undefined => {
4132
+ let newest: SessionRecord | undefined;
4133
+ for (const record of records) {
4134
+ if (!(includeAll || samePrincipal(record.auth, caller))) {
4135
+ continue;
4136
+ }
4137
+ if (
4138
+ newest === undefined ||
4139
+ record.updatedAt.localeCompare(newest.updatedAt) > 0
4140
+ ) {
4141
+ newest = record;
4142
+ }
4143
+ }
4144
+ return newest;
4145
+ };
4146
+ const local = newestVisible(await this.sessions.list());
4147
+ if (local !== undefined) {
4148
+ return {
4149
+ sessions: this.summarizeSessionRecords([local], caller, includeAll),
4150
+ source: "local",
4151
+ };
4152
+ }
4153
+ if (this.storage?.isCursorHosted === true) {
4154
+ try {
4155
+ const window = await this.storage.listNewestSessionsFromSink(
4156
+ LATEST_SESSION_SINK_WINDOW
4157
+ );
4158
+ if (window !== undefined) {
4159
+ const stored = newestVisible(window);
4160
+ if (stored !== undefined) {
4161
+ return {
4162
+ sessions: this.summarizeSessionRecords(
4163
+ [stored],
4164
+ caller,
4165
+ includeAll
4166
+ ),
4167
+ source: "cursor-hosted",
4168
+ };
4169
+ }
4170
+ }
4171
+ } catch (error) {
4172
+ this.logger(
4173
+ `[agent-sdk] cursor-hosted newest-session lookup failed; falling back to the full list: ${describeError(error)}`
4174
+ );
4175
+ }
4176
+ }
4177
+ const listed = await this.listSessionsWithSource(caller, options);
4178
+ return { sessions: listed.sessions.slice(0, 1), source: listed.source };
4179
+ }
4180
+
3588
4181
  /**
3589
4182
  * Durable sink rows plus local records (local wins on the same id when
3590
4183
  * newer — covers debounce windows that have not flushed yet). Newest
@@ -3638,6 +4231,7 @@ export class SessionEngine {
3638
4231
  : undefined,
3639
4232
  createdAt: record.createdAt,
3640
4233
  updatedAt: record.updatedAt,
4234
+ dryRun: record.dryRun === true ? true : undefined,
3641
4235
  continuationToken:
3642
4236
  record.httpContinuationKey !== undefined
3643
4237
  ? formatContinuationToken(
@@ -3967,7 +4561,7 @@ export class SessionEngine {
3967
4561
  channelId: record.channelId,
3968
4562
  mode: record.mode,
3969
4563
  purpose: record.purpose === "eval" ? "eval" : "live",
3970
- ...(record.dryRun === true ? { dryRun: true } : {}),
4564
+ dryRun: record.dryRun === true ? true : undefined,
3971
4565
  auth: record.auth,
3972
4566
  continuationKey: record.continuationKey,
3973
4567
  ...(record.abs === undefined ? {} : { abs: record.abs }),
@@ -4340,6 +4934,8 @@ export class SessionEngine {
4340
4934
  async close(): Promise<void> {
4341
4935
  this.stopMemoryTelemetry?.();
4342
4936
  this.approvals.cancelAll("deny");
4937
+ this.auths.cancelAll("deny");
4938
+ await this.oauthHub.close();
4343
4939
  await Promise.allSettled([...this.activeTurns.values()]);
4344
4940
  await Promise.allSettled([...this.backgroundWork]);
4345
4941
  await Promise.allSettled([...this.dispatchChains.values()]);
@@ -4357,6 +4953,13 @@ export class SessionEngine {
4357
4953
  /** Parallel event-stream loads during the startup bulk restore. */
4358
4954
  const RESTORE_FETCH_CONCURRENCY = 8;
4359
4955
 
4956
+ /**
4957
+ * Newest-written sink rows fetched for `GET /v1/sessions?latest=1` when the
4958
+ * local store has nothing for the caller. Wide enough that the caller's own
4959
+ * session is normally inside it even when other principals are busier.
4960
+ */
4961
+ const LATEST_SESSION_SINK_WINDOW = 50;
4962
+
4360
4963
  /** NDJSON payload for a restored event stream; undefined when empty. */
4361
4964
  function mergePendingImages(
4362
4965
  current: Array<{ data: string; mimeType: string }> | undefined,