@cursor/july 0.1.95 → 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.
- package/AGENTS.md +3 -4
- package/README.md +3 -2
- package/dist/bin/agent-serve.js +14 -3
- 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/origin/api.d.ts +72 -15
- package/dist/channels/origin/api.d.ts.map +1 -1
- package/dist/channels/origin/api.js +185 -28
- package/dist/channels/origin/binding.d.ts +13 -2
- package/dist/channels/origin/binding.d.ts.map +1 -1
- package/dist/channels/origin/binding.js +41 -25
- 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/index.d.ts +1 -0
- package/dist/channels/origin/index.d.ts.map +1 -1
- package/dist/channels/origin/index.js +1 -0
- package/dist/channels/origin/origin-channel.d.ts.map +1 -1
- package/dist/channels/origin/origin-channel.js +3 -0
- package/dist/channels/origin/reactions.d.ts +56 -0
- package/dist/channels/origin/reactions.d.ts.map +1 -0
- package/dist/channels/origin/reactions.js +67 -0
- package/dist/channels/origin/types.d.ts +11 -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/approvals.d.ts +16 -0
- package/dist/channels/slack/approvals.d.ts.map +1 -1
- package/dist/channels/slack/approvals.js +86 -0
- package/dist/channels/slack/cli-project.d.ts +19 -0
- package/dist/channels/slack/cli-project.d.ts.map +1 -0
- package/dist/channels/slack/cli-project.js +234 -0
- package/dist/channels/slack/defaults.d.ts.map +1 -1
- package/dist/channels/slack/defaults.js +110 -81
- 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/init.d.ts +3 -2
- package/dist/channels/slack/init.d.ts.map +1 -1
- package/dist/channels/slack/init.js +5 -5
- package/dist/channels/slack/interactive.d.ts +2 -1
- package/dist/channels/slack/interactive.d.ts.map +1 -1
- package/dist/channels/slack/interactive.js +15 -4
- 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 +1 -1
- package/dist/channels/slack/manifest.d.ts.map +1 -1
- package/dist/channels/slack/manifest.js +6 -2
- 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/setup.d.ts +1 -1
- package/dist/channels/slack/setup.d.ts.map +1 -1
- package/dist/channels/slack/setup.js +10 -6
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +3 -2
- package/dist/channels/slack/tool-approval-events.d.ts.map +1 -1
- package/dist/channels/slack/tool-approval-events.js +91 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +4 -4
- package/dist/docs/assets/{app.BBj0klBO.js → app.B3rWNYE1.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.BU9nHdnQ.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.CUEXpL78.js → VPLocalSearchBox.C1lhqJJR.js} +1 -1
- package/dist/docs/assets/chunks/{theme.DabmQnia.js → theme.B40_SXuv.js} +2 -2
- package/dist/docs/assets/{deployment.md.D9msOFOW.js → deployment.md.MFPKqsqI.js} +2 -2
- package/dist/docs/assets/{guides_mcp-oauth.md.DPYmBCbV.js → guides_mcp-oauth.md.CN-6YmTJ.js} +4 -4
- package/dist/docs/assets/{guides_mcp-oauth.md.DPYmBCbV.lean.js → guides_mcp-oauth.md.CN-6YmTJ.lean.js} +1 -1
- package/dist/docs/assets/guides_slack.md.VDXQV3ja.js +22 -0
- package/dist/docs/assets/guides_slack.md.VDXQV3ja.lean.js +1 -0
- package/dist/docs/assets/index.md.CVeRUOeZ.js +5 -0
- package/dist/docs/assets/{index.md.BoHaMdeZ.lean.js → index.md.CVeRUOeZ.lean.js} +1 -1
- package/dist/docs/assets/{reference_agent-config.md.Cp_x38Nl.js → reference_agent-config.md.BHeYMgme.js} +1 -1
- package/dist/docs/assets/{reference_cli.md.BsMOxDvh.js → reference_cli.md.RyZf5OTE.js} +10 -6
- package/dist/docs/assets/{reference_cli.md.BsMOxDvh.lean.js → reference_cli.md.RyZf5OTE.lean.js} +1 -1
- package/dist/docs/assets/{reference_connections.md.BH8Oc0D0.js → reference_connections.md.DxldvyIB.js} +4 -3
- package/dist/docs/assets/{reference_http-api.md.D89k1mdm.js → reference_http-api.md.5zOAbV86.js} +1 -1
- package/dist/docs/assets/reference_sessions.md.NxFP_CAC.js +1 -0
- package/dist/docs/assets/{reference_tools.md.BHeXn2id.js → reference_tools.md.B84gw9Ii.js} +1 -1
- package/dist/docs/assets/templates_agents-md.md.CXSMZljE.js +2 -0
- package/dist/docs/assets/templates_agents-md.md.CXSMZljE.lean.js +1 -0
- 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/troubleshooting.md.DCiPBhYs.js +1 -0
- package/dist/docs/building-with-agents.html +4 -4
- package/dist/docs/concepts.html +4 -4
- package/dist/docs/deployment.html +7 -7
- package/dist/docs/deployment.md +5 -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 +8 -8
- package/dist/docs/guides/mcp-oauth.md +26 -8
- package/dist/docs/guides/opentelemetry.html +4 -4
- package/dist/docs/guides/slack.html +17 -33
- package/dist/docs/guides/slack.md +92 -201
- 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 +7 -3
- package/dist/docs/llms-full.txt +398 -653
- package/dist/docs/llms.txt +3 -2
- package/dist/docs/quickstart.html +4 -4
- package/dist/docs/reference/agent-config.html +6 -6
- package/dist/docs/reference/agent-config.md +2 -2
- package/dist/docs/reference/artifacts.html +4 -4
- package/dist/docs/reference/channels.html +4 -4
- package/dist/docs/reference/cli.html +14 -10
- package/dist/docs/reference/cli.md +39 -16
- package/dist/docs/reference/connections.html +9 -8
- package/dist/docs/reference/connections.md +6 -3
- package/dist/docs/reference/hooks.html +4 -4
- package/dist/docs/reference/http-api.html +6 -6
- package/dist/docs/reference/http-api.md +2 -1
- 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 +5 -5
- package/dist/docs/reference/sessions.md +4 -2
- package/dist/docs/reference/skills.html +4 -4
- package/dist/docs/reference/subagents.html +4 -4
- package/dist/docs/reference/tools.html +6 -6
- package/dist/docs/reference/tools.md +9 -2
- package/dist/docs/scaffolding-agents.html +4 -4
- package/dist/docs/storage.html +4 -4
- package/dist/docs/templates/agentic-owners.html +4 -4
- package/dist/docs/templates/agents-md.html +28 -0
- package/dist/docs/templates/agents-md.md +105 -0
- package/dist/docs/templates/code-wiki.html +28 -0
- package/dist/docs/templates/code-wiki.md +91 -0
- package/dist/docs/templates/demo.html +5 -5
- package/dist/docs/templates/pr-autofixer.html +4 -4
- package/dist/docs/templates/security-help.html +4 -4
- package/dist/docs/templates/security-reviewer.html +5 -5
- package/dist/docs/templates/triage.html +4 -4
- package/dist/docs/troubleshooting.html +5 -5
- package/dist/docs/troubleshooting.md +2 -2
- package/dist/evals/run-facts.d.ts.map +1 -1
- package/dist/evals/run-facts.js +27 -0
- package/dist/files-backends/cursor-hosted.d.ts +26 -4
- package/dist/files-backends/cursor-hosted.d.ts.map +1 -1
- package/dist/files-backends/cursor-hosted.js +32 -4
- 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 +30 -10
- package/dist/internal/approval-gate.d.ts +12 -13
- package/dist/internal/approval-gate.d.ts.map +1 -1
- package/dist/internal/approval-gate.js +16 -21
- 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/chat-client.js +10 -0
- package/dist/internal/cli-deploy.d.ts +5 -0
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +57 -18
- package/dist/internal/cli-mcp-oauth.js +8 -18
- package/dist/internal/cli-slack-manual.d.ts +24 -2
- package/dist/internal/cli-slack-manual.d.ts.map +1 -1
- package/dist/internal/cli-slack-manual.js +251 -20
- package/dist/internal/cli-slack.d.ts +9 -2
- package/dist/internal/cli-slack.d.ts.map +1 -1
- package/dist/internal/cli-slack.js +29 -82
- package/dist/internal/conversation-mirror.d.ts +11 -8
- package/dist/internal/conversation-mirror.d.ts.map +1 -1
- package/dist/internal/conversation-mirror.js +116 -55
- package/dist/internal/cursor/account-mcp.d.ts +1 -0
- package/dist/internal/cursor/account-mcp.d.ts.map +1 -1
- package/dist/internal/cursor/account-mcp.js +17 -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 +2 -0
- package/dist/internal/deploy-client.d.ts +17 -0
- package/dist/internal/deploy-client.d.ts.map +1 -1
- package/dist/internal/deploy-client.js +31 -10
- package/dist/internal/env-file.d.ts +19 -0
- package/dist/internal/env-file.d.ts.map +1 -0
- package/dist/internal/env-file.js +90 -0
- 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 +210 -0
- package/dist/internal/framework-storage-selection.d.ts +11 -5
- package/dist/internal/framework-storage-selection.d.ts.map +1 -1
- package/dist/internal/framework-storage-selection.js +38 -11
- package/dist/internal/hosted-delivery-protocol.d.ts +7 -0
- package/dist/internal/hosted-delivery-protocol.d.ts.map +1 -1
- package/dist/internal/hosted-delivery-protocol.js +7 -0
- package/dist/internal/hosted-delivery.d.ts +13 -0
- package/dist/internal/hosted-delivery.d.ts.map +1 -1
- package/dist/internal/hosted-delivery.js +64 -19
- package/dist/internal/hosted-execution-diag.d.ts +48 -0
- package/dist/internal/hosted-execution-diag.d.ts.map +1 -0
- package/dist/internal/hosted-execution-diag.js +150 -0
- package/dist/internal/http-channel.d.ts.map +1 -1
- package/dist/internal/http-channel.js +81 -1
- package/dist/internal/init-project.d.ts.map +1 -1
- package/dist/internal/init-project.js +4 -0
- package/dist/internal/install-cursor-skills.d.ts.map +1 -1
- package/dist/internal/install-cursor-skills.js +1 -3
- package/dist/internal/logs-client.d.ts +5 -0
- package/dist/internal/logs-client.d.ts.map +1 -1
- package/dist/internal/logs-client.js +5 -0
- 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/mcp-endpoint.js +2 -3
- package/dist/internal/mcp-host.d.ts +10 -0
- package/dist/internal/mcp-host.d.ts.map +1 -1
- package/dist/internal/mcp-host.js +57 -8
- package/dist/internal/mcp-oauth-on-demand.d.ts +68 -0
- package/dist/internal/mcp-oauth-on-demand.d.ts.map +1 -0
- package/dist/internal/mcp-oauth-on-demand.js +287 -0
- package/dist/internal/mcp-oauth-persist.d.ts +42 -0
- package/dist/internal/mcp-oauth-persist.d.ts.map +1 -0
- package/dist/internal/mcp-oauth-persist.js +92 -0
- package/dist/internal/mcp-oauth.d.ts +1 -0
- package/dist/internal/mcp-oauth.d.ts.map +1 -1
- package/dist/internal/mcp-oauth.js +10 -2
- package/dist/internal/otel/collector.d.ts.map +1 -1
- package/dist/internal/otel/collector.js +22 -9
- 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/sdk-runner.d.ts +11 -0
- package/dist/internal/sdk-runner.d.ts.map +1 -1
- package/dist/internal/sdk-runner.js +39 -8
- package/dist/internal/server.d.ts +3 -0
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +49 -21
- package/dist/internal/session-engine.d.ts +101 -3
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +577 -46
- package/dist/internal/sessions-client.d.ts +9 -7
- package/dist/internal/sessions-client.d.ts.map +1 -1
- package/dist/internal/sessions-client.js +3 -1
- 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/internal/storage-coordinator.d.ts +7 -0
- package/dist/internal/storage-coordinator.d.ts.map +1 -1
- package/dist/internal/storage-coordinator.js +28 -5
- package/dist/internal/storage-roles.d.ts +5 -0
- package/dist/internal/storage-roles.d.ts.map +1 -1
- package/dist/internal/stream-progress.d.ts.map +1 -1
- package/dist/internal/stream-progress.js +10 -0
- package/dist/internal/tool-policy.d.ts +9 -1
- package/dist/internal/tool-policy.d.ts.map +1 -1
- package/dist/internal/tool-policy.js +13 -0
- package/dist/internal/trajectory.d.ts.map +1 -1
- package/dist/internal/trajectory.js +10 -0
- package/dist/internal/turn-outbox.d.ts +24 -0
- package/dist/internal/turn-outbox.d.ts.map +1 -0
- package/dist/internal/turn-outbox.js +80 -0
- package/dist/playground/assets/{index-CZA1uEWA.js → index-DrkI6y5O.js} +51 -51
- package/dist/playground/assets/index-DzNGwm7q.css +1 -0
- package/dist/playground/index.html +2 -2
- package/dist/storage-backends/cursor-hosted.d.ts +9 -6
- package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
- package/dist/storage-backends/cursor-hosted.js +16 -3
- package/dist/storage.d.ts +8 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/types.d.ts +65 -8
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +7 -3
- package/docs/deployment.md +5 -4
- package/docs/guides/mcp-oauth.md +26 -8
- package/docs/guides/slack.md +93 -202
- package/docs/reference/agent-config.md +2 -2
- package/docs/reference/cli.md +39 -16
- package/docs/reference/connections.md +6 -3
- package/docs/reference/http-api.md +2 -1
- package/docs/reference/sessions.md +4 -2
- package/docs/reference/tools.md +9 -2
- package/docs/templates/agents-md.md +110 -0
- package/docs/templates/code-wiki.md +96 -0
- package/docs/troubleshooting.md +2 -2
- package/package.json +2 -2
- package/skills/ab/SKILL.md +13 -97
- package/skills/create-agent/SKILL.md +124 -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 +51 -107
- package/skills/otel/SKILL.md +22 -68
- package/skills/setup-slack/SKILL.md +91 -212
- package/src/bin/agent-serve.ts +13 -1
- package/src/channels/deployments/types.ts +1 -0
- package/src/channels/origin/api.ts +264 -33
- package/src/channels/origin/binding.ts +57 -28
- package/src/channels/origin/checks.ts +6 -6
- package/src/channels/origin/cursor-account.ts +5 -3
- package/src/channels/origin/index.ts +1 -0
- package/src/channels/origin/origin-channel.ts +4 -0
- package/src/channels/origin/reactions.ts +82 -0
- package/src/channels/origin/types.ts +11 -0
- package/src/channels/slack/api.ts +41 -5
- package/src/channels/slack/approvals.ts +105 -0
- package/src/channels/slack/cli-project.ts +267 -0
- package/src/channels/slack/defaults.ts +122 -93
- package/src/channels/slack/dispatch.ts +56 -22
- package/src/channels/slack/inbound.ts +43 -0
- package/src/channels/slack/init.ts +10 -5
- package/src/channels/slack/interactive.ts +35 -9
- package/src/channels/slack/live-delivery.ts +235 -114
- package/src/channels/slack/manifest.ts +6 -2
- package/src/channels/slack/nudge-carrier.ts +48 -1
- package/src/channels/slack/setup.ts +10 -6
- package/src/channels/slack/slack-channel.ts +3 -0
- package/src/channels/slack/tool-approval-events.ts +93 -0
- package/src/evals/run-facts.ts +32 -0
- package/src/files-backends/cursor-hosted.ts +40 -7
- package/src/internal/advertise-tools.ts +49 -12
- package/src/internal/approval-gate.ts +20 -29
- package/src/internal/bounded.ts +28 -0
- package/src/internal/chat-client.ts +19 -0
- package/src/internal/cli-deploy.ts +91 -9
- package/src/internal/cli-mcp-oauth.ts +7 -21
- package/src/internal/cli-slack-manual.ts +350 -18
- package/src/internal/cli-slack.ts +40 -84
- package/src/internal/conversation-mirror.ts +145 -69
- package/src/internal/cursor/account-mcp.ts +21 -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 +2 -0
- package/src/internal/deploy-client.ts +43 -0
- package/src/internal/env-file.ts +84 -0
- package/src/internal/framework-file-storage.ts +228 -0
- package/src/internal/framework-storage-selection.ts +51 -11
- package/src/internal/hosted-delivery-protocol.ts +7 -0
- package/src/internal/hosted-delivery.ts +107 -24
- package/src/internal/hosted-execution-diag.ts +207 -0
- package/src/internal/http-channel.ts +99 -0
- package/src/internal/init-project.ts +4 -0
- package/src/internal/install-cursor-skills.ts +1 -4
- package/src/internal/logs-client.ts +5 -0
- package/src/internal/managed-delivery.ts +125 -0
- package/src/internal/mcp-endpoint.ts +13 -4
- package/src/internal/mcp-host.ts +77 -11
- package/src/internal/mcp-oauth-on-demand.ts +352 -0
- package/src/internal/mcp-oauth-persist.ts +127 -0
- package/src/internal/mcp-oauth.ts +17 -1
- package/src/internal/otel/collector.ts +30 -0
- package/src/internal/runtime-dispatch-runner.ts +10 -0
- package/src/internal/sdk-runner.ts +56 -8
- package/src/internal/server.ts +61 -9
- package/src/internal/session-engine.ts +723 -60
- package/src/internal/sessions-client.ts +17 -8
- package/src/internal/steering.ts +17 -0
- package/src/internal/storage-coordinator.ts +38 -8
- package/src/internal/storage-roles.ts +7 -0
- package/src/internal/stream-progress.ts +19 -0
- package/src/internal/tool-policy.ts +18 -0
- package/src/internal/trajectory.ts +10 -0
- package/src/internal/turn-outbox.ts +85 -0
- package/src/storage-backends/cursor-hosted.ts +22 -8
- package/src/storage.ts +12 -0
- package/src/types.ts +70 -9
- package/templates/agents-md/README.md +39 -0
- package/templates/agents-md/agent/agent.ts +8 -0
- package/templates/agents-md/agent/channels/github.ts +10 -0
- package/templates/agents-md/agent/channels/queue.ts +28 -0
- package/templates/agents-md/agent/channels/slack.ts +9 -0
- package/templates/agents-md/agent/instructions.md +39 -0
- package/templates/agents-md/agent/lib/collect.ts +439 -0
- package/templates/agents-md/agent/lib/config.ts +8 -0
- package/templates/agents-md/agent/lib/paths.ts +51 -0
- package/templates/agents-md/agent/schedules/weekly.ts +17 -0
- package/templates/agents-md/agent/tools/apply_agents.ts +226 -0
- package/templates/agents-md/evals/agents.eval.ts +135 -0
- package/templates/agents-md/evals/evals.config.ts +5 -0
- package/templates/agents-md/init.json +38 -0
- package/templates/agents-md/package.json +18 -0
- package/templates/agents-md/tsconfig.json +12 -0
- 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/dist/docs/assets/chunks/@localSearchIndexroot.oqPawjiD.js +0 -1
- package/dist/docs/assets/guides_slack.md.C32HsdKk.js +0 -38
- package/dist/docs/assets/guides_slack.md.C32HsdKk.lean.js +0 -1
- package/dist/docs/assets/index.md.BoHaMdeZ.js +0 -5
- package/dist/docs/assets/reference_sessions.md.D0mIh4KK.js +0 -1
- package/dist/docs/assets/troubleshooting.md.Ctv3T8C2.js +0 -1
- package/dist/docs/design/agsh.md +0 -406
- 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-CF7hUDnQ.css +0 -1
- package/docs/design/agsh.md +0 -406
- package/src/internal/deployment-scm-event-stream.ts +0 -593
- /package/dist/docs/assets/{deployment.md.D9msOFOW.lean.js → deployment.md.MFPKqsqI.lean.js} +0 -0
- /package/dist/docs/assets/{reference_agent-config.md.Cp_x38Nl.lean.js → reference_agent-config.md.BHeYMgme.lean.js} +0 -0
- /package/dist/docs/assets/{reference_connections.md.BH8Oc0D0.lean.js → reference_connections.md.DxldvyIB.lean.js} +0 -0
- /package/dist/docs/assets/{reference_http-api.md.D89k1mdm.lean.js → reference_http-api.md.5zOAbV86.lean.js} +0 -0
- /package/dist/docs/assets/{reference_sessions.md.D0mIh4KK.lean.js → reference_sessions.md.NxFP_CAC.lean.js} +0 -0
- /package/dist/docs/assets/{reference_tools.md.BHeXn2id.lean.js → reference_tools.md.B84gw9Ii.lean.js} +0 -0
- /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 {
|
|
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,
|
|
@@ -136,13 +144,14 @@ import {
|
|
|
136
144
|
SCHEDULE_CHANNEL_PREFIX,
|
|
137
145
|
} from "./prompt-context.js";
|
|
138
146
|
import { assertCloudCanHonorAccountServersFilters } from "./resolved-connections.js";
|
|
139
|
-
import type { AgentRunner } from "./sdk-runner.js";
|
|
147
|
+
import type { AgentRunner, SteerAckOutcome } from "./sdk-runner.js";
|
|
140
148
|
import { sessionExternalLinks } from "./session-links.js";
|
|
141
149
|
import { SessionStore } from "./session-store.js";
|
|
142
150
|
import { syncSkillsToStore } from "./skills-sync.js";
|
|
151
|
+
import { steeringEnabled } from "./steering.js";
|
|
143
152
|
import { StorageCoordinator } from "./storage-coordinator.js";
|
|
144
153
|
import type { FrameworkSessionStore } from "./storage-roles.js";
|
|
145
|
-
import { decideToolCall,
|
|
154
|
+
import { decideToolCall, directCallEffect } from "./tool-policy.js";
|
|
146
155
|
import { normalizeToolResult, toolCallErrorMessage } from "./tool-result.js";
|
|
147
156
|
import {
|
|
148
157
|
type EstimateTurnCostFn,
|
|
@@ -151,6 +160,7 @@ import {
|
|
|
151
160
|
} from "./turn-cost.js";
|
|
152
161
|
import { TurnGovernor } from "./turn-governor.js";
|
|
153
162
|
import { TurnLeaseStore } from "./turn-lease-store.js";
|
|
163
|
+
import { TurnOutbox } from "./turn-outbox.js";
|
|
154
164
|
import {
|
|
155
165
|
buildAgentsMdContent,
|
|
156
166
|
buildCloudMemorySection,
|
|
@@ -257,6 +267,22 @@ export class ApprovalInterruptedError extends Error {
|
|
|
257
267
|
}
|
|
258
268
|
}
|
|
259
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
|
+
|
|
260
286
|
export interface SessionEngineOptions {
|
|
261
287
|
project: AgentProject;
|
|
262
288
|
stateRoot: string;
|
|
@@ -311,6 +337,16 @@ interface ActiveToolContext {
|
|
|
311
337
|
emit: (payload: SessionEventPayload) => void;
|
|
312
338
|
}
|
|
313
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
|
+
|
|
314
350
|
export interface EngineSendOptions extends SendMessageOptions {
|
|
315
351
|
mode?: "chat" | "task";
|
|
316
352
|
/**
|
|
@@ -345,6 +381,8 @@ export class SessionEngine {
|
|
|
345
381
|
readonly askDedupe: SlackAskDedupeStore;
|
|
346
382
|
/** Cross-process turn ownership under `<stateRoot>/turn-leases`. */
|
|
347
383
|
readonly turnLeases: TurnLeaseStore;
|
|
384
|
+
/** Messages for turns another server owns, under `<stateRoot>/outbox`. */
|
|
385
|
+
readonly outbox: TurnOutbox<AdmitTurnOptions>;
|
|
348
386
|
/**
|
|
349
387
|
* Runtime for `agent/storage.ts` (`defineStorage`), when
|
|
350
388
|
* authored. All durable state changes funnel through it (session
|
|
@@ -369,6 +407,13 @@ export class SessionEngine {
|
|
|
369
407
|
/** Sessions with a turn being admitted (before the run promise exists). */
|
|
370
408
|
private readonly pendingTurns = new Set<string>();
|
|
371
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>();
|
|
372
417
|
/** Context used by the cloud turn's HTTP MCP callbacks. */
|
|
373
418
|
private readonly activeToolContexts = new Map<string, ActiveToolContext>();
|
|
374
419
|
/**
|
|
@@ -409,6 +454,11 @@ export class SessionEngine {
|
|
|
409
454
|
private readonly mcpHost: McpHost;
|
|
410
455
|
private readonly platforms: HostPlatforms;
|
|
411
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;
|
|
412
462
|
private readonly abCollector: ABCollector;
|
|
413
463
|
private readonly otel: OtelCollector | undefined;
|
|
414
464
|
private readonly otelHost: HostOtelSessionApi;
|
|
@@ -457,6 +507,7 @@ export class SessionEngine {
|
|
|
457
507
|
this.turnLeases = new TurnLeaseStore(
|
|
458
508
|
join(options.stateRoot, "turn-leases")
|
|
459
509
|
);
|
|
510
|
+
this.outbox = new TurnOutbox(join(options.stateRoot, "outbox"));
|
|
460
511
|
this.logs = new EventLogStore(
|
|
461
512
|
(sessionId) => this.sessions.eventFilePath(sessionId),
|
|
462
513
|
(sessionId, error) =>
|
|
@@ -564,10 +615,25 @@ export class SessionEngine {
|
|
|
564
615
|
this.cloudServerToolsConnection = connection;
|
|
565
616
|
}
|
|
566
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
|
+
|
|
567
633
|
/** Shared host services (MCP / GitHub / Slack / reminders / evals / kv / files / otel / Slack nudges). */
|
|
568
634
|
get host(): HostContext {
|
|
569
635
|
return {
|
|
570
|
-
mcp: this.mcpHost,
|
|
636
|
+
mcp: this.wrapMcp(this.mcpHost),
|
|
571
637
|
github: this.platforms.github,
|
|
572
638
|
slack: this.platforms.slack,
|
|
573
639
|
reminders: this.reminderApi,
|
|
@@ -698,6 +764,9 @@ export class SessionEngine {
|
|
|
698
764
|
) {
|
|
699
765
|
return;
|
|
700
766
|
}
|
|
767
|
+
if (steeringEnabled()) {
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
701
770
|
try {
|
|
702
771
|
await materializeSlackNudgeCarrier({
|
|
703
772
|
harnessCwd,
|
|
@@ -959,7 +1028,7 @@ export class SessionEngine {
|
|
|
959
1028
|
channelId,
|
|
960
1029
|
mode: sendOptions.mode ?? "chat",
|
|
961
1030
|
purpose,
|
|
962
|
-
|
|
1031
|
+
dryRun: sendOptions.dryRun === true ? true : undefined,
|
|
963
1032
|
continuationKey: sendOptions.continuationToken ?? newContinuationKey(),
|
|
964
1033
|
auth,
|
|
965
1034
|
title: sendOptions.title,
|
|
@@ -977,7 +1046,10 @@ export class SessionEngine {
|
|
|
977
1046
|
this.storage?.sessionRecord(record);
|
|
978
1047
|
await this.appendEvent(record.sessionId, {
|
|
979
1048
|
type: "session.started",
|
|
980
|
-
data: {
|
|
1049
|
+
data: {
|
|
1050
|
+
channelId,
|
|
1051
|
+
dryRun: record.dryRun === true ? true : undefined,
|
|
1052
|
+
},
|
|
981
1053
|
});
|
|
982
1054
|
record = await this.enrollABs(record);
|
|
983
1055
|
const since = (await this.logs.get(record.sessionId)).count;
|
|
@@ -1227,7 +1299,14 @@ export class SessionEngine {
|
|
|
1227
1299
|
const prior = this.admissionChains.get(sessionId) ?? Promise.resolve();
|
|
1228
1300
|
const admitted = prior
|
|
1229
1301
|
.catch(() => {})
|
|
1230
|
-
.then(() =>
|
|
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
|
+
});
|
|
1231
1310
|
this.admissionChains.set(
|
|
1232
1311
|
sessionId,
|
|
1233
1312
|
admitted.catch(() => {})
|
|
@@ -1235,6 +1314,20 @@ export class SessionEngine {
|
|
|
1235
1314
|
return admitted;
|
|
1236
1315
|
}
|
|
1237
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
|
+
|
|
1238
1331
|
private enqueueCoalesced(sessionId: string, item: CoalescedQueuedTurn): void {
|
|
1239
1332
|
const queue = this.coalesceQueues.get(sessionId) ?? [];
|
|
1240
1333
|
if (
|
|
@@ -1359,18 +1452,52 @@ export class SessionEngine {
|
|
|
1359
1452
|
});
|
|
1360
1453
|
}
|
|
1361
1454
|
|
|
1455
|
+
private async steerIntoRunningTurn(
|
|
1456
|
+
sessionId: string,
|
|
1457
|
+
message: string
|
|
1458
|
+
): Promise<SteerAckOutcome> {
|
|
1459
|
+
if (this.runner.steer === undefined) {
|
|
1460
|
+
return "revert_to_followup";
|
|
1461
|
+
}
|
|
1462
|
+
try {
|
|
1463
|
+
return await this.runner.steer({ sessionId, text: message });
|
|
1464
|
+
} catch (error) {
|
|
1465
|
+
this.logger(
|
|
1466
|
+
`[session] steer failed for ${sessionId}: ${describeError(error)}`
|
|
1467
|
+
);
|
|
1468
|
+
return "revert_to_followup";
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1362
1472
|
private async admitTurn(
|
|
1363
1473
|
sessionId: string,
|
|
1364
1474
|
message: string,
|
|
1365
1475
|
options: AdmitTurnOptions = {}
|
|
1366
1476
|
): Promise<boolean> {
|
|
1367
1477
|
const admission = options.admission ?? "preempt";
|
|
1478
|
+
if (
|
|
1479
|
+
admission === "coalesce" &&
|
|
1480
|
+
steeringEnabled() &&
|
|
1481
|
+
this.isTurnRunning(sessionId)
|
|
1482
|
+
) {
|
|
1483
|
+
const toolCtx = this.activeToolContexts.get(sessionId);
|
|
1484
|
+
const steerOutcome = await this.steerIntoRunningTurn(sessionId, message);
|
|
1485
|
+
if (steerOutcome === "complete_delivered") {
|
|
1486
|
+
this.logger(`[session] steered into running turn ${sessionId}`);
|
|
1487
|
+
if (toolCtx !== undefined) {
|
|
1488
|
+
toolCtx.emit({ type: "message.received", data: { text: message } });
|
|
1489
|
+
toolCtx.emit({ type: "nudge.delivered", data: {} });
|
|
1490
|
+
}
|
|
1491
|
+
return true;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
// Re-checked after the steer: a turn that ended during it falls through
|
|
1495
|
+
// and this message starts the next turn itself. Enqueueing here instead
|
|
1496
|
+
// would orphan it — the completion drain already ran.
|
|
1368
1497
|
if (admission === "coalesce" && this.isTurnRunning(sessionId)) {
|
|
1369
1498
|
this.enqueueCoalesced(sessionId, {
|
|
1370
1499
|
message,
|
|
1371
|
-
|
|
1372
|
-
? {}
|
|
1373
|
-
: { sourceTs: options.sourceTs }),
|
|
1500
|
+
sourceTs: options.sourceTs,
|
|
1374
1501
|
});
|
|
1375
1502
|
this.logger(`[session] coalesce enqueue ${sessionId}`);
|
|
1376
1503
|
return true;
|
|
@@ -1438,6 +1565,8 @@ export class SessionEngine {
|
|
|
1438
1565
|
let lifecycle!: Promise<TurnOutcome>;
|
|
1439
1566
|
lifecycle = (async (): Promise<TurnOutcome> => {
|
|
1440
1567
|
let outcome: TurnOutcome;
|
|
1568
|
+
const poll = setInterval(() => this.drainOutbox(sessionId), 1_000);
|
|
1569
|
+
poll.unref();
|
|
1441
1570
|
try {
|
|
1442
1571
|
outcome = await this.executeTurn(
|
|
1443
1572
|
record,
|
|
@@ -1451,6 +1580,8 @@ export class SessionEngine {
|
|
|
1451
1580
|
status: "error",
|
|
1452
1581
|
errorMessage: describeError(error),
|
|
1453
1582
|
};
|
|
1583
|
+
} finally {
|
|
1584
|
+
clearInterval(poll);
|
|
1454
1585
|
}
|
|
1455
1586
|
this.lastOutcomes.set(sessionId, outcome);
|
|
1456
1587
|
const wasCurrent = this.activeTurns.get(sessionId) === lifecycle;
|
|
@@ -1480,6 +1611,10 @@ export class SessionEngine {
|
|
|
1480
1611
|
// The drain may have nested a successor turn, which took its own
|
|
1481
1612
|
// reference; the lease outlives this release until that one ends too.
|
|
1482
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
|
+
}
|
|
1483
1618
|
return outcome;
|
|
1484
1619
|
})();
|
|
1485
1620
|
this.activeTurns.set(sessionId, lifecycle);
|
|
@@ -1524,6 +1659,7 @@ export class SessionEngine {
|
|
|
1524
1659
|
this.coalesceDrains.has(sessionId) ||
|
|
1525
1660
|
(this.coalesceQueues.get(sessionId)?.length ?? 0) > 0;
|
|
1526
1661
|
this.clearCoalesced(sessionId);
|
|
1662
|
+
await this.outbox.clear(sessionId);
|
|
1527
1663
|
const record = await this.sessions.get(sessionId);
|
|
1528
1664
|
const continuationKey = record?.continuationKey;
|
|
1529
1665
|
if (continuationKey !== undefined && continuationKey !== null) {
|
|
@@ -1757,6 +1893,7 @@ export class SessionEngine {
|
|
|
1757
1893
|
seed: isFirstTurn,
|
|
1758
1894
|
stateRoot: this.stateRoot,
|
|
1759
1895
|
});
|
|
1896
|
+
this.materializedWorkspaces.add(sessionId);
|
|
1760
1897
|
// Must precede the carrier: the harness resolves rules, skills,
|
|
1761
1898
|
// `mcp.json` and hooks from its git root, so a workspace nested in an
|
|
1762
1899
|
// outer repo would load that repo's hooks instead of the ones written
|
|
@@ -1841,11 +1978,14 @@ export class SessionEngine {
|
|
|
1841
1978
|
}
|
|
1842
1979
|
} catch (error) {
|
|
1843
1980
|
this.approvals.cancelSession(sessionId, "deny");
|
|
1981
|
+
this.auths.cancelSession(sessionId, "deny");
|
|
1844
1982
|
outcome = { status: "error", errorMessage: describeError(error) };
|
|
1845
1983
|
} finally {
|
|
1846
1984
|
permit.release();
|
|
1847
1985
|
this.approvals.cancelSession(sessionId, "deny");
|
|
1986
|
+
this.auths.cancelSession(sessionId, "deny");
|
|
1848
1987
|
await this.clearPendingApprovals(sessionId);
|
|
1988
|
+
await this.clearPendingAuths(sessionId);
|
|
1849
1989
|
// Anything still queued belongs to the next turn's boundary drain, not
|
|
1850
1990
|
// to this turn's edge detection.
|
|
1851
1991
|
this.deliveredNudges.delete(sessionId);
|
|
@@ -2480,11 +2620,21 @@ export class SessionEngine {
|
|
|
2480
2620
|
// named passthroughs resolved fresh on every turn — reminder fires and
|
|
2481
2621
|
// post-restart follow-ups included — and a listing failure throws,
|
|
2482
2622
|
// failing the turn closed.
|
|
2483
|
-
for (const tool of await
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
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
|
+
)) {
|
|
2488
2638
|
register(tool);
|
|
2489
2639
|
}
|
|
2490
2640
|
return tools;
|
|
@@ -2634,7 +2784,16 @@ export class SessionEngine {
|
|
|
2634
2784
|
}
|
|
2635
2785
|
const started = Date.now();
|
|
2636
2786
|
this.logger(`[session] tool ${args.tool.name} start`);
|
|
2637
|
-
const result = await
|
|
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
|
+
);
|
|
2638
2797
|
this.logger(
|
|
2639
2798
|
`[session] tool ${args.tool.name} done (${Date.now() - started}ms)`
|
|
2640
2799
|
);
|
|
@@ -2670,10 +2829,10 @@ export class SessionEngine {
|
|
|
2670
2829
|
args: args.args,
|
|
2671
2830
|
requestedAt,
|
|
2672
2831
|
};
|
|
2673
|
-
const resolutionPromise = this.approvals.wait(
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2832
|
+
const resolutionPromise = this.approvals.wait(
|
|
2833
|
+
args.sessionId,
|
|
2834
|
+
pendingRecord
|
|
2835
|
+
);
|
|
2677
2836
|
await this.updateRecord(args.sessionId, (current) => ({
|
|
2678
2837
|
...current,
|
|
2679
2838
|
pendingApprovals: [
|
|
@@ -2793,6 +2952,318 @@ export class SessionEngine {
|
|
|
2793
2952
|
}));
|
|
2794
2953
|
}
|
|
2795
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
|
+
|
|
2796
3267
|
// ==========================================================================
|
|
2797
3268
|
// Direct tool calls (deterministic invocation, no model turn)
|
|
2798
3269
|
// ==========================================================================
|
|
@@ -2808,8 +3279,10 @@ export class SessionEngine {
|
|
|
2808
3279
|
* and materialized workspace) and the call is recorded on the session's
|
|
2809
3280
|
* event stream as `actions.requested` / `action.result` under a
|
|
2810
3281
|
* per-call `turnId` (the call id), so it shows up in trajectories.
|
|
2811
|
-
*
|
|
2812
|
-
*
|
|
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
|
|
2813
3286
|
* workspace under `<stateRoot>/tool-calls/<callId>` (materialized with
|
|
2814
3287
|
* seed files, removed after the call returns).
|
|
2815
3288
|
*
|
|
@@ -2835,6 +3308,12 @@ export class SessionEngine {
|
|
|
2835
3308
|
session: SessionInfo;
|
|
2836
3309
|
workspaceDir: string;
|
|
2837
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;
|
|
2838
3317
|
}> {
|
|
2839
3318
|
if (
|
|
2840
3319
|
options.sessionId !== undefined &&
|
|
@@ -2860,9 +3339,9 @@ export class SessionEngine {
|
|
|
2860
3339
|
);
|
|
2861
3340
|
}
|
|
2862
3341
|
// A token that maps to a live (or restorable) session takes the
|
|
2863
|
-
// session path: same owner check, same
|
|
2864
|
-
//
|
|
2865
|
-
//
|
|
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.
|
|
2866
3345
|
record =
|
|
2867
3346
|
(await this.sessions.findByContinuation(
|
|
2868
3347
|
continuation.channelId,
|
|
@@ -2877,19 +3356,14 @@ export class SessionEngine {
|
|
|
2877
3356
|
}
|
|
2878
3357
|
}
|
|
2879
3358
|
if (record !== undefined) {
|
|
2880
|
-
// Direct calls respect the same serialization as follow-up sends: a
|
|
2881
|
-
// running model turn owns the session workspace and event stream.
|
|
2882
|
-
if (
|
|
2883
|
-
this.activeTurns.has(record.sessionId) ||
|
|
2884
|
-
this.pendingTurns.has(record.sessionId)
|
|
2885
|
-
) {
|
|
2886
|
-
throw new SessionBusyError(record.sessionId);
|
|
2887
|
-
}
|
|
2888
3359
|
return {
|
|
2889
3360
|
record,
|
|
2890
3361
|
session: this.sessionInfo(record),
|
|
2891
3362
|
workspaceDir: this.harnessCwd(record),
|
|
2892
3363
|
scratchDir: undefined,
|
|
3364
|
+
busy:
|
|
3365
|
+
this.activeTurns.has(record.sessionId) ||
|
|
3366
|
+
this.pendingTurns.has(record.sessionId),
|
|
2893
3367
|
};
|
|
2894
3368
|
}
|
|
2895
3369
|
// No session behind the token (or no token at all): a synthetic
|
|
@@ -2909,6 +3383,7 @@ export class SessionEngine {
|
|
|
2909
3383
|
},
|
|
2910
3384
|
workspaceDir: scratchDir,
|
|
2911
3385
|
scratchDir,
|
|
3386
|
+
busy: false,
|
|
2912
3387
|
};
|
|
2913
3388
|
}
|
|
2914
3389
|
|
|
@@ -2951,6 +3426,43 @@ export class SessionEngine {
|
|
|
2951
3426
|
};
|
|
2952
3427
|
}
|
|
2953
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
|
+
|
|
2954
3466
|
async callTool(
|
|
2955
3467
|
toolName: string,
|
|
2956
3468
|
input: unknown = {},
|
|
@@ -2977,7 +3489,7 @@ export class SessionEngine {
|
|
|
2977
3489
|
const advertised = await resolveAdvertisedTools({
|
|
2978
3490
|
agent: this.project.agent,
|
|
2979
3491
|
session,
|
|
2980
|
-
hostMcp: this.
|
|
3492
|
+
hostMcp: this.advertiseMcp(),
|
|
2981
3493
|
});
|
|
2982
3494
|
tool = advertised.find((t) => t.name === toolName);
|
|
2983
3495
|
}
|
|
@@ -2997,6 +3509,19 @@ export class SessionEngine {
|
|
|
2997
3509
|
if (!validation.ok) {
|
|
2998
3510
|
throw new ToolInputError({ toolName, detail: validation.message });
|
|
2999
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
|
+
}
|
|
3000
3525
|
let workspaceUnavailable = false;
|
|
3001
3526
|
try {
|
|
3002
3527
|
await mkdir(workspaceDir, { recursive: true });
|
|
@@ -3015,35 +3540,59 @@ export class SessionEngine {
|
|
|
3015
3540
|
// to write) authored tool may rely on ctx.workspaceDir being the
|
|
3016
3541
|
// real session workspace, so silently relocating it is not safe —
|
|
3017
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.
|
|
3018
3547
|
const relocatable =
|
|
3019
3548
|
tool.advertised === true ||
|
|
3020
|
-
|
|
3549
|
+
directCallEffect(tool, validation.value) === "read";
|
|
3021
3550
|
if (!relocatable) {
|
|
3022
3551
|
throw new WorkspaceUnavailableError(toolName, error);
|
|
3023
3552
|
}
|
|
3024
|
-
this.
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
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
|
+
}
|
|
3033
3568
|
}
|
|
3034
3569
|
// Match what a model turn would see: session workspaces get the same
|
|
3035
3570
|
// scaffold as executeTurn (seeds only before the first turn); scratch
|
|
3036
|
-
// workspaces are materialized fresh.
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
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
|
+
}
|
|
3047
3596
|
|
|
3048
3597
|
// A per-call turnId keeps these events visible in trajectories (which
|
|
3049
3598
|
// drop turn-less action events) without attributing them to a model turn.
|
|
@@ -3101,8 +3650,8 @@ export class SessionEngine {
|
|
|
3101
3650
|
result = normalizeToolResult(decision.result);
|
|
3102
3651
|
isError = false;
|
|
3103
3652
|
} else {
|
|
3104
|
-
|
|
3105
|
-
|
|
3653
|
+
const run = () =>
|
|
3654
|
+
execute(validation.value as never, {
|
|
3106
3655
|
toolCallId: callId,
|
|
3107
3656
|
session,
|
|
3108
3657
|
workspaceDir,
|
|
@@ -3127,7 +3676,20 @@ export class SessionEngine {
|
|
|
3127
3676
|
getSession: (channelId, sessionId) =>
|
|
3128
3677
|
this.getChannelSession(channelId, sessionId),
|
|
3129
3678
|
emit,
|
|
3130
|
-
})
|
|
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()
|
|
3131
3693
|
);
|
|
3132
3694
|
isError = isErrorEnvelope(result);
|
|
3133
3695
|
}
|
|
@@ -3394,6 +3956,28 @@ export class SessionEngine {
|
|
|
3394
3956
|
// Session handles
|
|
3395
3957
|
// ==========================================================================
|
|
3396
3958
|
|
|
3959
|
+
/**
|
|
3960
|
+
* Wait until everything this session has already emitted is durably
|
|
3961
|
+
* mirrored: its dispatch chain first (the chain's tail runs the turn-end
|
|
3962
|
+
* storage flush, so the record carries the boundary event's handler
|
|
3963
|
+
* mutations), then the storage delivery queue. Hosted v2 awaits this
|
|
3964
|
+
* before the execution reply — the pod can be parked the moment the
|
|
3965
|
+
* reply lands, and an unflushed session record would strand a
|
|
3966
|
+
* conversation whose blobs persisted but whose record (which selects the
|
|
3967
|
+
* conversation to hydrate) never reached the sink. No-op without a
|
|
3968
|
+
* storage mirror.
|
|
3969
|
+
*/
|
|
3970
|
+
async drainSessionDurability(sessionId: string): Promise<void> {
|
|
3971
|
+
if (this.storage === undefined) {
|
|
3972
|
+
return;
|
|
3973
|
+
}
|
|
3974
|
+
const chain = this.dispatchChains.get(sessionId);
|
|
3975
|
+
if (chain !== undefined) {
|
|
3976
|
+
await chain.catch(() => {});
|
|
3977
|
+
}
|
|
3978
|
+
await this.storage.whenIdle();
|
|
3979
|
+
}
|
|
3980
|
+
|
|
3397
3981
|
async waitForCompletion(sessionId: string): Promise<TurnOutcome> {
|
|
3398
3982
|
// Drain admission first so pendingTurns → activeTurns is visible before we
|
|
3399
3983
|
// decide the session is idle (reminders use this for queueBehindTurn).
|
|
@@ -3525,6 +4109,75 @@ export class SessionEngine {
|
|
|
3525
4109
|
};
|
|
3526
4110
|
}
|
|
3527
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
|
+
|
|
3528
4181
|
/**
|
|
3529
4182
|
* Durable sink rows plus local records (local wins on the same id when
|
|
3530
4183
|
* newer — covers debounce windows that have not flushed yet). Newest
|
|
@@ -3578,6 +4231,7 @@ export class SessionEngine {
|
|
|
3578
4231
|
: undefined,
|
|
3579
4232
|
createdAt: record.createdAt,
|
|
3580
4233
|
updatedAt: record.updatedAt,
|
|
4234
|
+
dryRun: record.dryRun === true ? true : undefined,
|
|
3581
4235
|
continuationToken:
|
|
3582
4236
|
record.httpContinuationKey !== undefined
|
|
3583
4237
|
? formatContinuationToken(
|
|
@@ -3907,7 +4561,7 @@ export class SessionEngine {
|
|
|
3907
4561
|
channelId: record.channelId,
|
|
3908
4562
|
mode: record.mode,
|
|
3909
4563
|
purpose: record.purpose === "eval" ? "eval" : "live",
|
|
3910
|
-
|
|
4564
|
+
dryRun: record.dryRun === true ? true : undefined,
|
|
3911
4565
|
auth: record.auth,
|
|
3912
4566
|
continuationKey: record.continuationKey,
|
|
3913
4567
|
...(record.abs === undefined ? {} : { abs: record.abs }),
|
|
@@ -4280,6 +4934,8 @@ export class SessionEngine {
|
|
|
4280
4934
|
async close(): Promise<void> {
|
|
4281
4935
|
this.stopMemoryTelemetry?.();
|
|
4282
4936
|
this.approvals.cancelAll("deny");
|
|
4937
|
+
this.auths.cancelAll("deny");
|
|
4938
|
+
await this.oauthHub.close();
|
|
4283
4939
|
await Promise.allSettled([...this.activeTurns.values()]);
|
|
4284
4940
|
await Promise.allSettled([...this.backgroundWork]);
|
|
4285
4941
|
await Promise.allSettled([...this.dispatchChains.values()]);
|
|
@@ -4297,6 +4953,13 @@ export class SessionEngine {
|
|
|
4297
4953
|
/** Parallel event-stream loads during the startup bulk restore. */
|
|
4298
4954
|
const RESTORE_FETCH_CONCURRENCY = 8;
|
|
4299
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
|
+
|
|
4300
4963
|
/** NDJSON payload for a restored event stream; undefined when empty. */
|
|
4301
4964
|
function mergePendingImages(
|
|
4302
4965
|
current: Array<{ data: string; mimeType: string }> | undefined,
|