@cursor/july 0.1.94 → 0.1.95

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 (209) hide show
  1. package/dist/channels/github/github-channel.d.ts.map +1 -1
  2. package/dist/channels/github/github-channel.js +14 -2
  3. package/dist/channels/github/types.d.ts +18 -3
  4. package/dist/channels/github/types.d.ts.map +1 -1
  5. package/dist/channels/origin/origin-channel.d.ts.map +1 -1
  6. package/dist/channels/origin/origin-channel.js +11 -3
  7. package/dist/channels/origin/origin-webhook.d.ts +11 -1
  8. package/dist/channels/origin/origin-webhook.d.ts.map +1 -1
  9. package/dist/channels/origin/origin-webhook.js +23 -3
  10. package/dist/channels/origin/types.d.ts +8 -0
  11. package/dist/channels/origin/types.d.ts.map +1 -1
  12. package/dist/channels/slack/pr-ref.d.ts +7 -1
  13. package/dist/channels/slack/pr-ref.d.ts.map +1 -1
  14. package/dist/channels/slack/pr-ref.js +42 -23
  15. package/dist/docs/404.html +2 -2
  16. package/dist/docs/ab.html +4 -4
  17. package/dist/docs/assets/{app.CFDEas4I.js → app.BBj0klBO.js} +1 -1
  18. package/dist/docs/assets/chunks/@localSearchIndexroot.oqPawjiD.js +1 -0
  19. package/dist/docs/assets/chunks/{VPLocalSearchBox.B1IIYpYS.js → VPLocalSearchBox.CUEXpL78.js} +1 -1
  20. package/dist/docs/assets/chunks/{theme.Ct4NSiLm.js → theme.DabmQnia.js} +2 -2
  21. package/dist/docs/assets/index.md.BoHaMdeZ.js +5 -0
  22. package/dist/docs/assets/{index.md.DRakGHFe.lean.js → index.md.BoHaMdeZ.lean.js} +1 -1
  23. package/dist/docs/assets/{reference_cli.md.Cw6_ICYG.js → reference_cli.md.BsMOxDvh.js} +3 -2
  24. package/dist/docs/assets/{reference_cli.md.Cw6_ICYG.lean.js → reference_cli.md.BsMOxDvh.lean.js} +1 -1
  25. package/dist/docs/assets/{reference_skills.md.8son6Hjm.js → reference_skills.md.BFW9retM.js} +3 -3
  26. package/dist/docs/assets/{templates_agentic-owners.md.DqtPdm6f.js → templates_agentic-owners.md.BZSH4N9z.js} +1 -1
  27. package/dist/docs/assets/{templates_demo.md.DhFcWN6j.js → templates_demo.md.BeQX9V3H.js} +1 -1
  28. package/dist/docs/assets/{templates_pr-autofixer.md.DU7dQpor.js → templates_pr-autofixer.md.x5zl6-GT.js} +2 -2
  29. package/dist/docs/assets/{templates_pr-autofixer.md.DU7dQpor.lean.js → templates_pr-autofixer.md.x5zl6-GT.lean.js} +1 -1
  30. package/dist/docs/assets/templates_security-help.md.C3Ny_Qr2.js +4 -0
  31. package/dist/docs/assets/templates_security-help.md.C3Ny_Qr2.lean.js +1 -0
  32. package/dist/docs/assets/{templates_security-reviewer.md.CTa7u_l1.js → templates_security-reviewer.md.lshxbCLK.js} +2 -2
  33. package/dist/docs/assets/{templates_security-reviewer.md.CTa7u_l1.lean.js → templates_security-reviewer.md.lshxbCLK.lean.js} +1 -1
  34. package/dist/docs/assets/{templates_triage.md.CVlpctKS.js → templates_triage.md.Co4UNzkZ.js} +3 -3
  35. package/dist/docs/assets/{templates_triage.md.CVlpctKS.lean.js → templates_triage.md.Co4UNzkZ.lean.js} +1 -1
  36. package/dist/docs/building-with-agents.html +4 -4
  37. package/dist/docs/concepts.html +4 -4
  38. package/dist/docs/deployment.html +4 -4
  39. package/dist/docs/evals.html +4 -4
  40. package/dist/docs/guides/agent-to-agent.html +4 -4
  41. package/dist/docs/guides/cloud-runtime.html +4 -4
  42. package/dist/docs/guides/convert-automation.html +4 -4
  43. package/dist/docs/guides/github.html +4 -4
  44. package/dist/docs/guides/human-in-the-loop.html +4 -4
  45. package/dist/docs/guides/mcp-oauth.html +4 -4
  46. package/dist/docs/guides/opentelemetry.html +4 -4
  47. package/dist/docs/guides/slack.html +4 -4
  48. package/dist/docs/guides/webhooks.html +4 -4
  49. package/dist/docs/hashmap.json +1 -1
  50. package/dist/docs/hillclimbing.html +4 -4
  51. package/dist/docs/index.html +6 -6
  52. package/dist/docs/index.md +3 -1
  53. package/dist/docs/llms-full.txt +90 -26
  54. package/dist/docs/llms.txt +1 -0
  55. package/dist/docs/quickstart.html +4 -4
  56. package/dist/docs/reference/agent-config.html +4 -4
  57. package/dist/docs/reference/artifacts.html +4 -4
  58. package/dist/docs/reference/channels.html +4 -4
  59. package/dist/docs/reference/cli.html +7 -6
  60. package/dist/docs/reference/cli.md +3 -1
  61. package/dist/docs/reference/connections.html +4 -4
  62. package/dist/docs/reference/hooks.html +4 -4
  63. package/dist/docs/reference/http-api.html +4 -4
  64. package/dist/docs/reference/instructions.html +4 -4
  65. package/dist/docs/reference/playground.html +4 -4
  66. package/dist/docs/reference/project-layout.html +4 -4
  67. package/dist/docs/reference/prompt.html +4 -4
  68. package/dist/docs/reference/schedules.html +4 -4
  69. package/dist/docs/reference/sessions.html +4 -4
  70. package/dist/docs/reference/skills.html +7 -7
  71. package/dist/docs/reference/subagents.html +4 -4
  72. package/dist/docs/reference/tools.html +4 -4
  73. package/dist/docs/scaffolding-agents.html +4 -4
  74. package/dist/docs/storage.html +4 -4
  75. package/dist/docs/templates/agentic-owners.html +7 -7
  76. package/dist/docs/templates/agentic-owners.md +1 -1
  77. package/dist/docs/templates/demo.html +6 -6
  78. package/dist/docs/templates/demo.md +3 -2
  79. package/dist/docs/templates/pr-autofixer.html +6 -6
  80. package/dist/docs/templates/pr-autofixer.md +7 -13
  81. package/dist/docs/templates/security-help.html +30 -0
  82. package/dist/docs/templates/security-help.md +65 -0
  83. package/dist/docs/templates/security-reviewer.html +6 -6
  84. package/dist/docs/templates/security-reviewer.md +1 -2
  85. package/dist/docs/templates/triage.html +7 -7
  86. package/dist/docs/templates/triage.md +2 -6
  87. package/dist/docs/troubleshooting.html +4 -4
  88. package/dist/files-backends/cursor-hosted.d.ts +6 -2
  89. package/dist/files-backends/cursor-hosted.d.ts.map +1 -1
  90. package/dist/files-backends/cursor-hosted.js +2 -2
  91. package/dist/files.d.ts +2 -0
  92. package/dist/files.d.ts.map +1 -1
  93. package/dist/files.js +5 -0
  94. package/dist/internal/cli-deploy.d.ts.map +1 -1
  95. package/dist/internal/cli-deploy.js +135 -6
  96. package/dist/internal/conversation-mirror.d.ts +82 -0
  97. package/dist/internal/conversation-mirror.d.ts.map +1 -0
  98. package/dist/internal/conversation-mirror.js +251 -0
  99. package/dist/internal/deferred-channel-session.d.ts +20 -0
  100. package/dist/internal/deferred-channel-session.d.ts.map +1 -0
  101. package/dist/internal/deferred-channel-session.js +62 -0
  102. package/dist/internal/deploy-client.d.ts +13 -1
  103. package/dist/internal/deploy-client.d.ts.map +1 -1
  104. package/dist/internal/deploy-client.js +11 -1
  105. package/dist/internal/deploy-manifest.d.ts.map +1 -1
  106. package/dist/internal/deploy-manifest.js +11 -5
  107. package/dist/internal/discovery.d.ts.map +1 -1
  108. package/dist/internal/discovery.js +22 -2
  109. package/dist/internal/framework-storage-selection.d.ts +32 -16
  110. package/dist/internal/framework-storage-selection.d.ts.map +1 -1
  111. package/dist/internal/framework-storage-selection.js +51 -17
  112. package/dist/internal/hosted-admission-context.d.ts +20 -0
  113. package/dist/internal/hosted-admission-context.d.ts.map +1 -0
  114. package/dist/internal/hosted-admission-context.js +31 -0
  115. package/dist/internal/hosted-delivery-protocol.d.ts +5 -0
  116. package/dist/internal/hosted-delivery-protocol.d.ts.map +1 -1
  117. package/dist/internal/hosted-delivery-protocol.js +33 -1
  118. package/dist/internal/hosted-delivery.d.ts +4 -2
  119. package/dist/internal/hosted-delivery.d.ts.map +1 -1
  120. package/dist/internal/hosted-delivery.js +75 -35
  121. package/dist/internal/hosted-managed-automation.d.ts +30 -0
  122. package/dist/internal/hosted-managed-automation.d.ts.map +1 -0
  123. package/dist/internal/hosted-managed-automation.js +58 -0
  124. package/dist/internal/review-comments.d.ts.map +1 -1
  125. package/dist/internal/review-comments.js +10 -98
  126. package/dist/internal/scm/pr-url.d.ts +14 -0
  127. package/dist/internal/scm/pr-url.d.ts.map +1 -0
  128. package/dist/internal/scm/pr-url.js +65 -0
  129. package/dist/internal/sdk-runner.d.ts +14 -0
  130. package/dist/internal/sdk-runner.d.ts.map +1 -1
  131. package/dist/internal/sdk-runner.js +41 -2
  132. package/dist/internal/server.d.ts.map +1 -1
  133. package/dist/internal/server.js +10 -2
  134. package/dist/internal/session-engine.d.ts.map +1 -1
  135. package/dist/internal/session-engine.js +9 -0
  136. package/dist/playground/assets/index-CF7hUDnQ.css +1 -0
  137. package/dist/playground/assets/{index-B3JCyigB.js → index-CZA1uEWA.js} +48 -48
  138. package/dist/playground/index.html +2 -2
  139. package/dist/storage-backends/cursor-hosted.d.ts +7 -2
  140. package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
  141. package/dist/storage-backends/cursor-hosted.js +2 -2
  142. package/docs/README.md +3 -1
  143. package/docs/reference/cli.md +3 -1
  144. package/docs/templates/agentic-owners.md +1 -1
  145. package/docs/templates/demo.md +3 -2
  146. package/docs/templates/pr-autofixer.md +7 -13
  147. package/docs/templates/security-help.md +70 -0
  148. package/docs/templates/security-reviewer.md +1 -2
  149. package/docs/templates/triage.md +2 -6
  150. package/package.json +1 -1
  151. package/src/channels/github/github-channel.ts +23 -8
  152. package/src/channels/github/types.ts +19 -2
  153. package/src/channels/origin/origin-channel.ts +13 -1
  154. package/src/channels/origin/origin-webhook.ts +27 -3
  155. package/src/channels/origin/types.ts +8 -0
  156. package/src/channels/slack/pr-ref.ts +56 -25
  157. package/src/files-backends/cursor-hosted.ts +9 -3
  158. package/src/files.ts +11 -0
  159. package/src/internal/cli-deploy.ts +171 -7
  160. package/src/internal/conversation-mirror.ts +330 -0
  161. package/src/internal/deferred-channel-session.ts +61 -0
  162. package/src/internal/deploy-client.ts +24 -1
  163. package/src/internal/deploy-manifest.ts +10 -5
  164. package/src/internal/discovery.ts +25 -2
  165. package/src/internal/framework-storage-selection.ts +61 -19
  166. package/src/internal/hosted-admission-context.ts +37 -0
  167. package/src/internal/hosted-delivery-protocol.ts +44 -1
  168. package/src/internal/hosted-delivery.ts +95 -40
  169. package/src/internal/hosted-managed-automation.ts +72 -0
  170. package/src/internal/review-comments.ts +10 -113
  171. package/src/internal/scm/pr-url.ts +95 -0
  172. package/src/internal/sdk-runner.ts +57 -2
  173. package/src/internal/server.ts +10 -0
  174. package/src/internal/session-engine.ts +12 -0
  175. package/src/storage-backends/cursor-hosted.ts +10 -3
  176. package/templates/agentic-owners/README.md +1 -1
  177. package/templates/agentic-owners/agent/agent.ts +0 -10
  178. package/templates/agentic-owners/agent/channels/github.ts +5 -14
  179. package/templates/agentic-owners/agent/lib/config.ts +0 -8
  180. package/templates/agentic-owners/agent/lib/review.ts +2 -15
  181. package/templates/agentic-owners/agent/tools/record_review.ts +2 -4
  182. package/templates/demo/agent/agent.ts +0 -10
  183. package/templates/pr-autofixer/README.md +0 -2
  184. package/templates/pr-autofixer/agent/agent.ts +0 -11
  185. package/templates/pr-autofixer/agent/channels/slack.ts +0 -7
  186. package/templates/pr-autofixer/agent/lib/pr-state.ts +5 -17
  187. package/templates/pr-autofixer/agent/lib/repos.ts +0 -1
  188. package/templates/security-help/README.md +2 -2
  189. package/templates/security-help/agent/agent.ts +1 -2
  190. package/templates/security-help/agent/channels/slack.ts +0 -3
  191. package/templates/security-help/agent/instructions.md +9 -10
  192. package/templates/security-help/agent/skills/access-request.md +1 -1
  193. package/templates/security-help/agent/skills/faq.md +31 -0
  194. package/templates/security-help/agent/skills/security-playbooks.md +1 -1
  195. package/templates/security-help/package.json +1 -2
  196. package/templates/security-reviewer/agent/agent.ts +0 -10
  197. package/templates/triage/agent/agent.ts +0 -10
  198. package/templates/triage/agent/channels/queue.ts +1 -1
  199. package/templates/triage/agent/channels/webhook.ts +1 -3
  200. package/dist/docs/assets/chunks/@localSearchIndexroot.DU3U2Ij2.js +0 -1
  201. package/dist/docs/assets/index.md.DRakGHFe.js +0 -5
  202. package/dist/playground/assets/index-D9N7-q97.css +0 -1
  203. package/templates/security-help/agent/knowledge/faq/approvals.md +0 -5
  204. package/templates/security-help/agent/knowledge/faq/channels.md +0 -6
  205. package/templates/security-help/agent/knowledge/faq/phishing.md +0 -10
  206. package/templates/security-help/agent/skills/security-first-pass.md +0 -15
  207. /package/dist/docs/assets/{reference_skills.md.8son6Hjm.lean.js → reference_skills.md.BFW9retM.lean.js} +0 -0
  208. /package/dist/docs/assets/{templates_agentic-owners.md.DqtPdm6f.lean.js → templates_agentic-owners.md.BZSH4N9z.lean.js} +0 -0
  209. /package/dist/docs/assets/{templates_demo.md.DhFcWN6j.lean.js → templates_demo.md.BeQX9V3H.lean.js} +0 -0
@@ -168,12 +168,17 @@ export function buildAgentDeployManifest(
168
168
  declarations: {
169
169
  egressDomains: [...(project.agent.hosting?.egressDomains ?? [])],
170
170
  publicRoutePaths,
171
+ // v2 ignores authored storage; deploy manifests describe
172
+ // Cursor-hosted deployments, where framework-storage-selection.ts
173
+ // injects the Cursor-hosted mirror.
171
174
  frameworkStorage:
172
- project.storage === undefined
173
- ? "none"
174
- : isCursorHostedStorage(project.storage)
175
- ? "cursor-hosted"
176
- : "other",
175
+ project.agent.architecture === "v2"
176
+ ? "cursor-hosted"
177
+ : project.storage === undefined
178
+ ? "none"
179
+ : isCursorHostedStorage(project.storage)
180
+ ? "cursor-hosted"
181
+ : "other",
177
182
  ...(project.agent.architecture === undefined
178
183
  ? {}
179
184
  : { architecture: project.agent.architecture }),
@@ -307,7 +307,7 @@ export async function loadAgentProject(
307
307
  const hooks = await loadHooks(ctx, join(agentDir, "hooks"));
308
308
  const abs = await loadABs(ctx, agentDir);
309
309
  const abConfig = await loadABConfig(ctx, agentDir);
310
- const storage = await loadStorage(ctx, agentDir);
310
+ const storage = await loadStorage(ctx, agentDir, agent.architecture);
311
311
  const artifacts = await loadArtifacts(ctx, agentDir);
312
312
  const otel = await loadOtel(ctx, agentDir);
313
313
  await appendArtifactTool(ctx, agentDir, agent, artifacts);
@@ -2151,12 +2151,35 @@ async function loadABConfig(
2151
2151
 
2152
2152
  async function loadStorage(
2153
2153
  ctx: DiscoveryContext,
2154
- agentDir: string
2154
+ agentDir: string,
2155
+ architecture: AgentArchitecture | undefined
2155
2156
  ): Promise<StorageDefinition | undefined> {
2156
2157
  const path = await findModuleFile(agentDir, "storage");
2157
2158
  if (path === undefined) {
2158
2159
  return undefined;
2159
2160
  }
2161
+ if (architecture === "v2") {
2162
+ // Storage is a hosting-mode decision on v2, made by
2163
+ // framework-storage-selection.ts: the state root when self-hosted,
2164
+ // Cursor-managed storage on Cursor hosting. Fail early (error
2165
+ // diagnostics refuse serve and deploy) rather than silently running
2166
+ // with storage the author believes they configured. Not imported.
2167
+ ctx.diagnostics.push({
2168
+ severity: "error",
2169
+ path: displayPath(ctx, path),
2170
+ message:
2171
+ 'agent/storage.ts is not supported on architecture "v2": the platform selects storage (the local state root when self-hosted; Cursor-managed storage on Cursor hosting). Delete the file.',
2172
+ });
2173
+ return undefined;
2174
+ }
2175
+ // v1 keeps loading the authored sink, with notice: defineStorage is going
2176
+ // away with the v1 control plane.
2177
+ ctx.diagnostics.push({
2178
+ severity: "warning",
2179
+ path: displayPath(ctx, path),
2180
+ message:
2181
+ 'agent/storage.ts (defineStorage) is deprecated and going away: architecture "v2" selects storage automatically (the local state root when self-hosted; Cursor-managed storage on Cursor hosting). Still honored on v1.',
2182
+ });
2160
2183
  const definition = await importDefault(ctx, path, "storage");
2161
2184
  return definition as StorageDefinition | undefined;
2162
2185
  }
@@ -2,19 +2,31 @@
2
2
  * Which backend framework session state is mirrored to.
3
3
  *
4
4
  * Framework state — session records, event chunks, the continuation index —
5
- * is how a session outlives the process that served it. Today that mirror
6
- * exists only when the agent author wrote `agent/storage.ts`, which makes
7
- * platform durability an agent-level decision: of the first-party agents,
8
- * some choose `cursorHostedStorage()` and some choose `fileKv()` (pod-local
9
- * disk, lost on pod replacement), and one selects between them at runtime by
10
- * reading the deployment-id environment variable itself.
5
+ * is how a session outlives the process that served it. On architecture v1
6
+ * that mirror exists only when the agent author wrote `agent/storage.ts`,
7
+ * which makes platform durability an agent-level decision: of the
8
+ * first-party agents, some choose `cursorHostedStorage()` and some choose
9
+ * `fileKv()` (pod-local disk, lost on pod replacement), and one selects
10
+ * between them at runtime by reading the deployment-id environment variable
11
+ * itself.
11
12
  *
12
- * That selection is a hosting-mode question, not an authoring question, and
13
- * this module is where it will move. It deliberately reproduces today's
14
- * behavior exactly for now authored definition or nothing so the change
15
- * that makes framework durability automatic is a change to
16
- * {@link selectFrameworkStorageDefinition} alone, reviewable on its own and
17
- * revertable without touching the engine.
13
+ * That selection is a hosting-mode question, not an authoring question.
14
+ * Architecture v2 moves it here: `agent/storage.ts` is rejected at
15
+ * discovery (error diagnostic, so serve and deploy fail early), and the
16
+ * platform picks the default
17
+ *
18
+ * - **Cursor-managed hosting** (the deployment-generation environment
19
+ * variable is set, same signal that registers `GET /v1/manifest`):
20
+ * the Cursor-hosted backend, so session records, event chunks, `host.kv`,
21
+ * eval runs, and A/B samples land in the control plane and survive pod
22
+ * replacement. The API key resolves through the full credential chain on
23
+ * every call because hosted freeze snapshots listen unsigned and the
24
+ * binder drops the key file after restore.
25
+ * - **Self-hosted**: no mirror. `--state-root` is the durable copy, which
26
+ * is correct when the operator owns the disk.
27
+ *
28
+ * v1 keeps the authored definition (or nothing) so existing deployments are
29
+ * untouched while they run.
18
30
  *
19
31
  * Local disk is not part of this decision. `<stateRoot>/sessions/<id>/`
20
32
  * already holds `session.json` and `events.ndjson` unconditionally, through
@@ -22,24 +34,54 @@
22
34
  * pod's filesystem.
23
35
  */
24
36
 
25
- import type { StorageDefinition } from "../storage.js";
37
+ import { defineStorage, type StorageDefinition } from "../storage.js";
38
+ import { cursorHostedStorage } from "../storage-backends/cursor-hosted.js";
39
+ import type { AgentArchitecture } from "../types.js";
40
+ import { resolveApiKeySync } from "./cursor/credentials.js";
41
+ import { AGENT_SERVE_GENERATION_ENV_VAR } from "./cursor-relay-core.js";
26
42
 
27
43
  export interface FrameworkStorageSelectionInput {
28
- /** `agent/storage.ts`, when the author wrote one. */
44
+ /** `agent/storage.ts`, when the author wrote one (v1 only: discovery rejects it on v2). */
29
45
  readonly authored: StorageDefinition | undefined;
46
+ /** {@link AgentConfig.architecture}; omitted means v1. */
47
+ readonly architecture?: AgentArchitecture | undefined;
48
+ /** Environment override for tests; defaults to `process.env`. */
49
+ readonly env?: Record<string, string | undefined>;
30
50
  }
31
51
 
32
52
  /**
33
53
  * Pick the backend for framework session state.
34
54
  *
35
55
  * Returns `undefined` when there is no mirror, in which case the pod's local
36
- * disk is the only copy — the condition F0 exists to remove for hosted
37
- * deployments.
56
+ * disk is the only copy.
38
57
  */
39
58
  export function selectFrameworkStorageDefinition(
40
59
  input: FrameworkStorageSelectionInput
41
60
  ): StorageDefinition | undefined {
42
- // Today: the author decides, or there is no mirror. Hosting-mode selection
43
- // replaces this body; every caller already routes through here.
44
- return input.authored;
61
+ if (input.architecture !== "v2") {
62
+ // v1: the author decides, or there is no mirror.
63
+ return input.authored;
64
+ }
65
+ if (input.authored !== undefined) {
66
+ // Discovery rejects agent/storage.ts on v2 before an engine is built;
67
+ // an authored definition reaching this point is a wiring bug. Raise
68
+ // rather than silently discard what the author believes is configured.
69
+ throw new Error(
70
+ 'defineStorage (agent/storage.ts) is not supported on architecture "v2": storage is platform-selected. Delete the file.'
71
+ );
72
+ }
73
+ const env = input.env ?? process.env;
74
+ if (env[AGENT_SERVE_GENERATION_ENV_VAR] === undefined) {
75
+ // Self-hosted v2: the state root is the durable copy.
76
+ return undefined;
77
+ }
78
+ return defineStorage({
79
+ name: "cursor-hosted",
80
+ ...cursorHostedStorage({
81
+ // Full chain (env → key file → stored login): frozen engines listen
82
+ // unsigned and bind CURSOR_API_KEY_FILE at task time, after this
83
+ // definition is constructed.
84
+ apiKey: () => resolveApiKeySync()?.apiKey,
85
+ }),
86
+ });
45
87
  }
@@ -0,0 +1,37 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
3
+ /**
4
+ * A `send()` captured outside the delivery (typically at channel `onStart`,
5
+ * which binds directly to the engine) reached the engine during a hosted
6
+ * admission window. Running it would start a local July session on a
7
+ * sessionless pod — invisible to the control plane, destroyed with the pod —
8
+ * so it fails loudly instead. This is a deterministic authored bug and is
9
+ * surfaced as a non-retryable 422 by the admission route.
10
+ */
11
+ export class HostedAdmissionLocalSendError extends Error {
12
+ constructor() {
13
+ super(
14
+ "send() reached the local engine during hosted admission. A send " +
15
+ "captured at channel startup (onStart) bypasses hosted capture and " +
16
+ "would run an invisible local session on a sessionless pod. Use the " +
17
+ "per-delivery send passed to the task callback " +
18
+ "(task: (args) => args.send(…)) instead."
19
+ );
20
+ }
21
+ }
22
+
23
+ const admissionWindow = new AsyncLocalStorage<true>();
24
+
25
+ /**
26
+ * Run `work` inside the hosted admission window. The context follows the
27
+ * async graph, including `waitUntil` task bodies scheduled during dispatch,
28
+ * so the engine can refuse local sends that bypass the capture stub.
29
+ */
30
+ export function runDuringHostedAdmission<T>(work: () => T): T {
31
+ return admissionWindow.run(true, work);
32
+ }
33
+
34
+ /** True while executing inside a hosted admission window. */
35
+ export function isDuringHostedAdmission(): boolean {
36
+ return admissionWindow.getStore() === true;
37
+ }
@@ -3,6 +3,11 @@ export const HOSTED_DELIVERY_HEADER = "x-agent-sdk-hosted-delivery";
3
3
  export const HOSTED_ADMISSION_PATH = "/v1/internal/hosted-admission";
4
4
  export const HOSTED_EXECUTION_PATH = "/v1/internal/hosted-execution";
5
5
 
6
+ export type HostedAdmissionManagedAutomation = {
7
+ automationUuid: string;
8
+ mcpServerNames: string[];
9
+ };
10
+
6
11
  export type HostedAdmission = {
7
12
  v: 2;
8
13
  channelId: string;
@@ -12,6 +17,7 @@ export type HostedAdmission = {
12
17
  sessionMode: "event" | "continuation";
13
18
  mailboxContinuationKey: string;
14
19
  payload: unknown;
20
+ managedAutomation?: HostedAdmissionManagedAutomation;
15
21
  };
16
22
 
17
23
  export type HostedAdmissionOutcome =
@@ -39,13 +45,49 @@ const DELIVERY_KEYS = new Set([
39
45
  "sessionMode",
40
46
  "mailboxContinuationKey",
41
47
  "payload",
48
+ "managedAutomation",
42
49
  ]);
50
+ const MANAGED_AUTOMATION_KEYS = new Set(["automationUuid", "mcpServerNames"]);
51
+ /** More names than any real automation configures is a malformed envelope. */
52
+ const MANAGED_AUTOMATION_MAX_SERVERS = 64;
43
53
  const EXECUTION_KEYS = new Set(["v", "admissionToken", "sessionPublicId"]);
44
54
 
45
55
  function boundedString(value: unknown, max: number): value is string {
46
56
  return typeof value === "string" && value.length > 0 && value.length <= max;
47
57
  }
48
58
 
59
+ /**
60
+ * Parse the optional managed-automation slice. Absent → valid (undefined);
61
+ * present-but-malformed → invalid (the whole admission fails closed, the
62
+ * strict posture the rest of the envelope already takes).
63
+ */
64
+ function parseManagedAutomation(
65
+ value: unknown
66
+ ): { managedAutomation?: HostedAdmissionManagedAutomation } | undefined {
67
+ if (value === undefined) {
68
+ return {};
69
+ }
70
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
71
+ return undefined;
72
+ }
73
+ const body = value as Record<string, unknown>;
74
+ if (
75
+ Object.keys(body).some((key) => !MANAGED_AUTOMATION_KEYS.has(key)) ||
76
+ !boundedString(body.automationUuid, 128) ||
77
+ !Array.isArray(body.mcpServerNames) ||
78
+ body.mcpServerNames.length > MANAGED_AUTOMATION_MAX_SERVERS ||
79
+ !body.mcpServerNames.every((name) => boundedString(name, 256))
80
+ ) {
81
+ return undefined;
82
+ }
83
+ return {
84
+ managedAutomation: {
85
+ automationUuid: body.automationUuid,
86
+ mcpServerNames: body.mcpServerNames,
87
+ },
88
+ };
89
+ }
90
+
49
91
  export function parseHostedAdmission(
50
92
  value: unknown
51
93
  ): HostedAdmission | undefined {
@@ -62,7 +104,8 @@ export function parseHostedAdmission(
62
104
  !boundedString(body.eventType, 128) ||
63
105
  !boundedString(body.deliveryId, 256) ||
64
106
  (body.sessionMode !== "event" && body.sessionMode !== "continuation") ||
65
- !boundedString(body.mailboxContinuationKey, 512)
107
+ !boundedString(body.mailboxContinuationKey, 512) ||
108
+ parseManagedAutomation(body.managedAutomation) === undefined
66
109
  ) {
67
110
  return undefined;
68
111
  }
@@ -10,6 +10,14 @@ import type {
10
10
  ChannelHandlerArgs,
11
11
  SendMessageOptions,
12
12
  } from "../types.js";
13
+ import {
14
+ createDeferredChannelSession,
15
+ DeferredChannelSessionError,
16
+ } from "./deferred-channel-session.js";
17
+ import {
18
+ HostedAdmissionLocalSendError,
19
+ runDuringHostedAdmission,
20
+ } from "./hosted-admission-context.js";
13
21
  import {
14
22
  HOSTED_ADMISSION_PATH,
15
23
  HOSTED_DELIVERY_HEADER,
@@ -19,6 +27,7 @@ import {
19
27
  parseHostedAdmission,
20
28
  parseHostedExecution,
21
29
  } from "./hosted-delivery-protocol.js";
30
+ import { recordHostedManagedAutomation } from "./hosted-managed-automation.js";
22
31
  import { errorResponse, readJsonObject } from "./http-channel.js";
23
32
  import type { Router } from "./router.js";
24
33
 
@@ -45,19 +54,66 @@ type ExecutionReply = {
45
54
  body: Record<string, unknown>;
46
55
  };
47
56
 
48
- class HostedSendAdmitted extends Error {
49
- constructor(
50
- readonly prompt: string,
51
- readonly options: SendMessageOptions
52
- ) {
53
- super("hosted channel admitted a send");
57
+ /**
58
+ * A second `send()` in one delivery is a deterministic authored bug — the
59
+ * wire protocol carries exactly one pending send per delivery — so it must
60
+ * fail the delivery exactly once. It surfaces as a 422 because the control
61
+ * plane retries 5xx admissions (fresh pod, side effects re-run, identical
62
+ * failure, up to the Temporal attempt cap); a non-5xx/408/429 status makes
63
+ * it reject the delivery non-retryably instead.
64
+ */
65
+ class HostedSendLimitExceeded extends Error {
66
+ constructor() {
67
+ super(
68
+ "The hosted delivery protocol carries exactly one pending send per " +
69
+ "delivery, and this delivery already captured one. Combine both " +
70
+ "messages into a single send(), or split the work across deliveries."
71
+ );
54
72
  }
55
73
  }
56
74
 
75
+ // Authored/task-derived text stays bounded on this internal response path
76
+ // (and is never persisted to Temporal history by the control plane).
77
+ const MAX_ERROR_MESSAGE_LENGTH = 1024;
78
+
79
+ function boundedMessage(text: string): string {
80
+ return text.length > MAX_ERROR_MESSAGE_LENGTH
81
+ ? `${text.slice(0, MAX_ERROR_MESSAGE_LENGTH)}…`
82
+ : text;
83
+ }
84
+
85
+ /**
86
+ * Classify deterministic authored protocol violations as non-retryable 422
87
+ * admissions. Anything else returns `undefined` so callers fall through to
88
+ * the existing (retryable or rethrow) handling.
89
+ */
90
+ function nonRetryableRejection(error: unknown): AdmissionDispatch | undefined {
91
+ const code =
92
+ error instanceof HostedSendLimitExceeded
93
+ ? "hosted_send_limit_exceeded"
94
+ : error instanceof DeferredChannelSessionError
95
+ ? "hosted_deferred_session_unusable"
96
+ : error instanceof HostedAdmissionLocalSendError
97
+ ? "hosted_local_send_rejected"
98
+ : undefined;
99
+ if (code === undefined) {
100
+ return undefined;
101
+ }
102
+ return {
103
+ response: errorResponse(
104
+ 422,
105
+ code,
106
+ boundedMessage((error as Error).message)
107
+ ),
108
+ };
109
+ }
110
+
57
111
  /**
58
112
  * Run authored admission without creating a framework session. V1 GitHub
59
- * `task` callbacks execute as sessionless work; `send` is captured and
60
- * resumed only after the control plane allocates `ses_…`.
113
+ * `task` callbacks execute as sessionless work; the first `send` is captured
114
+ * and executed only after the control plane allocates `ses_…`. Capturing
115
+ * does not terminate the caller — `send` returns an inert deferred handle so
116
+ * a task body runs to completion before the admission response is computed.
61
117
  */
62
118
  export async function dispatchHostedAdmission(
63
119
  input: DispatchInput
@@ -71,24 +127,28 @@ export async function dispatchHostedAdmission(
71
127
  ),
72
128
  };
73
129
  }
130
+ // The admission window marker follows the async graph into task bodies so
131
+ // the engine can refuse local sends that would bypass the capture stub.
132
+ return runDuringHostedAdmission(() => runAdmission(input, github));
133
+ }
74
134
 
135
+ async function runAdmission(
136
+ input: DispatchInput,
137
+ github: NonNullable<ReturnType<typeof getGitHubChannelOptions>>
138
+ ): Promise<AdmissionDispatch> {
75
139
  const background: Promise<unknown>[] = [];
76
- let deferredSend: HostedSendAdmitted | undefined;
140
+ let captured: { message: string; options: SendMessageOptions } | undefined;
77
141
  const args = withPropagatingGitHubBackgroundErrors({
78
142
  ...input.args,
79
143
  send: async (message, options = {}) => {
80
- throw new HostedSendAdmitted(message, options);
144
+ if (captured !== undefined) {
145
+ throw new HostedSendLimitExceeded();
146
+ }
147
+ captured = { message, options };
148
+ return createDeferredChannelSession();
81
149
  },
82
150
  waitUntil: (promise) => {
83
- background.push(
84
- promise.catch((error: unknown) => {
85
- if (error instanceof HostedSendAdmitted) {
86
- deferredSend = error;
87
- return;
88
- }
89
- throw error;
90
- })
91
- );
151
+ background.push(promise);
92
152
  },
93
153
  });
94
154
  let dispatched: Response;
@@ -101,20 +161,9 @@ export async function dispatchHostedAdmission(
101
161
  args,
102
162
  });
103
163
  } catch (error) {
104
- if (error instanceof HostedSendAdmitted) {
105
- return {
106
- response: Response.json({
107
- ok: true,
108
- v: HOSTED_DELIVERY_PROTOCOL_VERSION,
109
- outcome: "send",
110
- }),
111
- pendingSend: {
112
- channelId: input.delivery.channelId,
113
- message: error.prompt,
114
- options: error.options,
115
- mailboxContinuationKey: input.delivery.mailboxContinuationKey,
116
- },
117
- };
164
+ const rejected = nonRetryableRejection(error);
165
+ if (rejected !== undefined) {
166
+ return rejected;
118
167
  }
119
168
  throw error;
120
169
  }
@@ -129,15 +178,20 @@ export async function dispatchHostedAdmission(
129
178
 
130
179
  try {
131
180
  await Promise.all(background);
132
- } catch {
181
+ } catch (error) {
182
+ const rejected = nonRetryableRejection(error);
183
+ if (rejected !== undefined) {
184
+ return rejected;
185
+ }
133
186
  return {
134
- response: Response.json(
135
- { ok: false, error: "background_dispatch_failed" },
136
- { status: 503 }
187
+ response: errorResponse(
188
+ 503,
189
+ "background_dispatch_failed",
190
+ boundedMessage(error instanceof Error ? error.message : String(error))
137
191
  ),
138
192
  };
139
193
  }
140
- if (deferredSend !== undefined) {
194
+ if (captured !== undefined) {
141
195
  return {
142
196
  response: Response.json({
143
197
  ok: true,
@@ -146,8 +200,8 @@ export async function dispatchHostedAdmission(
146
200
  }),
147
201
  pendingSend: {
148
202
  channelId: input.delivery.channelId,
149
- message: deferredSend.prompt,
150
- options: deferredSend.options,
203
+ message: captured.message,
204
+ options: captured.options,
151
205
  mailboxContinuationKey: input.delivery.mailboxContinuationKey,
152
206
  },
153
207
  };
@@ -241,6 +295,7 @@ export function registerHostedDeliveryRoute(input: {
241
295
  )
242
296
  );
243
297
  }
298
+ recordHostedManagedAutomation(delivery);
244
299
  const channel = input.channels.find(
245
300
  (candidate) => candidate.id === delivery.channelId
246
301
  );
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Managed-automation envelope slices by provider delivery id (plan 0002
3
+ * G2), recorded at hosted admission so channel code running the delivery's
4
+ * task (or session turn) can attach the customer's MCP servers through the
5
+ * control-plane relay. In-memory and TTL-bounded like the hosted pending-
6
+ * send table: the envelope slice carries names only (never credentials)
7
+ * and evaporates with the process.
8
+ *
9
+ * A leaf module (protocol type import only) so product channels can read
10
+ * it without pulling the hosted-delivery route — and its session-engine
11
+ * dependency graph — into their bundles.
12
+ *
13
+ * INTERNAL: first-party multi-tenant deployment plumbing. Deliberately
14
+ * excluded from the published `@cursor/july` npm package (see the `files`
15
+ * whitelist); consumed by internal deployments via relative source imports.
16
+ */
17
+
18
+ import type { HostedAdmissionManagedAutomation } from "./hosted-delivery-protocol.js";
19
+
20
+ const managedAutomationByDelivery = new Map<
21
+ string,
22
+ { managedAutomation: HostedAdmissionManagedAutomation; expiresAt: number }
23
+ >();
24
+ /** Long enough to outlive a slow review run kicked off by the delivery. */
25
+ const MANAGED_AUTOMATION_TTL_MS = 60 * 60_000;
26
+ const MANAGED_AUTOMATION_MAX_ENTRIES = 10_000;
27
+
28
+ export function recordHostedManagedAutomation(delivery: {
29
+ deliveryId: string;
30
+ managedAutomation?: HostedAdmissionManagedAutomation;
31
+ }): void {
32
+ if (delivery.managedAutomation === undefined) {
33
+ return;
34
+ }
35
+ const now = Date.now();
36
+ for (const [key, entry] of managedAutomationByDelivery) {
37
+ if (entry.expiresAt <= now) {
38
+ managedAutomationByDelivery.delete(key);
39
+ }
40
+ }
41
+ if (managedAutomationByDelivery.size >= MANAGED_AUTOMATION_MAX_ENTRIES) {
42
+ managedAutomationByDelivery.clear();
43
+ }
44
+ managedAutomationByDelivery.set(delivery.deliveryId, {
45
+ managedAutomation: delivery.managedAutomation,
46
+ expiresAt: now + MANAGED_AUTOMATION_TTL_MS,
47
+ });
48
+ }
49
+
50
+ /**
51
+ * The managed automation the control plane attributed to a hosted delivery,
52
+ * when the envelope carried one. Undefined for non-managed deliveries,
53
+ * expired entries, and non-hosted (webhook/stream) dispatch paths.
54
+ */
55
+ export function getHostedManagedAutomation(
56
+ deliveryId: string
57
+ ): HostedAdmissionManagedAutomation | undefined {
58
+ const entry = managedAutomationByDelivery.get(deliveryId);
59
+ if (entry === undefined) {
60
+ return undefined;
61
+ }
62
+ if (entry.expiresAt <= Date.now()) {
63
+ managedAutomationByDelivery.delete(deliveryId);
64
+ return undefined;
65
+ }
66
+ return entry.managedAutomation;
67
+ }
68
+
69
+ /** Drop recorded managed-automation slices (tests only). */
70
+ export function resetHostedManagedAutomationForTest(): void {
71
+ managedAutomationByDelivery.clear();
72
+ }