@cursor/july 0.1.94 → 0.1.96
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.
- package/AGENTS.md +3 -4
- package/dist/channels/deployments/types.d.ts +1 -1
- package/dist/channels/deployments/types.d.ts.map +1 -1
- package/dist/channels/deployments/types.js +1 -0
- package/dist/channels/github/github-channel.d.ts.map +1 -1
- package/dist/channels/github/github-channel.js +14 -2
- package/dist/channels/github/types.d.ts +18 -3
- package/dist/channels/github/types.d.ts.map +1 -1
- package/dist/channels/origin/api.d.ts +59 -13
- package/dist/channels/origin/api.d.ts.map +1 -1
- package/dist/channels/origin/api.js +168 -27
- package/dist/channels/origin/binding.d.ts +2 -2
- package/dist/channels/origin/checks.d.ts +6 -6
- package/dist/channels/origin/checks.js +6 -6
- package/dist/channels/origin/cursor-account.d.ts +5 -3
- package/dist/channels/origin/cursor-account.d.ts.map +1 -1
- package/dist/channels/origin/cursor-account.js +5 -3
- package/dist/channels/origin/origin-channel.d.ts.map +1 -1
- package/dist/channels/origin/origin-channel.js +11 -3
- package/dist/channels/origin/origin-webhook.d.ts +11 -1
- package/dist/channels/origin/origin-webhook.d.ts.map +1 -1
- package/dist/channels/origin/origin-webhook.js +23 -3
- package/dist/channels/origin/types.d.ts +8 -0
- package/dist/channels/origin/types.d.ts.map +1 -1
- package/dist/channels/slack/api.d.ts +9 -0
- package/dist/channels/slack/api.d.ts.map +1 -1
- package/dist/channels/slack/api.js +36 -5
- package/dist/channels/slack/defaults.d.ts.map +1 -1
- package/dist/channels/slack/defaults.js +17 -46
- package/dist/channels/slack/dispatch.d.ts +7 -1
- package/dist/channels/slack/dispatch.d.ts.map +1 -1
- package/dist/channels/slack/dispatch.js +47 -23
- package/dist/channels/slack/inbound.d.ts +8 -0
- package/dist/channels/slack/inbound.d.ts.map +1 -1
- package/dist/channels/slack/inbound.js +32 -0
- package/dist/channels/slack/live-delivery.d.ts +26 -29
- package/dist/channels/slack/live-delivery.d.ts.map +1 -1
- package/dist/channels/slack/live-delivery.js +172 -73
- package/dist/channels/slack/manifest.d.ts.map +1 -1
- package/dist/channels/slack/manifest.js +5 -1
- package/dist/channels/slack/nudge-carrier.d.ts +2 -0
- package/dist/channels/slack/nudge-carrier.d.ts.map +1 -1
- package/dist/channels/slack/nudge-carrier.js +48 -1
- package/dist/channels/slack/pr-ref.d.ts +7 -1
- package/dist/channels/slack/pr-ref.d.ts.map +1 -1
- package/dist/channels/slack/pr-ref.js +42 -23
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +2 -2
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +4 -4
- package/dist/docs/assets/{app.CFDEas4I.js → app.jXleCmOc.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.DFoGuETX.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.B1IIYpYS.js → VPLocalSearchBox.8LhyglIT.js} +1 -1
- package/dist/docs/assets/chunks/{theme.Ct4NSiLm.js → theme.9ged-pr2.js} +2 -2
- package/dist/docs/assets/index.md.CI93Ltlb.js +5 -0
- package/dist/docs/assets/{index.md.DRakGHFe.lean.js → index.md.CI93Ltlb.lean.js} +1 -1
- package/dist/docs/assets/{reference_cli.md.Cw6_ICYG.js → reference_cli.md.CCnG-nJx.js} +4 -2
- package/dist/docs/assets/{reference_cli.md.Cw6_ICYG.lean.js → reference_cli.md.CCnG-nJx.lean.js} +1 -1
- package/dist/docs/assets/{reference_skills.md.8son6Hjm.js → reference_skills.md.BFW9retM.js} +3 -3
- package/dist/docs/assets/{templates_agentic-owners.md.DqtPdm6f.js → templates_agentic-owners.md.BZSH4N9z.js} +1 -1
- package/dist/docs/assets/templates_code-wiki.md.CDYZIBFN.js +2 -0
- package/dist/docs/assets/templates_code-wiki.md.CDYZIBFN.lean.js +1 -0
- package/dist/docs/assets/{templates_demo.md.DhFcWN6j.js → templates_demo.md.BeQX9V3H.js} +1 -1
- package/dist/docs/assets/{templates_pr-autofixer.md.DU7dQpor.js → templates_pr-autofixer.md.x5zl6-GT.js} +2 -2
- package/dist/docs/assets/{templates_pr-autofixer.md.DU7dQpor.lean.js → templates_pr-autofixer.md.x5zl6-GT.lean.js} +1 -1
- package/dist/docs/assets/templates_security-help.md.C3Ny_Qr2.js +4 -0
- package/dist/docs/assets/templates_security-help.md.C3Ny_Qr2.lean.js +1 -0
- package/dist/docs/assets/{templates_security-reviewer.md.CTa7u_l1.js → templates_security-reviewer.md.lshxbCLK.js} +2 -2
- package/dist/docs/assets/{templates_security-reviewer.md.CTa7u_l1.lean.js → templates_security-reviewer.md.lshxbCLK.lean.js} +1 -1
- package/dist/docs/assets/{templates_triage.md.CVlpctKS.js → templates_triage.md.Co4UNzkZ.js} +3 -3
- package/dist/docs/assets/{templates_triage.md.CVlpctKS.lean.js → templates_triage.md.Co4UNzkZ.lean.js} +1 -1
- package/dist/docs/building-with-agents.html +4 -4
- package/dist/docs/concepts.html +4 -4
- package/dist/docs/deployment.html +4 -4
- package/dist/docs/evals.html +4 -4
- package/dist/docs/guides/agent-to-agent.html +4 -4
- package/dist/docs/guides/cloud-runtime.html +4 -4
- package/dist/docs/guides/convert-automation.html +4 -4
- package/dist/docs/guides/github.html +4 -4
- package/dist/docs/guides/human-in-the-loop.html +4 -4
- package/dist/docs/guides/mcp-oauth.html +4 -4
- package/dist/docs/guides/opentelemetry.html +4 -4
- package/dist/docs/guides/slack.html +4 -4
- package/dist/docs/guides/webhooks.html +4 -4
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +4 -4
- package/dist/docs/index.html +6 -6
- package/dist/docs/index.md +5 -1
- package/dist/docs/llms-full.txt +189 -26
- package/dist/docs/llms.txt +2 -0
- package/dist/docs/quickstart.html +4 -4
- package/dist/docs/reference/agent-config.html +4 -4
- package/dist/docs/reference/artifacts.html +4 -4
- package/dist/docs/reference/channels.html +4 -4
- package/dist/docs/reference/cli.html +8 -6
- package/dist/docs/reference/cli.md +4 -1
- package/dist/docs/reference/connections.html +4 -4
- package/dist/docs/reference/hooks.html +4 -4
- package/dist/docs/reference/http-api.html +4 -4
- package/dist/docs/reference/instructions.html +4 -4
- package/dist/docs/reference/playground.html +4 -4
- package/dist/docs/reference/project-layout.html +4 -4
- package/dist/docs/reference/prompt.html +4 -4
- package/dist/docs/reference/schedules.html +4 -4
- package/dist/docs/reference/sessions.html +4 -4
- package/dist/docs/reference/skills.html +7 -7
- package/dist/docs/reference/subagents.html +4 -4
- package/dist/docs/reference/tools.html +4 -4
- package/dist/docs/scaffolding-agents.html +4 -4
- package/dist/docs/storage.html +4 -4
- package/dist/docs/templates/agentic-owners.html +7 -7
- package/dist/docs/templates/agentic-owners.md +1 -1
- package/dist/docs/templates/code-wiki.html +28 -0
- package/dist/docs/templates/code-wiki.md +91 -0
- package/dist/docs/templates/demo.html +6 -6
- package/dist/docs/templates/demo.md +3 -2
- package/dist/docs/templates/pr-autofixer.html +6 -6
- package/dist/docs/templates/pr-autofixer.md +7 -13
- package/dist/docs/templates/security-help.html +30 -0
- package/dist/docs/templates/security-help.md +65 -0
- package/dist/docs/templates/security-reviewer.html +6 -6
- package/dist/docs/templates/security-reviewer.md +1 -2
- package/dist/docs/templates/triage.html +7 -7
- package/dist/docs/templates/triage.md +2 -6
- package/dist/docs/troubleshooting.html +4 -4
- package/dist/files-backends/cursor-hosted.d.ts +26 -0
- package/dist/files-backends/cursor-hosted.d.ts.map +1 -1
- package/dist/files-backends/cursor-hosted.js +32 -4
- package/dist/files.d.ts +2 -0
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +5 -0
- package/dist/internal/advertise-tools.d.ts +2 -0
- package/dist/internal/advertise-tools.d.ts.map +1 -1
- package/dist/internal/advertise-tools.js +15 -1
- package/dist/internal/bounded.d.ts +7 -0
- package/dist/internal/bounded.d.ts.map +1 -0
- package/dist/internal/bounded.js +32 -0
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +142 -7
- package/dist/internal/conversation-mirror.d.ts +85 -0
- package/dist/internal/conversation-mirror.d.ts.map +1 -0
- package/dist/internal/conversation-mirror.js +312 -0
- package/dist/internal/cursor/backend-client.d.ts.map +1 -1
- package/dist/internal/cursor/backend-client.js +22 -48
- package/dist/internal/cursor/session-token.d.ts +45 -0
- package/dist/internal/cursor/session-token.d.ts.map +1 -0
- package/dist/internal/cursor/session-token.js +99 -0
- package/dist/internal/cursor-agent-template.d.ts +1 -1
- package/dist/internal/cursor-agent-template.d.ts.map +1 -1
- package/dist/internal/cursor-agent-template.js +1 -0
- package/dist/internal/deferred-channel-session.d.ts +20 -0
- package/dist/internal/deferred-channel-session.d.ts.map +1 -0
- package/dist/internal/deferred-channel-session.js +62 -0
- package/dist/internal/deploy-client.d.ts +13 -1
- package/dist/internal/deploy-client.d.ts.map +1 -1
- package/dist/internal/deploy-client.js +11 -1
- package/dist/internal/deploy-manifest.d.ts.map +1 -1
- package/dist/internal/deploy-manifest.js +11 -5
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +22 -2
- package/dist/internal/framework-file-storage.d.ts +61 -0
- package/dist/internal/framework-file-storage.d.ts.map +1 -0
- package/dist/internal/framework-file-storage.js +204 -0
- package/dist/internal/framework-storage-selection.d.ts +38 -16
- package/dist/internal/framework-storage-selection.d.ts.map +1 -1
- package/dist/internal/framework-storage-selection.js +78 -17
- package/dist/internal/hosted-admission-context.d.ts +20 -0
- package/dist/internal/hosted-admission-context.d.ts.map +1 -0
- package/dist/internal/hosted-admission-context.js +31 -0
- package/dist/internal/hosted-delivery-protocol.d.ts +5 -0
- package/dist/internal/hosted-delivery-protocol.d.ts.map +1 -1
- package/dist/internal/hosted-delivery-protocol.js +33 -1
- package/dist/internal/hosted-delivery.d.ts +11 -2
- package/dist/internal/hosted-delivery.d.ts.map +1 -1
- package/dist/internal/hosted-delivery.js +79 -35
- package/dist/internal/hosted-managed-automation.d.ts +30 -0
- package/dist/internal/hosted-managed-automation.d.ts.map +1 -0
- package/dist/internal/hosted-managed-automation.js +58 -0
- package/dist/internal/init-project.d.ts.map +1 -1
- package/dist/internal/init-project.js +2 -0
- package/dist/internal/install-cursor-skills.d.ts.map +1 -1
- package/dist/internal/install-cursor-skills.js +1 -3
- package/dist/internal/managed-delivery.d.ts +80 -0
- package/dist/internal/managed-delivery.d.ts.map +1 -0
- package/dist/internal/managed-delivery.js +86 -0
- package/dist/internal/review-comments.d.ts.map +1 -1
- package/dist/internal/review-comments.js +10 -98
- package/dist/internal/runtime-dispatch-runner.d.ts +5 -1
- package/dist/internal/runtime-dispatch-runner.d.ts.map +1 -1
- package/dist/internal/runtime-dispatch-runner.js +7 -0
- package/dist/internal/scm/pr-url.d.ts +14 -0
- package/dist/internal/scm/pr-url.d.ts.map +1 -0
- package/dist/internal/scm/pr-url.js +65 -0
- package/dist/internal/sdk-runner.d.ts +25 -0
- package/dist/internal/sdk-runner.d.ts.map +1 -1
- package/dist/internal/sdk-runner.js +70 -0
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +11 -2
- package/dist/internal/session-engine.d.ts +13 -0
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +71 -3
- package/dist/internal/steering.d.ts +15 -0
- package/dist/internal/steering.d.ts.map +1 -0
- package/dist/internal/steering.js +18 -0
- package/dist/playground/assets/index-B1DDvBeC.css +1 -0
- package/dist/playground/assets/{index-B3JCyigB.js → index-Cx9CclK_.js} +47 -47
- package/dist/playground/index.html +2 -2
- package/dist/storage-backends/cursor-hosted.d.ts +8 -0
- package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
- package/dist/storage-backends/cursor-hosted.js +4 -3
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +5 -1
- package/docs/reference/cli.md +4 -1
- package/docs/templates/agentic-owners.md +1 -1
- package/docs/templates/code-wiki.md +96 -0
- package/docs/templates/demo.md +3 -2
- package/docs/templates/pr-autofixer.md +7 -13
- package/docs/templates/security-help.md +70 -0
- package/docs/templates/security-reviewer.md +1 -2
- package/docs/templates/triage.md +2 -6
- package/package.json +2 -2
- package/skills/ab/SKILL.md +13 -97
- package/skills/create-agent/SKILL.md +123 -223
- package/skills/debug/SKILL.md +34 -68
- package/skills/evals/SKILL.md +55 -180
- package/skills/framework-map/SKILL.md +83 -145
- package/skills/github/SKILL.md +53 -111
- package/skills/hillclimb/SKILL.md +40 -163
- package/skills/mcp-auth/SKILL.md +48 -107
- package/skills/otel/SKILL.md +22 -68
- package/skills/setup-slack/SKILL.md +77 -208
- package/src/channels/deployments/types.ts +1 -0
- package/src/channels/github/github-channel.ts +23 -8
- package/src/channels/github/types.ts +19 -2
- package/src/channels/origin/api.ts +242 -30
- package/src/channels/origin/binding.ts +2 -2
- package/src/channels/origin/checks.ts +6 -6
- package/src/channels/origin/cursor-account.ts +5 -3
- package/src/channels/origin/origin-channel.ts +13 -1
- package/src/channels/origin/origin-webhook.ts +27 -3
- package/src/channels/origin/types.ts +8 -0
- package/src/channels/slack/api.ts +41 -5
- package/src/channels/slack/defaults.ts +15 -49
- package/src/channels/slack/dispatch.ts +56 -22
- package/src/channels/slack/inbound.ts +43 -0
- package/src/channels/slack/live-delivery.ts +235 -114
- package/src/channels/slack/manifest.ts +5 -1
- package/src/channels/slack/nudge-carrier.ts +48 -1
- package/src/channels/slack/pr-ref.ts +56 -25
- package/src/channels/slack/slack-channel.ts +2 -0
- package/src/files-backends/cursor-hosted.ts +41 -2
- package/src/files.ts +11 -0
- package/src/internal/advertise-tools.ts +18 -2
- package/src/internal/bounded.ts +28 -0
- package/src/internal/cli-deploy.ts +183 -7
- package/src/internal/conversation-mirror.ts +406 -0
- package/src/internal/cursor/backend-client.ts +34 -58
- package/src/internal/cursor/session-token.ts +115 -0
- package/src/internal/cursor-agent-template.ts +1 -0
- package/src/internal/deferred-channel-session.ts +61 -0
- package/src/internal/deploy-client.ts +24 -1
- package/src/internal/deploy-manifest.ts +10 -5
- package/src/internal/discovery.ts +25 -2
- package/src/internal/framework-file-storage.ts +222 -0
- package/src/internal/framework-storage-selection.ts +101 -19
- package/src/internal/hosted-admission-context.ts +37 -0
- package/src/internal/hosted-delivery-protocol.ts +44 -1
- package/src/internal/hosted-delivery.ts +105 -40
- package/src/internal/hosted-managed-automation.ts +72 -0
- package/src/internal/init-project.ts +2 -0
- package/src/internal/install-cursor-skills.ts +1 -4
- package/src/internal/managed-delivery.ts +125 -0
- package/src/internal/review-comments.ts +10 -113
- package/src/internal/runtime-dispatch-runner.ts +10 -0
- package/src/internal/scm/pr-url.ts +95 -0
- package/src/internal/sdk-runner.ts +103 -0
- package/src/internal/server.ts +12 -0
- package/src/internal/session-engine.ts +76 -4
- package/src/internal/steering.ts +17 -0
- package/src/storage-backends/cursor-hosted.ts +10 -1
- package/src/types.ts +2 -1
- package/templates/agentic-owners/README.md +1 -1
- package/templates/agentic-owners/agent/agent.ts +0 -10
- package/templates/agentic-owners/agent/channels/github.ts +5 -14
- package/templates/agentic-owners/agent/lib/config.ts +0 -8
- package/templates/agentic-owners/agent/lib/review.ts +2 -15
- package/templates/agentic-owners/agent/tools/record_review.ts +2 -4
- package/templates/code-wiki/README.md +34 -0
- package/templates/code-wiki/agent/agent.ts +8 -0
- package/templates/code-wiki/agent/channels/github.ts +144 -0
- package/templates/code-wiki/agent/instructions.md +39 -0
- package/templates/code-wiki/agent/lib/repos.ts +5 -0
- package/templates/code-wiki/agent/lib/wiki.ts +22 -0
- package/templates/code-wiki/agent/tools/apply_wiki.ts +219 -0
- package/templates/code-wiki/evals/evals.config.ts +5 -0
- package/templates/code-wiki/evals/wiki.eval.ts +132 -0
- package/templates/code-wiki/init.json +25 -0
- package/templates/code-wiki/package.json +18 -0
- package/templates/code-wiki/tsconfig.json +12 -0
- package/templates/demo/agent/agent.ts +0 -10
- package/templates/pr-autofixer/README.md +0 -2
- package/templates/pr-autofixer/agent/agent.ts +0 -11
- package/templates/pr-autofixer/agent/channels/slack.ts +0 -7
- package/templates/pr-autofixer/agent/lib/pr-state.ts +5 -17
- package/templates/pr-autofixer/agent/lib/repos.ts +0 -1
- package/templates/security-help/README.md +2 -2
- package/templates/security-help/agent/agent.ts +1 -2
- package/templates/security-help/agent/channels/slack.ts +0 -3
- package/templates/security-help/agent/instructions.md +9 -10
- package/templates/security-help/agent/skills/access-request.md +1 -1
- package/templates/security-help/agent/skills/faq.md +31 -0
- package/templates/security-help/agent/skills/security-playbooks.md +1 -1
- package/templates/security-help/package.json +1 -2
- package/templates/security-reviewer/agent/agent.ts +0 -10
- package/templates/triage/agent/agent.ts +0 -10
- package/templates/triage/agent/channels/queue.ts +1 -1
- package/templates/triage/agent/channels/webhook.ts +1 -3
- package/dist/docs/assets/chunks/@localSearchIndexroot.DU3U2Ij2.js +0 -1
- package/dist/docs/assets/index.md.DRakGHFe.js +0 -5
- package/dist/internal/deployment-scm-event-stream.d.ts +0 -116
- package/dist/internal/deployment-scm-event-stream.d.ts.map +0 -1
- package/dist/internal/deployment-scm-event-stream.js +0 -474
- package/dist/playground/assets/index-D9N7-q97.css +0 -1
- package/src/internal/deployment-scm-event-stream.ts +0 -593
- package/templates/security-help/agent/knowledge/faq/approvals.md +0 -5
- package/templates/security-help/agent/knowledge/faq/channels.md +0 -6
- package/templates/security-help/agent/knowledge/faq/phishing.md +0 -10
- package/templates/security-help/agent/skills/security-first-pass.md +0 -15
- /package/dist/docs/assets/{reference_skills.md.8son6Hjm.lean.js → reference_skills.md.BFW9retM.lean.js} +0 -0
- /package/dist/docs/assets/{templates_agentic-owners.md.DqtPdm6f.lean.js → templates_agentic-owners.md.BZSH4N9z.lean.js} +0 -0
- /package/dist/docs/assets/{templates_demo.md.DhFcWN6j.lean.js → templates_demo.md.BeQX9V3H.lean.js} +0 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable mirror for the SDK runner's conversation store.
|
|
3
|
+
*
|
|
4
|
+
* The Cursor SDK keeps each local conversation in SQLite under the engine's
|
|
5
|
+
* state root (`runner/index.db` + `runner/agents/agent-<hash>/store.db`).
|
|
6
|
+
* That disk dies with the pod: on architecture v2, session **records** and
|
|
7
|
+
* event chunks survive replacement through the control-plane storage mirror
|
|
8
|
+
* (framework-storage-selection.ts), but the conversation itself was never
|
|
9
|
+
* hydrated anywhere — a replacement pod resumed every thread as a fresh
|
|
10
|
+
* conversation.
|
|
11
|
+
*
|
|
12
|
+
* This module mirrors the conversation through the SDK's own store APIs —
|
|
13
|
+
* the agent document (which carries `sdkMetadata.blobEncryptionKey`, without
|
|
14
|
+
* which the checkpoint blobs cannot be decrypted) plus its content-addressed
|
|
15
|
+
* checkpoint blobs — into the deployment's **framework-level** file store
|
|
16
|
+
* (`AGENT_SERVE_FRAMEWORK_STORE_SOURCE_ID`), alongside session records and
|
|
17
|
+
* transcripts and apart from the author-facing user store. The store is an
|
|
18
|
+
* Agent Store reached over `/v0/agent-serve-files` presigned URLs — **not
|
|
19
|
+
* mounted** as a filesystem in the engine VM (the FUSE mount at
|
|
20
|
+
* {@link DEPLOYMENT_STORE_MOUNT_PATH} exists only on cloud-runtime VMs) —
|
|
21
|
+
* so the mirror moves bytes through the {@link FileSink} contract instead.
|
|
22
|
+
*
|
|
23
|
+
* Going through the store APIs rather than copying `store.db` files sidesteps
|
|
24
|
+
* WAL consistency and the shared `index.db` (per-session raw copies would
|
|
25
|
+
* clobber other sessions' rows); checkpoint blobs are content-addressed, so
|
|
26
|
+
* repeat persists upload only what is new.
|
|
27
|
+
*
|
|
28
|
+
* Timing: the runner hydrates before `Agent.resume` (so a follow-up landing
|
|
29
|
+
* on a fresh pod — including a v2 hosted-execution delivery — finds the
|
|
30
|
+
* conversation before the turn runs) and persists after each local turn
|
|
31
|
+
* settles, **awaited**, because a hosted pod can be parked immediately after
|
|
32
|
+
* the execution reply.
|
|
33
|
+
*
|
|
34
|
+
* Run rows and run events are deliberately not mirrored: they are per-pod
|
|
35
|
+
* bookkeeping (turn numbers, stream offsets), and the durable session event
|
|
36
|
+
* stream already lives in the control-plane storage mirror.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import type { LocalAgentDocument, LocalAgentStore } from "@cursor/sdk";
|
|
40
|
+
import { agentStoreKeys, type FileSink } from "../files.js";
|
|
41
|
+
import {
|
|
42
|
+
cursorFrameworkStoreSourceId,
|
|
43
|
+
cursorHostedFiles,
|
|
44
|
+
} from "../files-backends/cursor-hosted.js";
|
|
45
|
+
import type { AgentProject } from "../types.js";
|
|
46
|
+
import { mapBounded } from "./bounded.js";
|
|
47
|
+
import { resolveApiKeySync } from "./cursor/credentials.js";
|
|
48
|
+
import { AGENT_SERVE_GENERATION_ENV_VAR } from "./cursor-relay-core.js";
|
|
49
|
+
import { describeError } from "./describe-error.js";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Persist/restore one SDK conversation between a {@link LocalAgentStore} and
|
|
53
|
+
* durable file storage. Implemented by {@link createConversationMirror};
|
|
54
|
+
* consumed by the SDK runner.
|
|
55
|
+
*/
|
|
56
|
+
export interface ConversationMirror {
|
|
57
|
+
/**
|
|
58
|
+
* Restore `sdkAgentId`'s conversation into `store` when the store does not
|
|
59
|
+
* already hold it. Resolves `true` when a conversation was restored,
|
|
60
|
+
* `false` on a definitive miss (never persisted, or already local — the
|
|
61
|
+
* caller falls through to its fresh-conversation path). A sink **failure**
|
|
62
|
+
* propagates: an outage must fail the turn (retryable) rather than
|
|
63
|
+
* silently fork the thread onto a fresh conversation.
|
|
64
|
+
*/
|
|
65
|
+
hydrate(store: LocalAgentStore, sdkAgentId: string): Promise<boolean>;
|
|
66
|
+
/**
|
|
67
|
+
* Mirror `sdkAgentId`'s conversation from `store` to the sink
|
|
68
|
+
* (last write wins). No-op until the conversation has a checkpoint.
|
|
69
|
+
*/
|
|
70
|
+
persist(store: LocalAgentStore, sdkAgentId: string): Promise<void>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Wire shape at {@link agentStoreKeys.conversationManifest}. */
|
|
74
|
+
interface ConversationManifestV1 {
|
|
75
|
+
v: 1;
|
|
76
|
+
/** The SDK agent document, verbatim apart from `activeRunId` (nulled). */
|
|
77
|
+
agent: LocalAgentDocument;
|
|
78
|
+
/** Checkpoint blob ids present under the conversation's blob prefix. */
|
|
79
|
+
blobIds: string[];
|
|
80
|
+
persistedAtMs: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface CreateConversationMirrorOptions {
|
|
84
|
+
sink: FileSink;
|
|
85
|
+
/** Agent name baked into every minted key. */
|
|
86
|
+
agentName: string;
|
|
87
|
+
logger?: (line: string) => void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Build a {@link ConversationMirror} over a {@link FileSink}. */
|
|
91
|
+
export function createConversationMirror(
|
|
92
|
+
options: CreateConversationMirrorOptions
|
|
93
|
+
): ConversationMirror {
|
|
94
|
+
const { sink, agentName } = options;
|
|
95
|
+
const logger = options.logger ?? (() => {});
|
|
96
|
+
/**
|
|
97
|
+
* Blob ids known to be in the sink, per SDK agent id (upload dedupe).
|
|
98
|
+
* Bounded oldest-out like the files backend's etag cache: a resident
|
|
99
|
+
* engine touches an unbounded number of conversations over its lifetime,
|
|
100
|
+
* and a miss costs one manifest read, never correctness.
|
|
101
|
+
*/
|
|
102
|
+
const mirrored = new Map<string, Set<string>>();
|
|
103
|
+
const rememberMirrored = (sdkAgentId: string, blobIds: Set<string>): void => {
|
|
104
|
+
if (
|
|
105
|
+
!mirrored.has(sdkAgentId) &&
|
|
106
|
+
mirrored.size >= MAX_TRACKED_CONVERSATIONS
|
|
107
|
+
) {
|
|
108
|
+
const oldest = mirrored.keys().next();
|
|
109
|
+
if (!oldest.done) {
|
|
110
|
+
mirrored.delete(oldest.value);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
mirrored.set(sdkAgentId, blobIds);
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Per-conversation serialization so a persist cannot interleave with a
|
|
117
|
+
* hydrate (or another persist) for the same conversation. Entries are
|
|
118
|
+
* removed once their chain settles as the tail, so the map tracks only
|
|
119
|
+
* conversations with work in flight.
|
|
120
|
+
*/
|
|
121
|
+
const chains = new Map<string, Promise<unknown>>();
|
|
122
|
+
|
|
123
|
+
function serialize<T>(sdkAgentId: string, run: () => Promise<T>): Promise<T> {
|
|
124
|
+
const previous = chains.get(sdkAgentId) ?? Promise.resolve();
|
|
125
|
+
const next = previous.then(run, run);
|
|
126
|
+
const stored = next
|
|
127
|
+
.catch(() => {})
|
|
128
|
+
.then(() => {
|
|
129
|
+
if (chains.get(sdkAgentId) === stored) {
|
|
130
|
+
chains.delete(sdkAgentId);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
chains.set(sdkAgentId, stored);
|
|
134
|
+
return next;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function mirroredBlobIds(sdkAgentId: string): Promise<Set<string>> {
|
|
138
|
+
const cached = mirrored.get(sdkAgentId);
|
|
139
|
+
if (cached !== undefined) {
|
|
140
|
+
return cached;
|
|
141
|
+
}
|
|
142
|
+
// Seed from the previous manifest so a fresh pod does not re-upload the
|
|
143
|
+
// whole history on its first persist. A miss, an unusable manifest, or
|
|
144
|
+
// a failed read seeds empty — re-uploading is wasteful, never wrong.
|
|
145
|
+
const seeded = new Set<string>();
|
|
146
|
+
try {
|
|
147
|
+
const read = await readManifest(sink, agentName, sdkAgentId);
|
|
148
|
+
if (read.outcome === "found") {
|
|
149
|
+
for (const blobId of read.manifest.blobIds) {
|
|
150
|
+
seeded.add(blobId);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch (error) {
|
|
154
|
+
logger(
|
|
155
|
+
`[agent-sdk] conversation mirror: could not read the prior manifest for ${sdkAgentId} (${describeError(error)}); re-uploading blobs`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
rememberMirrored(sdkAgentId, seeded);
|
|
159
|
+
return seeded;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
persist(store, sdkAgentId): Promise<void> {
|
|
164
|
+
return serialize(sdkAgentId, async () => {
|
|
165
|
+
const document = await store.agents.get({ agentId: sdkAgentId });
|
|
166
|
+
if (document === null || document.latestCheckpoint == null) {
|
|
167
|
+
// Nothing restorable yet (no conversation, or no checkpoint).
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const uploaded = await mirroredBlobIds(sdkAgentId);
|
|
171
|
+
const blobIds = await listCheckpointBlobIds(store, sdkAgentId);
|
|
172
|
+
for (const blobId of blobIds) {
|
|
173
|
+
if (uploaded.has(blobId)) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const bytes = await store.checkpoints.get({
|
|
177
|
+
agentId: sdkAgentId,
|
|
178
|
+
blobId,
|
|
179
|
+
});
|
|
180
|
+
if (bytes === null) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
await sink.put(
|
|
184
|
+
agentStoreKeys.conversationBlob(agentName, sdkAgentId, blobId),
|
|
185
|
+
bytes
|
|
186
|
+
);
|
|
187
|
+
// Content-addressed: once in the sink, never re-uploaded.
|
|
188
|
+
uploaded.add(blobId);
|
|
189
|
+
}
|
|
190
|
+
const manifest: ConversationManifestV1 = {
|
|
191
|
+
v: 1,
|
|
192
|
+
// The referenced run is pod-local bookkeeping and is not mirrored;
|
|
193
|
+
// a hydrated store must not point at a run row it does not hold.
|
|
194
|
+
agent: { ...document, activeRunId: null },
|
|
195
|
+
blobIds: blobIds.filter((blobId) => uploaded.has(blobId)),
|
|
196
|
+
persistedAtMs: Date.now(),
|
|
197
|
+
};
|
|
198
|
+
// Manifest last: it only ever references blobs already uploaded.
|
|
199
|
+
await sink.put(
|
|
200
|
+
agentStoreKeys.conversationManifest(agentName, sdkAgentId),
|
|
201
|
+
new TextEncoder().encode(JSON.stringify(manifest))
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
hydrate(store, sdkAgentId): Promise<boolean> {
|
|
207
|
+
return serialize(sdkAgentId, async () => {
|
|
208
|
+
const existing = await store.agents.get({ agentId: sdkAgentId });
|
|
209
|
+
if (existing !== null) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
const read = await readManifest(sink, agentName, sdkAgentId);
|
|
213
|
+
if (read.outcome === "miss") {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
if (read.outcome === "unusable") {
|
|
217
|
+
// Definitive, non-transient state (a future manifest version
|
|
218
|
+
// during a rolling redeploy, or corruption): retrying would fail
|
|
219
|
+
// identically forever, so fall through to the caller's
|
|
220
|
+
// fresh-conversation path instead of wedging the session.
|
|
221
|
+
logger(
|
|
222
|
+
`[agent-sdk] conversation mirror: the manifest for ${sdkAgentId} is unusable (${read.reason}); treating the conversation as unrestorable`
|
|
223
|
+
);
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
const manifest = read.manifest;
|
|
227
|
+
const rootBlobId = manifest.agent.latestCheckpoint?.rootBlobId;
|
|
228
|
+
if (
|
|
229
|
+
rootBlobId === undefined ||
|
|
230
|
+
!manifest.blobIds.includes(rootBlobId)
|
|
231
|
+
) {
|
|
232
|
+
logger(
|
|
233
|
+
`[agent-sdk] conversation mirror: the manifest for ${sdkAgentId} does not list its root checkpoint blob; treating the conversation as unrestorable`
|
|
234
|
+
);
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
// Bounded-parallel downloads: this sits on the resume critical path
|
|
238
|
+
// (a presign plus an object GET per blob against the hosted sink).
|
|
239
|
+
const bodies = await mapBounded(
|
|
240
|
+
manifest.blobIds,
|
|
241
|
+
HYDRATE_DOWNLOAD_CONCURRENCY,
|
|
242
|
+
async (blobId) => ({
|
|
243
|
+
blobId,
|
|
244
|
+
body: await sink.get(
|
|
245
|
+
agentStoreKeys.conversationBlob(agentName, sdkAgentId, blobId)
|
|
246
|
+
),
|
|
247
|
+
})
|
|
248
|
+
);
|
|
249
|
+
const missing = bodies.filter((entry) => entry.body === undefined);
|
|
250
|
+
if (missing.length > 0) {
|
|
251
|
+
// The manifest lists exactly what a previous persist uploaded, so
|
|
252
|
+
// any definitive absence is store loss — and the checkpoint graph
|
|
253
|
+
// may reach any listed blob, not just the root. Restoring a
|
|
254
|
+
// partial graph would create an agent row whose resume succeeds
|
|
255
|
+
// and whose checkpoint walk then fails unrecoverably.
|
|
256
|
+
logger(
|
|
257
|
+
`[agent-sdk] conversation mirror: ${missing.length} checkpoint blob(s) for ${sdkAgentId} are missing from the sink; treating the conversation as unrestorable`
|
|
258
|
+
);
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
const restored = new Set<string>();
|
|
262
|
+
for (const entry of bodies) {
|
|
263
|
+
const body = entry.body as NonNullable<typeof entry.body>;
|
|
264
|
+
const bytes = body instanceof Uint8Array ? body : body.body;
|
|
265
|
+
if (
|
|
266
|
+
(await store.checkpoints.get({
|
|
267
|
+
agentId: sdkAgentId,
|
|
268
|
+
blobId: entry.blobId,
|
|
269
|
+
})) === null
|
|
270
|
+
) {
|
|
271
|
+
await store.checkpoints.create({
|
|
272
|
+
agentId: sdkAgentId,
|
|
273
|
+
blobId: entry.blobId,
|
|
274
|
+
data: bytes,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
restored.add(entry.blobId);
|
|
278
|
+
}
|
|
279
|
+
await store.agents.create({
|
|
280
|
+
agent: { ...manifest.agent, activeRunId: null },
|
|
281
|
+
});
|
|
282
|
+
rememberMirrored(sdkAgentId, restored);
|
|
283
|
+
logger(
|
|
284
|
+
`[agent-sdk] conversation mirror: restored the conversation for ${sdkAgentId} (${restored.size} checkpoint blob(s))`
|
|
285
|
+
);
|
|
286
|
+
return true;
|
|
287
|
+
});
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Oldest-out cap on the per-conversation upload-dedupe cache. */
|
|
293
|
+
const MAX_TRACKED_CONVERSATIONS = 1_000;
|
|
294
|
+
|
|
295
|
+
/** Parallel blob downloads during hydrate (resume critical path). */
|
|
296
|
+
const HYDRATE_DOWNLOAD_CONCURRENCY = 8;
|
|
297
|
+
|
|
298
|
+
type ManifestRead =
|
|
299
|
+
/** No manifest was ever persisted (definitive miss). */
|
|
300
|
+
| { outcome: "miss" }
|
|
301
|
+
/**
|
|
302
|
+
* A manifest exists but cannot be used (future version, corruption,
|
|
303
|
+
* wrong agent). Definitive and non-transient — callers must not retry,
|
|
304
|
+
* and must not treat it as a sink outage.
|
|
305
|
+
*/
|
|
306
|
+
| { outcome: "unusable"; reason: string }
|
|
307
|
+
| { outcome: "found"; manifest: ConversationManifestV1 };
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Read the persisted manifest. Sink transport failures propagate (an
|
|
311
|
+
* outage must fail the follow-up, not fork the thread); everything the
|
|
312
|
+
* bytes themselves can be wrong about is reported as `unusable`.
|
|
313
|
+
*/
|
|
314
|
+
async function readManifest(
|
|
315
|
+
sink: FileSink,
|
|
316
|
+
agentName: string,
|
|
317
|
+
sdkAgentId: string
|
|
318
|
+
): Promise<ManifestRead> {
|
|
319
|
+
const body = await sink.get(
|
|
320
|
+
agentStoreKeys.conversationManifest(agentName, sdkAgentId)
|
|
321
|
+
);
|
|
322
|
+
if (body === undefined) {
|
|
323
|
+
return { outcome: "miss" };
|
|
324
|
+
}
|
|
325
|
+
const bytes = body instanceof Uint8Array ? body : body.body;
|
|
326
|
+
let parsed: unknown;
|
|
327
|
+
try {
|
|
328
|
+
parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
329
|
+
} catch {
|
|
330
|
+
return { outcome: "unusable", reason: "not JSON" };
|
|
331
|
+
}
|
|
332
|
+
if (
|
|
333
|
+
parsed === null ||
|
|
334
|
+
typeof parsed !== "object" ||
|
|
335
|
+
(parsed as { v?: unknown }).v !== 1
|
|
336
|
+
) {
|
|
337
|
+
return { outcome: "unusable", reason: "unsupported manifest version" };
|
|
338
|
+
}
|
|
339
|
+
const manifest = parsed as ConversationManifestV1;
|
|
340
|
+
if (
|
|
341
|
+
manifest.agent === null ||
|
|
342
|
+
typeof manifest.agent !== "object" ||
|
|
343
|
+
manifest.agent.agentId !== sdkAgentId ||
|
|
344
|
+
!Array.isArray(manifest.blobIds)
|
|
345
|
+
) {
|
|
346
|
+
return { outcome: "unusable", reason: "malformed manifest" };
|
|
347
|
+
}
|
|
348
|
+
return { outcome: "found", manifest };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function listCheckpointBlobIds(
|
|
352
|
+
store: LocalAgentStore,
|
|
353
|
+
sdkAgentId: string
|
|
354
|
+
): Promise<string[]> {
|
|
355
|
+
const blobIds: string[] = [];
|
|
356
|
+
let cursor: string | undefined;
|
|
357
|
+
do {
|
|
358
|
+
const page = await store.checkpoints.list({
|
|
359
|
+
filter: { agentIds: [sdkAgentId], cursor },
|
|
360
|
+
});
|
|
361
|
+
blobIds.push(...page.items);
|
|
362
|
+
cursor = page.nextCursor;
|
|
363
|
+
} while (cursor !== undefined);
|
|
364
|
+
return blobIds;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Whether (and over which sink) this serve process mirrors conversations.
|
|
369
|
+
*
|
|
370
|
+
* Selected for architecture-v2 agents on Cursor-managed hosting — the same
|
|
371
|
+
* generation signal as framework-storage-selection.ts, plus a provisioned
|
|
372
|
+
* **framework-level** file store: conversation state is engine-owned, so it
|
|
373
|
+
* lives with session records and transcripts, never on the author-facing
|
|
374
|
+
* user store. Self-hosted v2 keeps everything on the state root (the
|
|
375
|
+
* operator owns the disk), and v1 keeps today's behavior while it runs.
|
|
376
|
+
*/
|
|
377
|
+
export function selectConversationMirror(input: {
|
|
378
|
+
project: AgentProject;
|
|
379
|
+
logger?: (line: string) => void;
|
|
380
|
+
env?: Record<string, string | undefined>;
|
|
381
|
+
}): ConversationMirror | undefined {
|
|
382
|
+
if (input.project.agent.architecture !== "v2") {
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
const env = input.env ?? process.env;
|
|
386
|
+
const frameworkStoreId = cursorFrameworkStoreSourceId(
|
|
387
|
+
env as NodeJS.ProcessEnv
|
|
388
|
+
);
|
|
389
|
+
if (
|
|
390
|
+
env[AGENT_SERVE_GENERATION_ENV_VAR] === undefined ||
|
|
391
|
+
frameworkStoreId === undefined
|
|
392
|
+
) {
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
return createConversationMirror({
|
|
396
|
+
sink: cursorHostedFiles({
|
|
397
|
+
storeSourceId: frameworkStoreId,
|
|
398
|
+
// Full chain (env → key file → stored login): frozen engines listen
|
|
399
|
+
// unsigned and bind CURSOR_API_KEY_FILE at task time, after this sink
|
|
400
|
+
// is constructed.
|
|
401
|
+
resolveApiKey: () => resolveApiKeySync()?.apiKey,
|
|
402
|
+
}),
|
|
403
|
+
agentName: input.project.name,
|
|
404
|
+
logger: input.logger,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
@@ -17,14 +17,15 @@ import { randomUUID } from "node:crypto";
|
|
|
17
17
|
import { z } from "zod";
|
|
18
18
|
import type { McpToolAnnotations } from "../../types.js";
|
|
19
19
|
import { boundedToolAnnotations } from "../mcp-host.js";
|
|
20
|
+
import {
|
|
21
|
+
CURSOR_SESSION_EXCHANGE_PATH,
|
|
22
|
+
CursorSessionExchangeError,
|
|
23
|
+
exchangeCursorApiKeyForSession,
|
|
24
|
+
SESSION_TOKEN_EXPIRY_LEEWAY_MS,
|
|
25
|
+
} from "./session-token.js";
|
|
20
26
|
|
|
21
27
|
const CLIENT_VERSION = "agent-serve-0.1.0";
|
|
22
|
-
const EXCHANGE_PATH = "/auth/exchange_user_api_key";
|
|
23
28
|
const SERVICE_PREFIX = "/aiserver.v1.DashboardService";
|
|
24
|
-
/** Re-exchange when the cached token is within this window of expiry. */
|
|
25
|
-
const TOKEN_EXPIRY_LEEWAY_MS = 5 * 60 * 1000;
|
|
26
|
-
/** Fallback token lifetime when the JWT `exp` claim cannot be parsed. */
|
|
27
|
-
const FALLBACK_TOKEN_LIFETIME_MS = 30 * 60 * 1000;
|
|
28
29
|
const LIST_TIMEOUT_MS = 60_000;
|
|
29
30
|
/** Backend-executed tools call out to third-party servers; be generous. */
|
|
30
31
|
const EXECUTE_TIMEOUT_MS = 120_000;
|
|
@@ -550,7 +551,7 @@ export class CursorBackendClient {
|
|
|
550
551
|
}
|
|
551
552
|
if (
|
|
552
553
|
this.token !== undefined &&
|
|
553
|
-
Date.now() < this.token.expiresAtMs -
|
|
554
|
+
Date.now() < this.token.expiresAtMs - SESSION_TOKEN_EXPIRY_LEEWAY_MS
|
|
554
555
|
) {
|
|
555
556
|
return Promise.resolve(this.token.value);
|
|
556
557
|
}
|
|
@@ -565,44 +566,39 @@ export class CursorBackendClient {
|
|
|
565
566
|
|
|
566
567
|
private async exchangeApiKey(): Promise<string> {
|
|
567
568
|
const apiKey = this.currentApiKey();
|
|
568
|
-
let
|
|
569
|
+
let session: { token: string; expiresAtMs: number };
|
|
569
570
|
try {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
authorization: `Bearer ${apiKey}`,
|
|
575
|
-
},
|
|
576
|
-
body: "{}",
|
|
577
|
-
signal: AbortSignal.timeout(30_000),
|
|
571
|
+
session = await exchangeCursorApiKeyForSession({
|
|
572
|
+
exchangeUrl: `${this.backendUrl}${CURSOR_SESSION_EXCHANGE_PATH}`,
|
|
573
|
+
apiKey,
|
|
574
|
+
fetchImpl: this.fetchImpl,
|
|
578
575
|
});
|
|
579
576
|
} catch (error) {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
577
|
+
if (error instanceof CursorSessionExchangeError) {
|
|
578
|
+
if (error.status === undefined) {
|
|
579
|
+
throw new CursorBackendError(
|
|
580
|
+
`Could not reach the Cursor backend at ${this.backendUrl}: ${error.message}`
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
if (error.status >= 200 && error.status < 300) {
|
|
584
|
+
// 2xx without a token in the body.
|
|
585
|
+
throw new CursorBackendError(
|
|
586
|
+
"Cursor API key exchange returned no access token."
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
throw new CursorBackendError(
|
|
590
|
+
error.status === 401
|
|
591
|
+
? "The Cursor API key was rejected. Run `agent-serve login` again or update CURSOR_API_KEY."
|
|
592
|
+
: `Cursor API key exchange failed (${error.status}).`,
|
|
593
|
+
error.status
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
throw error;
|
|
598
597
|
}
|
|
599
|
-
this.token = {
|
|
600
|
-
value: body.data.accessToken,
|
|
601
|
-
expiresAtMs: jwtExpiryMs(body.data.accessToken),
|
|
602
|
-
};
|
|
598
|
+
this.token = { value: session.token, expiresAtMs: session.expiresAtMs };
|
|
603
599
|
this.lastExchangedApiKey = apiKey;
|
|
604
600
|
this.logger("[agent-sdk] cursor account: minted a fresh access token");
|
|
605
|
-
return
|
|
601
|
+
return session.token;
|
|
606
602
|
}
|
|
607
603
|
}
|
|
608
604
|
|
|
@@ -705,8 +701,6 @@ export async function revokeMintedUserApiKey(args: {
|
|
|
705
701
|
// Helpers
|
|
706
702
|
// ============================================================================
|
|
707
703
|
|
|
708
|
-
const accessTokenResponseSchema = z.object({ accessToken: z.string() });
|
|
709
|
-
|
|
710
704
|
function safeJsonParse(text: string): unknown {
|
|
711
705
|
try {
|
|
712
706
|
return JSON.parse(text) as unknown;
|
|
@@ -883,24 +877,6 @@ function retryAfterMsFromResponse(response: Response): number | undefined {
|
|
|
883
877
|
return Math.min(retryAfter * 1000, MAX_GITHUB_TOKEN_RETRY_AFTER_MS);
|
|
884
878
|
}
|
|
885
879
|
|
|
886
|
-
function jwtExpiryMs(token: string): number {
|
|
887
|
-
try {
|
|
888
|
-
const payload = token.split(".")[1];
|
|
889
|
-
if (payload === undefined) {
|
|
890
|
-
throw new Error("not a JWT");
|
|
891
|
-
}
|
|
892
|
-
const claims = JSON.parse(
|
|
893
|
-
Buffer.from(payload, "base64url").toString("utf8")
|
|
894
|
-
) as { exp?: unknown };
|
|
895
|
-
if (typeof claims.exp === "number") {
|
|
896
|
-
return claims.exp * 1000;
|
|
897
|
-
}
|
|
898
|
-
} catch {
|
|
899
|
-
// Opaque token — assume a conservative lifetime.
|
|
900
|
-
}
|
|
901
|
-
return Date.now() + FALLBACK_TOKEN_LIFETIME_MS;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
880
|
function describeError(error: unknown): string {
|
|
905
881
|
return error instanceof Error ? error.message : String(error);
|
|
906
882
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared `CURSOR_API_KEY` → Cursor session-token exchange primitive
|
|
3
|
+
* (`POST /auth/exchange_user_api_key`, ~1h session).
|
|
4
|
+
*
|
|
5
|
+
* Both `CursorBackendClient` (dashboard RPCs) and the Origin channel's
|
|
6
|
+
* session provider (`cursorSessionTokenProvider` in
|
|
7
|
+
* `../../channels/origin/api.ts`) mint through this module, so the URL,
|
|
8
|
+
* request shape, response parsing, JWT-expiry handling, and renewal
|
|
9
|
+
* constants cannot drift between the two. Callers own their caching and
|
|
10
|
+
* retry policies; this module owns exactly one exchange round trip.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const CURSOR_SESSION_EXCHANGE_PATH = "/auth/exchange_user_api_key";
|
|
14
|
+
/** Re-exchange when the cached session is within this window of expiry. */
|
|
15
|
+
export const SESSION_TOKEN_EXPIRY_LEEWAY_MS: number = 5 * 60 * 1000;
|
|
16
|
+
/** Fallback session lifetime when the JWT `exp` claim cannot be parsed. */
|
|
17
|
+
export const SESSION_TOKEN_FALLBACK_LIFETIME_MS: number = 30 * 60 * 1000;
|
|
18
|
+
const EXCHANGE_TIMEOUT_MS = 30_000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Exchange failure. `status` is the HTTP status when the backend answered,
|
|
22
|
+
* and undefined for network/transport errors. `detail` carries the server's
|
|
23
|
+
* `error` field when present so callers can surface the reason verbatim.
|
|
24
|
+
*/
|
|
25
|
+
export class CursorSessionExchangeError extends Error {
|
|
26
|
+
constructor(
|
|
27
|
+
message: string,
|
|
28
|
+
readonly status?: number,
|
|
29
|
+
readonly detail?: string
|
|
30
|
+
) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = "CursorSessionExchangeError";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Epoch-ms expiry from a JWT `exp` claim, or undefined when unparsable. */
|
|
37
|
+
export function jwtExpiryMs(token: string): number | undefined {
|
|
38
|
+
try {
|
|
39
|
+
const payload = token.split(".")[1];
|
|
40
|
+
if (payload === undefined) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const claims = JSON.parse(
|
|
44
|
+
Buffer.from(payload, "base64url").toString("utf8")
|
|
45
|
+
) as { exp?: unknown };
|
|
46
|
+
return typeof claims.exp === "number" ? claims.exp * 1000 : undefined;
|
|
47
|
+
} catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* One exchange round trip. Throws {@link CursorSessionExchangeError} on
|
|
54
|
+
* transport failure, a non-2xx response, or a token-free body. `expiresAtMs`
|
|
55
|
+
* comes from the session JWT's `exp` claim, falling back to
|
|
56
|
+
* {@link SESSION_TOKEN_FALLBACK_LIFETIME_MS} for opaque tokens.
|
|
57
|
+
*/
|
|
58
|
+
export async function exchangeCursorApiKeyForSession(args: {
|
|
59
|
+
/** Full exchange URL (base + {@link CURSOR_SESSION_EXCHANGE_PATH}). */
|
|
60
|
+
exchangeUrl: string;
|
|
61
|
+
apiKey: string;
|
|
62
|
+
fetchImpl?: typeof fetch;
|
|
63
|
+
nowMs?: number;
|
|
64
|
+
}): Promise<{ token: string; expiresAtMs: number }> {
|
|
65
|
+
const fetchImpl = args.fetchImpl ?? fetch;
|
|
66
|
+
let response: Response;
|
|
67
|
+
try {
|
|
68
|
+
response = await fetchImpl(args.exchangeUrl, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: {
|
|
71
|
+
"content-type": "application/json",
|
|
72
|
+
authorization: `Bearer ${args.apiKey}`,
|
|
73
|
+
},
|
|
74
|
+
body: "{}",
|
|
75
|
+
signal: AbortSignal.timeout(EXCHANGE_TIMEOUT_MS),
|
|
76
|
+
});
|
|
77
|
+
} catch (error) {
|
|
78
|
+
throw new CursorSessionExchangeError(
|
|
79
|
+
`Could not reach the Cursor session exchange at ${args.exchangeUrl}: ${
|
|
80
|
+
error instanceof Error ? error.message : String(error)
|
|
81
|
+
}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const body: unknown = await response.json().catch(() => undefined);
|
|
85
|
+
const record =
|
|
86
|
+
typeof body === "object" && body !== null && !Array.isArray(body)
|
|
87
|
+
? (body as Record<string, unknown>)
|
|
88
|
+
: {};
|
|
89
|
+
const detail =
|
|
90
|
+
typeof record.error === "string" && record.error !== ""
|
|
91
|
+
? record.error
|
|
92
|
+
: undefined;
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
throw new CursorSessionExchangeError(
|
|
95
|
+
`Cursor session exchange failed with HTTP ${response.status}${
|
|
96
|
+
detail === undefined ? "" : ` (${detail})`
|
|
97
|
+
}.`,
|
|
98
|
+
response.status,
|
|
99
|
+
detail
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const token = record.accessToken;
|
|
103
|
+
if (typeof token !== "string" || token === "") {
|
|
104
|
+
throw new CursorSessionExchangeError(
|
|
105
|
+
"Cursor session exchange returned no access token.",
|
|
106
|
+
response.status
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const nowMs = args.nowMs ?? Date.now();
|
|
110
|
+
return {
|
|
111
|
+
token,
|
|
112
|
+
expiresAtMs:
|
|
113
|
+
jwtExpiryMs(token) ?? nowMs + SESSION_TOKEN_FALLBACK_LIFETIME_MS,
|
|
114
|
+
};
|
|
115
|
+
}
|