@cursor/july 0.1.96 → 0.1.99
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 +1 -1
- package/README.md +3 -2
- package/dist/bin/agent-serve.js +13 -3
- package/dist/channels/github/github-channel.d.ts.map +1 -1
- package/dist/channels/github/github-channel.js +16 -0
- package/dist/channels/origin/api.d.ts +15 -4
- package/dist/channels/origin/api.d.ts.map +1 -1
- package/dist/channels/origin/api.js +19 -3
- package/dist/channels/origin/binding.d.ts +11 -0
- package/dist/channels/origin/binding.d.ts.map +1 -1
- package/dist/channels/origin/binding.js +41 -25
- 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 +2 -0
- package/dist/channels/origin/origin-channel.d.ts.map +1 -1
- package/dist/channels/origin/origin-channel.js +40 -4
- 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/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 +142 -63
- package/dist/channels/slack/dispatch.d.ts.map +1 -1
- package/dist/channels/slack/dispatch.js +1 -1
- 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 +1 -1
- package/dist/channels/slack/live-delivery.d.ts.map +1 -1
- package/dist/channels/slack/live-delivery.js +4 -2
- package/dist/channels/slack/manifest.d.ts +1 -1
- package/dist/channels/slack/manifest.js +1 -1
- package/dist/channels/slack/progress-delivery.d.ts +28 -0
- package/dist/channels/slack/progress-delivery.d.ts.map +1 -0
- package/dist/channels/slack/progress-delivery.js +115 -0
- 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 +1 -0
- package/dist/channels/slack/tool-approval-events.d.ts.map +1 -1
- package/dist/channels/slack/tool-approval-events.js +91 -1
- package/dist/channels/slack/types.d.ts +9 -1
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +4 -4
- package/dist/docs/assets/{app.jXleCmOc.js → app.BplEAZ3S.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.DpuI4i7Z.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.8LhyglIT.js → VPLocalSearchBox.CHXyv4wP.js} +1 -1
- package/dist/docs/assets/chunks/{theme.9ged-pr2.js → theme.BTcsZmjQ.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/{reference_agent-config.md.Cp_x38Nl.js → reference_agent-config.md.BHeYMgme.js} +1 -1
- package/dist/docs/assets/{reference_cli.md.CCnG-nJx.js → reference_cli.md.DfoeyvL0.js} +9 -6
- package/dist/docs/assets/{reference_cli.md.CCnG-nJx.lean.js → reference_cli.md.DfoeyvL0.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.BEJx9XVj.js} +2 -2
- package/dist/docs/assets/reference_sessions.md.NxFP_CAC.js +1 -0
- package/dist/docs/assets/{reference_tools.md.BHeXn2id.js → reference_tools.md.CvAHsdSp.js} +10 -2
- package/dist/docs/assets/{reference_tools.md.BHeXn2id.lean.js → reference_tools.md.CvAHsdSp.lean.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/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/design/runtime-abstraction.md +1757 -0
- 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 +5 -3
- package/dist/docs/llms-full.txt +2065 -637
- 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 +13 -10
- package/dist/docs/reference/cli.md +32 -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 +7 -7
- package/dist/docs/reference/http-api.md +3 -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 +15 -7
- package/dist/docs/reference/tools.md +34 -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 +5 -5
- package/dist/docs/templates/demo.html +4 -4
- 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 +3 -2
- package/dist/files-backends/cursor-hosted.d.ts.map +1 -1
- package/dist/files-backends/cursor-hosted.js +3 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/internal/advertise-tools.d.ts.map +1 -1
- package/dist/internal/advertise-tools.js +15 -9
- 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/as-of.d.ts +19 -0
- package/dist/internal/as-of.d.ts.map +1 -0
- package/dist/internal/as-of.js +82 -0
- package/dist/internal/chat-client.js +10 -0
- package/dist/internal/cli-deploy.d.ts +6 -0
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +149 -21
- 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/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-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/cursor-event-relay.d.ts.map +1 -1
- package/dist/internal/cursor-event-relay.js +5 -6
- 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 +11 -17
- package/dist/internal/framework-file-storage.d.ts.map +1 -1
- package/dist/internal/framework-file-storage.js +17 -40
- package/dist/internal/framework-storage-selection.d.ts +13 -11
- package/dist/internal/framework-storage-selection.d.ts.map +1 -1
- package/dist/internal/framework-storage-selection.js +37 -26
- package/dist/internal/hosted-admission-adapter.d.ts +28 -0
- package/dist/internal/hosted-admission-adapter.d.ts.map +1 -0
- package/dist/internal/hosted-admission-adapter.js +7 -0
- package/dist/internal/hosted-delivery-protocol.d.ts +13 -1
- package/dist/internal/hosted-delivery-protocol.d.ts.map +1 -1
- package/dist/internal/hosted-delivery-protocol.js +8 -1
- package/dist/internal/hosted-delivery.d.ts +21 -1
- package/dist/internal/hosted-delivery.d.ts.map +1 -1
- package/dist/internal/hosted-delivery.js +101 -34
- package/dist/internal/hosted-execution-diag.d.ts +59 -0
- package/dist/internal/hosted-execution-diag.d.ts.map +1 -0
- package/dist/internal/hosted-execution-diag.js +187 -0
- package/dist/internal/http-channel.d.ts.map +1 -1
- package/dist/internal/http-channel.js +85 -2
- package/dist/internal/init-project.d.ts.map +1 -1
- package/dist/internal/init-project.js +2 -0
- 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/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/null-kv-storage.d.ts +48 -0
- package/dist/internal/null-kv-storage.d.ts.map +1 -0
- package/dist/internal/null-kv-storage.js +58 -0
- package/dist/internal/otel/collector.d.ts.map +1 -1
- package/dist/internal/otel/collector.js +22 -9
- package/dist/internal/server.d.ts +6 -0
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +123 -33
- package/dist/internal/session-engine.d.ts +88 -3
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +538 -46
- package/dist/internal/session-run-log.d.ts +120 -0
- package/dist/internal/session-run-log.d.ts.map +1 -0
- package/dist/internal/session-run-log.js +359 -0
- 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/storage-coordinator.d.ts +13 -0
- package/dist/internal/storage-coordinator.d.ts.map +1 -1
- package/dist/internal/storage-coordinator.js +37 -6
- 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 +29 -9
- package/dist/internal/tool-policy.d.ts.map +1 -1
- package/dist/internal/tool-policy.js +24 -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-D7rGZNjy.css +1 -0
- package/dist/playground/assets/index-jzNbJENa.js +69 -0
- package/dist/playground/index.html +2 -2
- package/dist/storage-backends/cursor-hosted.d.ts +1 -1
- package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
- package/dist/storage-backends/cursor-hosted.js +12 -0
- package/dist/storage-protocol.d.ts +8 -0
- package/dist/storage-protocol.d.ts.map +1 -1
- package/dist/storage-protocol.js +8 -0
- package/dist/storage.d.ts +8 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/tools.d.ts +12 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/types.d.ts +88 -7
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +5 -3
- package/docs/deployment.md +5 -4
- package/docs/design/runtime-abstraction.md +1757 -0
- 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 +32 -16
- package/docs/reference/connections.md +6 -3
- package/docs/reference/http-api.md +3 -1
- package/docs/reference/sessions.md +4 -2
- package/docs/reference/tools.md +34 -2
- package/docs/templates/agents-md.md +110 -0
- package/docs/troubleshooting.md +2 -2
- package/package.json +1 -1
- package/skills/create-agent/SKILL.md +2 -1
- package/skills/mcp-auth/SKILL.md +6 -3
- package/skills/setup-slack/SKILL.md +23 -13
- package/src/bin/agent-serve.ts +12 -1
- package/src/channels/github/github-channel.ts +17 -0
- package/src/channels/origin/api.ts +24 -5
- package/src/channels/origin/binding.ts +55 -26
- package/src/channels/origin/index.ts +1 -0
- package/src/channels/origin/origin-channel.ts +78 -10
- package/src/channels/origin/reactions.ts +82 -0
- package/src/channels/origin/types.ts +11 -0
- package/src/channels/slack/approvals.ts +105 -0
- package/src/channels/slack/cli-project.ts +267 -0
- package/src/channels/slack/defaults.ts +165 -74
- package/src/channels/slack/dispatch.ts +2 -0
- package/src/channels/slack/init.ts +10 -5
- package/src/channels/slack/interactive.ts +35 -9
- package/src/channels/slack/live-delivery.ts +5 -3
- package/src/channels/slack/manifest.ts +1 -1
- package/src/channels/slack/progress-delivery.ts +133 -0
- package/src/channels/slack/setup.ts +10 -6
- package/src/channels/slack/slack-channel.ts +1 -0
- package/src/channels/slack/tool-approval-events.ts +93 -0
- package/src/channels/slack/types.ts +9 -1
- package/src/evals/run-facts.ts +32 -0
- package/src/files-backends/cursor-hosted.ts +3 -2
- package/src/index.ts +4 -0
- package/src/internal/advertise-tools.ts +31 -10
- package/src/internal/approval-gate.ts +20 -29
- package/src/internal/as-of.ts +107 -0
- package/src/internal/chat-client.ts +19 -0
- package/src/internal/cli-deploy.ts +203 -13
- 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/cursor/account-mcp.ts +21 -0
- package/src/internal/cursor-agent-template.ts +1 -0
- package/src/internal/cursor-event-relay.ts +5 -6
- package/src/internal/deploy-client.ts +43 -0
- package/src/internal/env-file.ts +84 -0
- package/src/internal/framework-file-storage.ts +18 -46
- package/src/internal/framework-storage-selection.ts +41 -25
- package/src/internal/hosted-admission-adapter.ts +42 -0
- package/src/internal/hosted-delivery-protocol.ts +14 -2
- package/src/internal/hosted-delivery.ts +169 -44
- package/src/internal/hosted-execution-diag.ts +252 -0
- package/src/internal/http-channel.ts +107 -0
- package/src/internal/init-project.ts +2 -0
- package/src/internal/logs-client.ts +5 -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/null-kv-storage.ts +74 -0
- package/src/internal/otel/collector.ts +30 -0
- package/src/internal/server.ts +159 -11
- package/src/internal/session-engine.ts +691 -57
- package/src/internal/session-run-log.ts +584 -0
- package/src/internal/sessions-client.ts +17 -8
- package/src/internal/storage-coordinator.ts +49 -9
- package/src/internal/storage-roles.ts +7 -0
- package/src/internal/stream-progress.ts +19 -0
- package/src/internal/tool-policy.ts +49 -9
- package/src/internal/trajectory.ts +10 -0
- package/src/internal/turn-outbox.ts +85 -0
- package/src/storage-backends/cursor-hosted.ts +13 -1
- package/src/storage-protocol.ts +10 -0
- package/src/storage.ts +12 -0
- package/src/tools.ts +12 -2
- package/src/types.ts +93 -8
- 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/dist/docs/assets/chunks/@localSearchIndexroot.DFoGuETX.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.CI93Ltlb.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/playground/assets/index-B1DDvBeC.css +0 -1
- package/dist/playground/assets/index-Cx9CclK_.js +0 -88
- package/docs/design/agsh.md +0 -406
- /package/dist/docs/assets/{deployment.md.D9msOFOW.lean.js → deployment.md.MFPKqsqI.lean.js} +0 -0
- /package/dist/docs/assets/{index.md.CI93Ltlb.lean.js → index.md.CVeRUOeZ.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.BEJx9XVj.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/{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,9 +83,13 @@ 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";
|
|
92
|
+
import { assertTimeArgs, normalizeAsOf, sessionClock } from "./as-of.js";
|
|
85
93
|
import { SessionAuthError, samePrincipal } from "./auth.js";
|
|
86
94
|
import { mergeCloudOptions, resolveSessionRuntime } from "./cloud-merge.js";
|
|
87
95
|
import {
|
|
@@ -125,7 +133,8 @@ import {
|
|
|
125
133
|
parseContinuationToken,
|
|
126
134
|
} from "./ids.js";
|
|
127
135
|
import { validateToolInput } from "./json-schema.js";
|
|
128
|
-
import { McpHost } from "./mcp-host.js";
|
|
136
|
+
import { isMcpNeedsAuthError, McpHost } from "./mcp-host.js";
|
|
137
|
+
import { McpOauthOnDemandHub } from "./mcp-oauth-on-demand.js";
|
|
129
138
|
import type { OtelCollector } from "./otel/collector.js";
|
|
130
139
|
import {
|
|
131
140
|
shouldStartProcessMemoryTelemetry,
|
|
@@ -143,7 +152,7 @@ import { syncSkillsToStore } from "./skills-sync.js";
|
|
|
143
152
|
import { steeringEnabled } from "./steering.js";
|
|
144
153
|
import { StorageCoordinator } from "./storage-coordinator.js";
|
|
145
154
|
import type { FrameworkSessionStore } from "./storage-roles.js";
|
|
146
|
-
import { decideToolCall,
|
|
155
|
+
import { decideToolCall, directCallEffect } from "./tool-policy.js";
|
|
147
156
|
import { normalizeToolResult, toolCallErrorMessage } from "./tool-result.js";
|
|
148
157
|
import {
|
|
149
158
|
type EstimateTurnCostFn,
|
|
@@ -152,6 +161,7 @@ import {
|
|
|
152
161
|
} from "./turn-cost.js";
|
|
153
162
|
import { TurnGovernor } from "./turn-governor.js";
|
|
154
163
|
import { TurnLeaseStore } from "./turn-lease-store.js";
|
|
164
|
+
import { TurnOutbox } from "./turn-outbox.js";
|
|
155
165
|
import {
|
|
156
166
|
buildAgentsMdContent,
|
|
157
167
|
buildCloudMemorySection,
|
|
@@ -258,6 +268,22 @@ export class ApprovalInterruptedError extends Error {
|
|
|
258
268
|
}
|
|
259
269
|
}
|
|
260
270
|
|
|
271
|
+
export class AuthNotFoundError extends Error {
|
|
272
|
+
constructor(sessionId: string, callId: string) {
|
|
273
|
+
super(`No pending MCP auth for session ${sessionId} call ${callId}`);
|
|
274
|
+
this.name = "AuthNotFoundError";
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export class AuthInterruptedError extends Error {
|
|
279
|
+
constructor(sessionId: string, callId: string) {
|
|
280
|
+
super(
|
|
281
|
+
`MCP auth for session ${sessionId} call ${callId} is no longer live (host may have restarted while the tool was parked)`
|
|
282
|
+
);
|
|
283
|
+
this.name = "AuthInterruptedError";
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
261
287
|
export interface SessionEngineOptions {
|
|
262
288
|
project: AgentProject;
|
|
263
289
|
stateRoot: string;
|
|
@@ -312,6 +338,16 @@ interface ActiveToolContext {
|
|
|
312
338
|
emit: (payload: SessionEventPayload) => void;
|
|
313
339
|
}
|
|
314
340
|
|
|
341
|
+
type McpAuthParkCtx = {
|
|
342
|
+
sessionId: string;
|
|
343
|
+
turnId: string;
|
|
344
|
+
callId: string;
|
|
345
|
+
toolName: string;
|
|
346
|
+
emit: (payload: SessionEventPayload) => void;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const mcpAuthAls = new AsyncLocalStorage<McpAuthParkCtx>();
|
|
350
|
+
|
|
315
351
|
export interface EngineSendOptions extends SendMessageOptions {
|
|
316
352
|
mode?: "chat" | "task";
|
|
317
353
|
/**
|
|
@@ -346,6 +382,8 @@ export class SessionEngine {
|
|
|
346
382
|
readonly askDedupe: SlackAskDedupeStore;
|
|
347
383
|
/** Cross-process turn ownership under `<stateRoot>/turn-leases`. */
|
|
348
384
|
readonly turnLeases: TurnLeaseStore;
|
|
385
|
+
/** Messages for turns another server owns, under `<stateRoot>/outbox`. */
|
|
386
|
+
readonly outbox: TurnOutbox<AdmitTurnOptions>;
|
|
349
387
|
/**
|
|
350
388
|
* Runtime for `agent/storage.ts` (`defineStorage`), when
|
|
351
389
|
* authored. All durable state changes funnel through it (session
|
|
@@ -370,6 +408,13 @@ export class SessionEngine {
|
|
|
370
408
|
/** Sessions with a turn being admitted (before the run promise exists). */
|
|
371
409
|
private readonly pendingTurns = new Set<string>();
|
|
372
410
|
private readonly activeTurns = new Map<string, Promise<TurnOutcome>>();
|
|
411
|
+
/**
|
|
412
|
+
* Sessions whose workspace a turn in this process has materialized. A
|
|
413
|
+
* session is busy from admit — before the governor slot and the turn's
|
|
414
|
+
* own materialize — so a read running alongside a turn consults this,
|
|
415
|
+
* not busy, to know whether the workspace is ready to be left alone.
|
|
416
|
+
*/
|
|
417
|
+
private readonly materializedWorkspaces = new Set<string>();
|
|
373
418
|
/** Context used by the cloud turn's HTTP MCP callbacks. */
|
|
374
419
|
private readonly activeToolContexts = new Map<string, ActiveToolContext>();
|
|
375
420
|
/**
|
|
@@ -410,6 +455,11 @@ export class SessionEngine {
|
|
|
410
455
|
private readonly mcpHost: McpHost;
|
|
411
456
|
private readonly platforms: HostPlatforms;
|
|
412
457
|
private readonly approvals = new ApprovalGate();
|
|
458
|
+
private readonly auths = new ApprovalGate<PendingAuth>();
|
|
459
|
+
private readonly oauthHub = new McpOauthOnDemandHub();
|
|
460
|
+
private probeCursorAccount:
|
|
461
|
+
| ((connection: string) => Promise<string | undefined>)
|
|
462
|
+
| undefined;
|
|
413
463
|
private readonly abCollector: ABCollector;
|
|
414
464
|
private readonly otel: OtelCollector | undefined;
|
|
415
465
|
private readonly otelHost: HostOtelSessionApi;
|
|
@@ -458,6 +508,7 @@ export class SessionEngine {
|
|
|
458
508
|
this.turnLeases = new TurnLeaseStore(
|
|
459
509
|
join(options.stateRoot, "turn-leases")
|
|
460
510
|
);
|
|
511
|
+
this.outbox = new TurnOutbox(join(options.stateRoot, "outbox"));
|
|
461
512
|
this.logs = new EventLogStore(
|
|
462
513
|
(sessionId) => this.sessions.eventFilePath(sessionId),
|
|
463
514
|
(sessionId, error) =>
|
|
@@ -565,10 +616,25 @@ export class SessionEngine {
|
|
|
565
616
|
this.cloudServerToolsConnection = connection;
|
|
566
617
|
}
|
|
567
618
|
|
|
619
|
+
setMcpOauthCallbackUrl(url: string): void {
|
|
620
|
+
this.oauthHub.callbackUrl = url;
|
|
621
|
+
this.mcpHost.setOAuthRedirectUrl(url);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
setCursorAccountAuthProbe(
|
|
625
|
+
probe: (connection: string) => Promise<string | undefined>
|
|
626
|
+
): void {
|
|
627
|
+
this.probeCursorAccount = probe;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
get oauthCallbackHub(): McpOauthOnDemandHub {
|
|
631
|
+
return this.oauthHub;
|
|
632
|
+
}
|
|
633
|
+
|
|
568
634
|
/** Shared host services (MCP / GitHub / Slack / reminders / evals / kv / files / otel / Slack nudges). */
|
|
569
635
|
get host(): HostContext {
|
|
570
636
|
return {
|
|
571
|
-
mcp: this.mcpHost,
|
|
637
|
+
mcp: this.wrapMcp(this.mcpHost),
|
|
572
638
|
github: this.platforms.github,
|
|
573
639
|
slack: this.platforms.slack,
|
|
574
640
|
reminders: this.reminderApi,
|
|
@@ -958,12 +1024,15 @@ export class SessionEngine {
|
|
|
958
1024
|
const now = new Date().toISOString();
|
|
959
1025
|
const channelState = this.initialChannelState(channelId, sendOptions.state);
|
|
960
1026
|
const purpose = sendOptions.purpose === "eval" ? "eval" : "live";
|
|
1027
|
+
const frozenAsOf = normalizeAsOf(sendOptions.asOf);
|
|
1028
|
+
assertTimeArgs(this.project.agent.tools, frozenAsOf);
|
|
961
1029
|
let record: SessionRecord = {
|
|
962
1030
|
sessionId: sendOptions.hostedSessionId ?? newSessionId(),
|
|
963
1031
|
channelId,
|
|
964
1032
|
mode: sendOptions.mode ?? "chat",
|
|
965
1033
|
purpose,
|
|
966
|
-
|
|
1034
|
+
dryRun: sendOptions.dryRun === true ? true : undefined,
|
|
1035
|
+
asOf: frozenAsOf,
|
|
967
1036
|
continuationKey: sendOptions.continuationToken ?? newContinuationKey(),
|
|
968
1037
|
auth,
|
|
969
1038
|
title: sendOptions.title,
|
|
@@ -981,7 +1050,10 @@ export class SessionEngine {
|
|
|
981
1050
|
this.storage?.sessionRecord(record);
|
|
982
1051
|
await this.appendEvent(record.sessionId, {
|
|
983
1052
|
type: "session.started",
|
|
984
|
-
data: {
|
|
1053
|
+
data: {
|
|
1054
|
+
channelId,
|
|
1055
|
+
dryRun: record.dryRun === true ? true : undefined,
|
|
1056
|
+
},
|
|
985
1057
|
});
|
|
986
1058
|
record = await this.enrollABs(record);
|
|
987
1059
|
const since = (await this.logs.get(record.sessionId)).count;
|
|
@@ -1231,7 +1303,14 @@ export class SessionEngine {
|
|
|
1231
1303
|
const prior = this.admissionChains.get(sessionId) ?? Promise.resolve();
|
|
1232
1304
|
const admitted = prior
|
|
1233
1305
|
.catch(() => {})
|
|
1234
|
-
.then(() =>
|
|
1306
|
+
.then(async () => {
|
|
1307
|
+
// Another server holds the turn: hand the message to it and coalesce.
|
|
1308
|
+
if (await this.turnLeases.runningElsewhere(sessionId)) {
|
|
1309
|
+
await this.outbox.post(sessionId, { message, options });
|
|
1310
|
+
return true;
|
|
1311
|
+
}
|
|
1312
|
+
return this.admitTurn(sessionId, message, options);
|
|
1313
|
+
});
|
|
1235
1314
|
this.admissionChains.set(
|
|
1236
1315
|
sessionId,
|
|
1237
1316
|
admitted.catch(() => {})
|
|
@@ -1239,6 +1318,20 @@ export class SessionEngine {
|
|
|
1239
1318
|
return admitted;
|
|
1240
1319
|
}
|
|
1241
1320
|
|
|
1321
|
+
/** Admit what other servers handed to this turn's holder. Never awaited by the lifecycle. */
|
|
1322
|
+
private drainOutbox(sessionId: string): void {
|
|
1323
|
+
void this.outbox
|
|
1324
|
+
.takeAll(sessionId)
|
|
1325
|
+
.then((items) => {
|
|
1326
|
+
for (const item of items) {
|
|
1327
|
+
void this.startTurn(sessionId, item.message, item.options).catch(
|
|
1328
|
+
() => {}
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
})
|
|
1332
|
+
.catch(() => {});
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1242
1335
|
private enqueueCoalesced(sessionId: string, item: CoalescedQueuedTurn): void {
|
|
1243
1336
|
const queue = this.coalesceQueues.get(sessionId) ?? [];
|
|
1244
1337
|
if (
|
|
@@ -1476,6 +1569,8 @@ export class SessionEngine {
|
|
|
1476
1569
|
let lifecycle!: Promise<TurnOutcome>;
|
|
1477
1570
|
lifecycle = (async (): Promise<TurnOutcome> => {
|
|
1478
1571
|
let outcome: TurnOutcome;
|
|
1572
|
+
const poll = setInterval(() => this.drainOutbox(sessionId), 1_000);
|
|
1573
|
+
poll.unref();
|
|
1479
1574
|
try {
|
|
1480
1575
|
outcome = await this.executeTurn(
|
|
1481
1576
|
record,
|
|
@@ -1489,6 +1584,8 @@ export class SessionEngine {
|
|
|
1489
1584
|
status: "error",
|
|
1490
1585
|
errorMessage: describeError(error),
|
|
1491
1586
|
};
|
|
1587
|
+
} finally {
|
|
1588
|
+
clearInterval(poll);
|
|
1492
1589
|
}
|
|
1493
1590
|
this.lastOutcomes.set(sessionId, outcome);
|
|
1494
1591
|
const wasCurrent = this.activeTurns.get(sessionId) === lifecycle;
|
|
@@ -1518,6 +1615,10 @@ export class SessionEngine {
|
|
|
1518
1615
|
// The drain may have nested a successor turn, which took its own
|
|
1519
1616
|
// reference; the lease outlives this release until that one ends too.
|
|
1520
1617
|
await lease.release();
|
|
1618
|
+
// Handed off between the last poll and the release: nobody else will.
|
|
1619
|
+
if (!abort.signal.aborted) {
|
|
1620
|
+
this.drainOutbox(sessionId);
|
|
1621
|
+
}
|
|
1521
1622
|
return outcome;
|
|
1522
1623
|
})();
|
|
1523
1624
|
this.activeTurns.set(sessionId, lifecycle);
|
|
@@ -1562,6 +1663,7 @@ export class SessionEngine {
|
|
|
1562
1663
|
this.coalesceDrains.has(sessionId) ||
|
|
1563
1664
|
(this.coalesceQueues.get(sessionId)?.length ?? 0) > 0;
|
|
1564
1665
|
this.clearCoalesced(sessionId);
|
|
1666
|
+
await this.outbox.clear(sessionId);
|
|
1565
1667
|
const record = await this.sessions.get(sessionId);
|
|
1566
1668
|
const continuationKey = record?.continuationKey;
|
|
1567
1669
|
if (continuationKey !== undefined && continuationKey !== null) {
|
|
@@ -1795,6 +1897,7 @@ export class SessionEngine {
|
|
|
1795
1897
|
seed: isFirstTurn,
|
|
1796
1898
|
stateRoot: this.stateRoot,
|
|
1797
1899
|
});
|
|
1900
|
+
this.materializedWorkspaces.add(sessionId);
|
|
1798
1901
|
// Must precede the carrier: the harness resolves rules, skills,
|
|
1799
1902
|
// `mcp.json` and hooks from its git root, so a workspace nested in an
|
|
1800
1903
|
// outer repo would load that repo's hooks instead of the ones written
|
|
@@ -1879,11 +1982,14 @@ export class SessionEngine {
|
|
|
1879
1982
|
}
|
|
1880
1983
|
} catch (error) {
|
|
1881
1984
|
this.approvals.cancelSession(sessionId, "deny");
|
|
1985
|
+
this.auths.cancelSession(sessionId, "deny");
|
|
1882
1986
|
outcome = { status: "error", errorMessage: describeError(error) };
|
|
1883
1987
|
} finally {
|
|
1884
1988
|
permit.release();
|
|
1885
1989
|
this.approvals.cancelSession(sessionId, "deny");
|
|
1990
|
+
this.auths.cancelSession(sessionId, "deny");
|
|
1886
1991
|
await this.clearPendingApprovals(sessionId);
|
|
1992
|
+
await this.clearPendingAuths(sessionId);
|
|
1887
1993
|
// Anything still queued belongs to the next turn's boundary drain, not
|
|
1888
1994
|
// to this turn's edge detection.
|
|
1889
1995
|
this.deliveredNudges.delete(sessionId);
|
|
@@ -2105,6 +2211,8 @@ export class SessionEngine {
|
|
|
2105
2211
|
channelId: record.channelId,
|
|
2106
2212
|
channelKind: this.channelsById.get(record.channelId)?.meta?.kind,
|
|
2107
2213
|
environment: isDevMode() ? "local dev server" : "hosted deployment",
|
|
2214
|
+
now:
|
|
2215
|
+
record.asOf === undefined ? undefined : sessionClock(record.asOf)(),
|
|
2108
2216
|
});
|
|
2109
2217
|
turnPrompt = `${contextBlock}\n\n${prompt}`;
|
|
2110
2218
|
}
|
|
@@ -2518,11 +2626,21 @@ export class SessionEngine {
|
|
|
2518
2626
|
// named passthroughs resolved fresh on every turn — reminder fires and
|
|
2519
2627
|
// post-restart follow-ups included — and a listing failure throws,
|
|
2520
2628
|
// failing the turn closed.
|
|
2521
|
-
for (const tool of await
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2629
|
+
for (const tool of await mcpAuthAls.run(
|
|
2630
|
+
{
|
|
2631
|
+
sessionId: record.sessionId,
|
|
2632
|
+
turnId,
|
|
2633
|
+
callId: newToolCallId("mcp"),
|
|
2634
|
+
toolName: "mcp",
|
|
2635
|
+
emit,
|
|
2636
|
+
},
|
|
2637
|
+
() =>
|
|
2638
|
+
resolveAdvertisedTools({
|
|
2639
|
+
agent,
|
|
2640
|
+
session: this.sessionInfo(record),
|
|
2641
|
+
hostMcp: this.advertiseMcp(),
|
|
2642
|
+
})
|
|
2643
|
+
)) {
|
|
2526
2644
|
register(tool);
|
|
2527
2645
|
}
|
|
2528
2646
|
return tools;
|
|
@@ -2615,6 +2733,7 @@ export class SessionEngine {
|
|
|
2615
2733
|
getSession: (channelId, sessionId) =>
|
|
2616
2734
|
this.getChannelSession(channelId, sessionId),
|
|
2617
2735
|
emit: args.emit,
|
|
2736
|
+
now: sessionClock(args.record.asOf),
|
|
2618
2737
|
};
|
|
2619
2738
|
try {
|
|
2620
2739
|
// Before the approval gate: a dry run is unattended, so parking a write
|
|
@@ -2632,6 +2751,8 @@ export class SessionEngine {
|
|
|
2632
2751
|
: {}),
|
|
2633
2752
|
dryRunResult: args.tool.definition.dryRunResult,
|
|
2634
2753
|
input: validation.value,
|
|
2754
|
+
asOf: args.record.asOf,
|
|
2755
|
+
timeArgs: args.tool.definition.timeArgs,
|
|
2635
2756
|
});
|
|
2636
2757
|
if (decision.kind === "answer") {
|
|
2637
2758
|
this.stubbedCallIds.get(args.record.sessionId)?.add(toolCallId);
|
|
@@ -2640,6 +2761,12 @@ export class SessionEngine {
|
|
|
2640
2761
|
);
|
|
2641
2762
|
return normalizeToolResult(decision.result);
|
|
2642
2763
|
}
|
|
2764
|
+
if (decision.kind === "unbounded") {
|
|
2765
|
+
this.logger(
|
|
2766
|
+
`[session] tool ${args.tool.name} refused, unbounded_time (${toolCallId})`
|
|
2767
|
+
);
|
|
2768
|
+
return toolErrorResult(decision.message);
|
|
2769
|
+
}
|
|
2643
2770
|
if (decision.kind === "refuse") {
|
|
2644
2771
|
this.logger(
|
|
2645
2772
|
`[session] tool ${args.tool.name} refused, effect unclassified (${toolCallId})`
|
|
@@ -2672,7 +2799,16 @@ export class SessionEngine {
|
|
|
2672
2799
|
}
|
|
2673
2800
|
const started = Date.now();
|
|
2674
2801
|
this.logger(`[session] tool ${args.tool.name} start`);
|
|
2675
|
-
const result = await
|
|
2802
|
+
const result = await mcpAuthAls.run(
|
|
2803
|
+
{
|
|
2804
|
+
sessionId: args.record.sessionId,
|
|
2805
|
+
turnId: args.turnId,
|
|
2806
|
+
callId: toolCallId,
|
|
2807
|
+
toolName: args.tool.name,
|
|
2808
|
+
emit: args.emit,
|
|
2809
|
+
},
|
|
2810
|
+
() => execute(validation.value as never, toolCtx)
|
|
2811
|
+
);
|
|
2676
2812
|
this.logger(
|
|
2677
2813
|
`[session] tool ${args.tool.name} done (${Date.now() - started}ms)`
|
|
2678
2814
|
);
|
|
@@ -2708,10 +2844,10 @@ export class SessionEngine {
|
|
|
2708
2844
|
args: args.args,
|
|
2709
2845
|
requestedAt,
|
|
2710
2846
|
};
|
|
2711
|
-
const resolutionPromise = this.approvals.wait(
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2847
|
+
const resolutionPromise = this.approvals.wait(
|
|
2848
|
+
args.sessionId,
|
|
2849
|
+
pendingRecord
|
|
2850
|
+
);
|
|
2715
2851
|
await this.updateRecord(args.sessionId, (current) => ({
|
|
2716
2852
|
...current,
|
|
2717
2853
|
pendingApprovals: [
|
|
@@ -2831,6 +2967,318 @@ export class SessionEngine {
|
|
|
2831
2967
|
}));
|
|
2832
2968
|
}
|
|
2833
2969
|
|
|
2970
|
+
private wrapMcp(inner: McpHost): HostMcpRegistry {
|
|
2971
|
+
return {
|
|
2972
|
+
names: () => inner.names(),
|
|
2973
|
+
listTools: (name) => this.withMcpAuth(name, () => inner.listTools(name)),
|
|
2974
|
+
callTool: (name, tool, args) =>
|
|
2975
|
+
this.withMcpAuth(name, () => inner.callTool(name, tool, args)),
|
|
2976
|
+
};
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
private advertiseMcp(): AdvertiseHostMcp {
|
|
2980
|
+
return {
|
|
2981
|
+
listTools: (name) =>
|
|
2982
|
+
this.withMcpListAuth(name, () => this.mcpHost.listTools(name)),
|
|
2983
|
+
callTool: (name, tool, args) =>
|
|
2984
|
+
this.withMcpAuth(name, () => this.mcpHost.callTool(name, tool, args)),
|
|
2985
|
+
oneOff: (connection) => {
|
|
2986
|
+
const inner = this.mcpHost.oneOff(connection);
|
|
2987
|
+
return {
|
|
2988
|
+
listTools: () =>
|
|
2989
|
+
this.withMcpListAuth(connection.name, () => inner.listTools()),
|
|
2990
|
+
callTool: (tool, args) =>
|
|
2991
|
+
this.withMcpAuth(connection.name, () => inner.callTool(tool, args)),
|
|
2992
|
+
};
|
|
2993
|
+
},
|
|
2994
|
+
};
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
private withMcpListAuth<T>(
|
|
2998
|
+
connection: string,
|
|
2999
|
+
run: () => Promise<T>
|
|
3000
|
+
): Promise<T> {
|
|
3001
|
+
const ctx = mcpAuthAls.getStore();
|
|
3002
|
+
if (ctx === undefined) {
|
|
3003
|
+
return this.withMcpAuth(connection, run);
|
|
3004
|
+
}
|
|
3005
|
+
const toolName = `mcp:${connection}`;
|
|
3006
|
+
return mcpAuthAls.run(
|
|
3007
|
+
{
|
|
3008
|
+
...ctx,
|
|
3009
|
+
callId: newToolCallId(toolName),
|
|
3010
|
+
toolName,
|
|
3011
|
+
},
|
|
3012
|
+
() => this.withMcpAuth(connection, run)
|
|
3013
|
+
);
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
private async withMcpAuth<T>(
|
|
3017
|
+
connection: string,
|
|
3018
|
+
run: () => Promise<T>
|
|
3019
|
+
): Promise<T> {
|
|
3020
|
+
try {
|
|
3021
|
+
return await run();
|
|
3022
|
+
} catch (error) {
|
|
3023
|
+
if (!isMcpNeedsAuthError(error)) {
|
|
3024
|
+
throw error;
|
|
3025
|
+
}
|
|
3026
|
+
return await this.parkAndRetryMcp(connection, run, error);
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
private async parkAndRetryMcp<T>(
|
|
3031
|
+
connection: string,
|
|
3032
|
+
run: () => Promise<T>,
|
|
3033
|
+
error: unknown
|
|
3034
|
+
): Promise<T> {
|
|
3035
|
+
const ctx = mcpAuthAls.getStore();
|
|
3036
|
+
if (ctx === undefined) {
|
|
3037
|
+
throw error;
|
|
3038
|
+
}
|
|
3039
|
+
const minted = await this.mintAuthUrl(connection, ctx.sessionId);
|
|
3040
|
+
if (minted === undefined) {
|
|
3041
|
+
throw error;
|
|
3042
|
+
}
|
|
3043
|
+
if ("authorized" in minted) {
|
|
3044
|
+
await this.persistHostedMcpOauth(connection);
|
|
3045
|
+
await this.mcpHost.dropClient(connection);
|
|
3046
|
+
return await this.withMcpAuth(connection, run);
|
|
3047
|
+
}
|
|
3048
|
+
if (minted.state !== "") {
|
|
3049
|
+
const bind = this.oauthHub.bind(minted.state, ctx.sessionId, ctx.callId);
|
|
3050
|
+
if (bind === "already_done") {
|
|
3051
|
+
await this.persistHostedMcpOauth(connection);
|
|
3052
|
+
await this.mcpHost.dropClient(connection);
|
|
3053
|
+
return await this.withMcpAuth(connection, run);
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
const decision = await this.parkForAuth({
|
|
3057
|
+
sessionId: ctx.sessionId,
|
|
3058
|
+
turnId: ctx.turnId,
|
|
3059
|
+
callId: ctx.callId,
|
|
3060
|
+
toolName: ctx.toolName,
|
|
3061
|
+
connection,
|
|
3062
|
+
authUrl: minted.authUrl,
|
|
3063
|
+
emit: ctx.emit,
|
|
3064
|
+
});
|
|
3065
|
+
if (decision === "deny") {
|
|
3066
|
+
throw new Error(
|
|
3067
|
+
`MCP connection "${connection}" authorization was denied.`
|
|
3068
|
+
);
|
|
3069
|
+
}
|
|
3070
|
+
await this.persistHostedMcpOauth(connection);
|
|
3071
|
+
await this.mcpHost.dropClient(connection);
|
|
3072
|
+
return await this.withMcpAuth(connection, run);
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
private async persistHostedMcpOauth(connection: string): Promise<void> {
|
|
3076
|
+
const authored = this.authoredConnection(connection);
|
|
3077
|
+
if (
|
|
3078
|
+
authored === undefined ||
|
|
3079
|
+
!isHostOauthConnectionTransport(authored.transport)
|
|
3080
|
+
) {
|
|
3081
|
+
return;
|
|
3082
|
+
}
|
|
3083
|
+
await this.oauthHub.persistFor(connection, authored.transport.url);
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
private authoredConnection(name: string) {
|
|
3087
|
+
return (
|
|
3088
|
+
this.project.agent.connections.find((entry) => entry.name === name) ??
|
|
3089
|
+
this.project.agent.hostConnections?.find((entry) => entry.name === name)
|
|
3090
|
+
);
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3093
|
+
private async mintAuthUrl(
|
|
3094
|
+
connection: string,
|
|
3095
|
+
sessionId: string
|
|
3096
|
+
): Promise<
|
|
3097
|
+
{ authorized: true } | { authUrl: string; state: string } | undefined
|
|
3098
|
+
> {
|
|
3099
|
+
const authored = this.authoredConnection(connection);
|
|
3100
|
+
if (authored === undefined) {
|
|
3101
|
+
return undefined;
|
|
3102
|
+
}
|
|
3103
|
+
if (isCursorAccountTransport(authored.transport)) {
|
|
3104
|
+
const probe = this.probeCursorAccount;
|
|
3105
|
+
if (probe === undefined) {
|
|
3106
|
+
return undefined;
|
|
3107
|
+
}
|
|
3108
|
+
const authUrl = await probe(connection);
|
|
3109
|
+
if (authUrl === undefined || authUrl === "") {
|
|
3110
|
+
return undefined;
|
|
3111
|
+
}
|
|
3112
|
+
return { authUrl, state: "" };
|
|
3113
|
+
}
|
|
3114
|
+
if (!isHostOauthConnectionTransport(authored.transport)) {
|
|
3115
|
+
return undefined;
|
|
3116
|
+
}
|
|
3117
|
+
return await this.oauthHub.mint({
|
|
3118
|
+
connection,
|
|
3119
|
+
url: authored.transport.url,
|
|
3120
|
+
type: authored.transport.type === "sse" ? "sse" : "http",
|
|
3121
|
+
headers: authored.transport.headers,
|
|
3122
|
+
sessionId,
|
|
3123
|
+
});
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
private async parkForAuth(args: {
|
|
3127
|
+
sessionId: string;
|
|
3128
|
+
turnId: string;
|
|
3129
|
+
callId: string;
|
|
3130
|
+
toolName: string;
|
|
3131
|
+
connection: string;
|
|
3132
|
+
authUrl: string;
|
|
3133
|
+
emit: (payload: SessionEventPayload) => void;
|
|
3134
|
+
}): Promise<ApprovalDecision> {
|
|
3135
|
+
const requestedAt = new Date().toISOString();
|
|
3136
|
+
const pendingRecord: PendingAuth = {
|
|
3137
|
+
callId: args.callId,
|
|
3138
|
+
turnId: args.turnId,
|
|
3139
|
+
toolName: args.toolName,
|
|
3140
|
+
connection: args.connection,
|
|
3141
|
+
authUrl: args.authUrl,
|
|
3142
|
+
requestedAt,
|
|
3143
|
+
};
|
|
3144
|
+
const resolutionPromise = this.auths.wait(args.sessionId, pendingRecord);
|
|
3145
|
+
await this.updateRecord(args.sessionId, (current) => ({
|
|
3146
|
+
...current,
|
|
3147
|
+
pendingAuths: [
|
|
3148
|
+
...(current.pendingAuths ?? []).filter(
|
|
3149
|
+
(item) => item.callId !== args.callId
|
|
3150
|
+
),
|
|
3151
|
+
pendingRecord,
|
|
3152
|
+
],
|
|
3153
|
+
}));
|
|
3154
|
+
args.emit({
|
|
3155
|
+
type: "action.auth_requested",
|
|
3156
|
+
data: {
|
|
3157
|
+
callId: args.callId,
|
|
3158
|
+
toolName: args.toolName,
|
|
3159
|
+
connection: args.connection,
|
|
3160
|
+
authUrl: args.authUrl,
|
|
3161
|
+
},
|
|
3162
|
+
});
|
|
3163
|
+
this.logger(
|
|
3164
|
+
`[session] tool ${args.toolName} awaiting MCP auth ${args.connection} (${args.callId})`
|
|
3165
|
+
);
|
|
3166
|
+
const { decision, by } = await resolutionPromise;
|
|
3167
|
+
await this.updateRecord(args.sessionId, (current) => ({
|
|
3168
|
+
...current,
|
|
3169
|
+
pendingAuths: (current.pendingAuths ?? []).filter(
|
|
3170
|
+
(item) => item.callId !== args.callId
|
|
3171
|
+
),
|
|
3172
|
+
}));
|
|
3173
|
+
args.emit({
|
|
3174
|
+
type: "action.auth_resolved",
|
|
3175
|
+
data: {
|
|
3176
|
+
callId: args.callId,
|
|
3177
|
+
toolName: args.toolName,
|
|
3178
|
+
connection: args.connection,
|
|
3179
|
+
decision,
|
|
3180
|
+
by,
|
|
3181
|
+
},
|
|
3182
|
+
});
|
|
3183
|
+
this.logger(
|
|
3184
|
+
`[session] tool ${args.toolName} MCP auth ${decision} (${args.callId})`
|
|
3185
|
+
);
|
|
3186
|
+
return decision;
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
private async clearPendingAuths(sessionId: string): Promise<void> {
|
|
3190
|
+
await this.oauthHub.abortSession(sessionId);
|
|
3191
|
+
const current = await this.sessions.get(sessionId);
|
|
3192
|
+
if (
|
|
3193
|
+
current === undefined ||
|
|
3194
|
+
current.pendingAuths === undefined ||
|
|
3195
|
+
current.pendingAuths.length === 0
|
|
3196
|
+
) {
|
|
3197
|
+
return;
|
|
3198
|
+
}
|
|
3199
|
+
await this.updateRecord(sessionId, (record) => ({
|
|
3200
|
+
...record,
|
|
3201
|
+
pendingAuths: [],
|
|
3202
|
+
}));
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
async listPendingAuths(sessionId: string): Promise<PendingAuth[]> {
|
|
3206
|
+
const record = await this.sessions.get(sessionId);
|
|
3207
|
+
if (record === undefined) {
|
|
3208
|
+
return [];
|
|
3209
|
+
}
|
|
3210
|
+
const live = this.auths.list(sessionId);
|
|
3211
|
+
if (live.length > 0) {
|
|
3212
|
+
return live;
|
|
3213
|
+
}
|
|
3214
|
+
return record.pendingAuths ?? [];
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
async resolveAuth(
|
|
3218
|
+
sessionId: string,
|
|
3219
|
+
callId: string,
|
|
3220
|
+
decision: ApprovalDecision,
|
|
3221
|
+
auth: AuthContext | null,
|
|
3222
|
+
options?: { asSessionOwner?: boolean }
|
|
3223
|
+
): Promise<PendingAuth> {
|
|
3224
|
+
const record = await this.sessions.get(sessionId);
|
|
3225
|
+
if (record === undefined) {
|
|
3226
|
+
throw new AuthNotFoundError(sessionId, callId);
|
|
3227
|
+
}
|
|
3228
|
+
const ownershipAuth = options?.asSessionOwner === true ? record.auth : auth;
|
|
3229
|
+
if (!samePrincipal(record.auth, ownershipAuth)) {
|
|
3230
|
+
throw new SessionAuthError(sessionId);
|
|
3231
|
+
}
|
|
3232
|
+
const pending =
|
|
3233
|
+
(record.pendingAuths ?? []).find((item) => item.callId === callId) ??
|
|
3234
|
+
this.auths.list(sessionId).find((item) => item.callId === callId);
|
|
3235
|
+
if (pending === undefined) {
|
|
3236
|
+
throw new AuthNotFoundError(sessionId, callId);
|
|
3237
|
+
}
|
|
3238
|
+
const resolvedBy = approvalResolverLabel(auth);
|
|
3239
|
+
if (decision === "deny") {
|
|
3240
|
+
await this.oauthHub.abortCall(sessionId, callId);
|
|
3241
|
+
}
|
|
3242
|
+
if (!this.auths.resolve(sessionId, callId, decision, resolvedBy)) {
|
|
3243
|
+
await this.removePendingAuth(sessionId, callId);
|
|
3244
|
+
throw new AuthInterruptedError(sessionId, callId);
|
|
3245
|
+
}
|
|
3246
|
+
return pending;
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
private async removePendingAuth(
|
|
3250
|
+
sessionId: string,
|
|
3251
|
+
callId: string
|
|
3252
|
+
): Promise<void> {
|
|
3253
|
+
await this.updateRecord(sessionId, (current) => ({
|
|
3254
|
+
...current,
|
|
3255
|
+
pendingAuths: (current.pendingAuths ?? []).filter(
|
|
3256
|
+
(item) => item.callId !== callId
|
|
3257
|
+
),
|
|
3258
|
+
}));
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
async runWithMcpPark<T>(
|
|
3262
|
+
sessionId: string,
|
|
3263
|
+
toolName: string,
|
|
3264
|
+
fn: () => Promise<T>
|
|
3265
|
+
): Promise<T> {
|
|
3266
|
+
const active = this.activeToolContexts.get(sessionId);
|
|
3267
|
+
if (active === undefined) {
|
|
3268
|
+
return fn();
|
|
3269
|
+
}
|
|
3270
|
+
return mcpAuthAls.run(
|
|
3271
|
+
{
|
|
3272
|
+
sessionId,
|
|
3273
|
+
turnId: active.turnId,
|
|
3274
|
+
callId: newToolCallId(toolName),
|
|
3275
|
+
toolName,
|
|
3276
|
+
emit: active.emit,
|
|
3277
|
+
},
|
|
3278
|
+
fn
|
|
3279
|
+
);
|
|
3280
|
+
}
|
|
3281
|
+
|
|
2834
3282
|
// ==========================================================================
|
|
2835
3283
|
// Direct tool calls (deterministic invocation, no model turn)
|
|
2836
3284
|
// ==========================================================================
|
|
@@ -2846,8 +3294,10 @@ export class SessionEngine {
|
|
|
2846
3294
|
* and materialized workspace) and the call is recorded on the session's
|
|
2847
3295
|
* event stream as `actions.requested` / `action.result` under a
|
|
2848
3296
|
* per-call `turnId` (the call id), so it shows up in trajectories.
|
|
2849
|
-
*
|
|
2850
|
-
*
|
|
3297
|
+
* While a model turn is running, a read-effect call runs alongside it
|
|
3298
|
+
* (the turn's workspace is read as-is) and a write-effect call is
|
|
3299
|
+
* rejected with {@link SessionBusyError}. Without a session, the tool
|
|
3300
|
+
* runs against an **ephemeral** scratch
|
|
2851
3301
|
* workspace under `<stateRoot>/tool-calls/<callId>` (materialized with
|
|
2852
3302
|
* seed files, removed after the call returns).
|
|
2853
3303
|
*
|
|
@@ -2873,6 +3323,12 @@ export class SessionEngine {
|
|
|
2873
3323
|
session: SessionInfo;
|
|
2874
3324
|
workspaceDir: string;
|
|
2875
3325
|
scratchDir: string | undefined;
|
|
3326
|
+
/**
|
|
3327
|
+
* A model turn is running or queued on the session. Reported, not
|
|
3328
|
+
* enforced: what it means depends on the call — a write waits, a read
|
|
3329
|
+
* (including the catalog) runs alongside the turn.
|
|
3330
|
+
*/
|
|
3331
|
+
busy: boolean;
|
|
2876
3332
|
}> {
|
|
2877
3333
|
if (
|
|
2878
3334
|
options.sessionId !== undefined &&
|
|
@@ -2898,9 +3354,9 @@ export class SessionEngine {
|
|
|
2898
3354
|
);
|
|
2899
3355
|
}
|
|
2900
3356
|
// A token that maps to a live (or restorable) session takes the
|
|
2901
|
-
// session path: same owner check, same
|
|
2902
|
-
//
|
|
2903
|
-
//
|
|
3357
|
+
// session path: same owner check, same busy semantics, same event
|
|
3358
|
+
// recording as an explicit sessionId. Skipping those just because
|
|
3359
|
+
// the caller addressed the session by token would fail open.
|
|
2904
3360
|
record =
|
|
2905
3361
|
(await this.sessions.findByContinuation(
|
|
2906
3362
|
continuation.channelId,
|
|
@@ -2915,19 +3371,14 @@ export class SessionEngine {
|
|
|
2915
3371
|
}
|
|
2916
3372
|
}
|
|
2917
3373
|
if (record !== undefined) {
|
|
2918
|
-
// Direct calls respect the same serialization as follow-up sends: a
|
|
2919
|
-
// running model turn owns the session workspace and event stream.
|
|
2920
|
-
if (
|
|
2921
|
-
this.activeTurns.has(record.sessionId) ||
|
|
2922
|
-
this.pendingTurns.has(record.sessionId)
|
|
2923
|
-
) {
|
|
2924
|
-
throw new SessionBusyError(record.sessionId);
|
|
2925
|
-
}
|
|
2926
3374
|
return {
|
|
2927
3375
|
record,
|
|
2928
3376
|
session: this.sessionInfo(record),
|
|
2929
3377
|
workspaceDir: this.harnessCwd(record),
|
|
2930
3378
|
scratchDir: undefined,
|
|
3379
|
+
busy:
|
|
3380
|
+
this.activeTurns.has(record.sessionId) ||
|
|
3381
|
+
this.pendingTurns.has(record.sessionId),
|
|
2931
3382
|
};
|
|
2932
3383
|
}
|
|
2933
3384
|
// No session behind the token (or no token at all): a synthetic
|
|
@@ -2947,6 +3398,7 @@ export class SessionEngine {
|
|
|
2947
3398
|
},
|
|
2948
3399
|
workspaceDir: scratchDir,
|
|
2949
3400
|
scratchDir,
|
|
3401
|
+
busy: false,
|
|
2950
3402
|
};
|
|
2951
3403
|
}
|
|
2952
3404
|
|
|
@@ -2989,6 +3441,43 @@ export class SessionEngine {
|
|
|
2989
3441
|
};
|
|
2990
3442
|
}
|
|
2991
3443
|
|
|
3444
|
+
/**
|
|
3445
|
+
* One catalog tool by name. Authored tools are local — their schema does
|
|
3446
|
+
* not depend on session identity or connection listings — so a detail
|
|
3447
|
+
* fetch for `ls` must not wait on every advertised MCP server.
|
|
3448
|
+
*/
|
|
3449
|
+
async describeCatalogTool(
|
|
3450
|
+
toolName: string,
|
|
3451
|
+
options: Pick<
|
|
3452
|
+
CallToolOptions,
|
|
3453
|
+
"auth" | "sessionId" | "continuationToken"
|
|
3454
|
+
> = {}
|
|
3455
|
+
): Promise<
|
|
3456
|
+
| { tool: DiscoveredTool }
|
|
3457
|
+
| {
|
|
3458
|
+
tool: undefined;
|
|
3459
|
+
availableTools: string[];
|
|
3460
|
+
connectionErrors: Array<{ connection: string; message: string }>;
|
|
3461
|
+
}
|
|
3462
|
+
> {
|
|
3463
|
+
const authored = this.project.agent.tools.find(
|
|
3464
|
+
(tool) => tool.name === toolName
|
|
3465
|
+
);
|
|
3466
|
+
if (authored !== undefined) {
|
|
3467
|
+
return { tool: authored };
|
|
3468
|
+
}
|
|
3469
|
+
const catalog = await this.listCatalogTools(options);
|
|
3470
|
+
const tool = catalog.tools.find((entry) => entry.name === toolName);
|
|
3471
|
+
if (tool === undefined) {
|
|
3472
|
+
return {
|
|
3473
|
+
tool: undefined,
|
|
3474
|
+
availableTools: catalog.tools.map((entry) => entry.name),
|
|
3475
|
+
connectionErrors: catalog.connectionErrors,
|
|
3476
|
+
};
|
|
3477
|
+
}
|
|
3478
|
+
return { tool };
|
|
3479
|
+
}
|
|
3480
|
+
|
|
2992
3481
|
async callTool(
|
|
2993
3482
|
toolName: string,
|
|
2994
3483
|
input: unknown = {},
|
|
@@ -3015,7 +3504,7 @@ export class SessionEngine {
|
|
|
3015
3504
|
const advertised = await resolveAdvertisedTools({
|
|
3016
3505
|
agent: this.project.agent,
|
|
3017
3506
|
session,
|
|
3018
|
-
hostMcp: this.
|
|
3507
|
+
hostMcp: this.advertiseMcp(),
|
|
3019
3508
|
});
|
|
3020
3509
|
tool = advertised.find((t) => t.name === toolName);
|
|
3021
3510
|
}
|
|
@@ -3035,6 +3524,19 @@ export class SessionEngine {
|
|
|
3035
3524
|
if (!validation.ok) {
|
|
3036
3525
|
throw new ToolInputError({ toolName, detail: validation.message });
|
|
3037
3526
|
}
|
|
3527
|
+
// A running model turn owns the session workspace: a direct write would
|
|
3528
|
+
// race the harness's own edits, so it waits (409). A read runs alongside
|
|
3529
|
+
// the turn — the operator inspecting a live session is the point — and
|
|
3530
|
+
// its events stay separable by their per-call turnId. The effect can
|
|
3531
|
+
// depend on the input, hence deciding after validation; an MCP tool
|
|
3532
|
+
// nothing classifies is a write here, fail-closed.
|
|
3533
|
+
if (
|
|
3534
|
+
identity.busy &&
|
|
3535
|
+
record !== undefined &&
|
|
3536
|
+
directCallEffect(tool, validation.value) !== "read"
|
|
3537
|
+
) {
|
|
3538
|
+
throw new SessionBusyError(record.sessionId);
|
|
3539
|
+
}
|
|
3038
3540
|
let workspaceUnavailable = false;
|
|
3039
3541
|
try {
|
|
3040
3542
|
await mkdir(workspaceDir, { recursive: true });
|
|
@@ -3053,35 +3555,59 @@ export class SessionEngine {
|
|
|
3053
3555
|
// to write) authored tool may rely on ctx.workspaceDir being the
|
|
3054
3556
|
// real session workspace, so silently relocating it is not safe —
|
|
3055
3557
|
// fail typed instead.
|
|
3558
|
+
//
|
|
3559
|
+
// A shared `local.workspaceDir` (change-monitor's phantom /repo) is
|
|
3560
|
+
// that path by design. Relocating it to scratch would flag a false
|
|
3561
|
+
// session-state warning; keep the borrowed cwd and skip the flag.
|
|
3056
3562
|
const relocatable =
|
|
3057
3563
|
tool.advertised === true ||
|
|
3058
|
-
|
|
3564
|
+
directCallEffect(tool, validation.value) === "read";
|
|
3059
3565
|
if (!relocatable) {
|
|
3060
3566
|
throw new WorkspaceUnavailableError(toolName, error);
|
|
3061
3567
|
}
|
|
3062
|
-
this.
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3568
|
+
if (this.project.agent.local?.workspaceDir !== undefined) {
|
|
3569
|
+
this.logger(
|
|
3570
|
+
`[session] tool ${toolName} harness cwd unavailable (direct ${callId}); keeping borrowed workspace`
|
|
3571
|
+
);
|
|
3572
|
+
} else {
|
|
3573
|
+
this.logger(
|
|
3574
|
+
`[session] tool ${toolName} workspace unavailable (direct ${callId}); using scratch: ${
|
|
3575
|
+
error instanceof Error ? error.message : String(error)
|
|
3576
|
+
}`
|
|
3577
|
+
);
|
|
3578
|
+
scratchDir = join(this.stateRoot, "tool-calls", callId);
|
|
3579
|
+
workspaceDir = scratchDir;
|
|
3580
|
+
workspaceUnavailable = true;
|
|
3581
|
+
await mkdir(workspaceDir, { recursive: true });
|
|
3582
|
+
}
|
|
3071
3583
|
}
|
|
3072
3584
|
// Match what a model turn would see: session workspaces get the same
|
|
3073
3585
|
// scaffold as executeTurn (seeds only before the first turn); scratch
|
|
3074
|
-
// workspaces are materialized fresh.
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3586
|
+
// workspaces are materialized fresh. A read running alongside a turn
|
|
3587
|
+
// that has materialized its workspace leaves it alone — re-seeding
|
|
3588
|
+
// would restore files the agent deleted mid-turn. A session is busy
|
|
3589
|
+
// from admit, though, so a read that lands while the first turn is
|
|
3590
|
+
// still queued or scaffolding materializes the workspace itself, seeds
|
|
3591
|
+
// included (turnCount is already 1 for that pending first turn).
|
|
3592
|
+
const turnMaterialized =
|
|
3593
|
+
record !== undefined &&
|
|
3594
|
+
identity.busy &&
|
|
3595
|
+
this.materializedWorkspaces.has(record.sessionId);
|
|
3596
|
+
if (record === undefined || workspaceUnavailable || !turnMaterialized) {
|
|
3597
|
+
await materializeWorkspace({
|
|
3598
|
+
agent: this.project.agent,
|
|
3599
|
+
workspaceDir:
|
|
3600
|
+
record === undefined || workspaceUnavailable
|
|
3601
|
+
? workspaceDir
|
|
3602
|
+
: this.scaffoldDir(record),
|
|
3603
|
+
seed:
|
|
3604
|
+
record === undefined ||
|
|
3605
|
+
workspaceUnavailable ||
|
|
3606
|
+
record.turnCount === 0 ||
|
|
3607
|
+
(identity.busy && record.turnCount === 1),
|
|
3608
|
+
stateRoot: this.stateRoot,
|
|
3609
|
+
});
|
|
3610
|
+
}
|
|
3085
3611
|
|
|
3086
3612
|
// A per-call turnId keeps these events visible in trajectories (which
|
|
3087
3613
|
// drop turn-less action events) without attributing them to a model turn.
|
|
@@ -3104,6 +3630,8 @@ export class SessionEngine {
|
|
|
3104
3630
|
: {}),
|
|
3105
3631
|
dryRunResult: tool.definition.dryRunResult,
|
|
3106
3632
|
input: validation.value,
|
|
3633
|
+
asOf: record?.asOf,
|
|
3634
|
+
timeArgs: tool.definition.timeArgs,
|
|
3107
3635
|
});
|
|
3108
3636
|
|
|
3109
3637
|
const started = Date.now();
|
|
@@ -3122,7 +3650,13 @@ export class SessionEngine {
|
|
|
3122
3650
|
}).catch(() => {});
|
|
3123
3651
|
};
|
|
3124
3652
|
try {
|
|
3125
|
-
if (decision.kind === "
|
|
3653
|
+
if (decision.kind === "unbounded") {
|
|
3654
|
+
this.logger(
|
|
3655
|
+
`[session] tool ${toolName} refused, unbounded_time (direct ${callId})`
|
|
3656
|
+
);
|
|
3657
|
+
result = normalizeToolResult(toolErrorResult(decision.message));
|
|
3658
|
+
isError = true;
|
|
3659
|
+
} else if (decision.kind === "refuse") {
|
|
3126
3660
|
this.logger(
|
|
3127
3661
|
`[session] tool ${toolName} refused, effect unclassified (direct ${callId})`
|
|
3128
3662
|
);
|
|
@@ -3139,8 +3673,8 @@ export class SessionEngine {
|
|
|
3139
3673
|
result = normalizeToolResult(decision.result);
|
|
3140
3674
|
isError = false;
|
|
3141
3675
|
} else {
|
|
3142
|
-
|
|
3143
|
-
|
|
3676
|
+
const run = () =>
|
|
3677
|
+
execute(validation.value as never, {
|
|
3144
3678
|
toolCallId: callId,
|
|
3145
3679
|
session,
|
|
3146
3680
|
workspaceDir,
|
|
@@ -3165,7 +3699,21 @@ export class SessionEngine {
|
|
|
3165
3699
|
getSession: (channelId, sessionId) =>
|
|
3166
3700
|
this.getChannelSession(channelId, sessionId),
|
|
3167
3701
|
emit,
|
|
3168
|
-
|
|
3702
|
+
now: sessionClock(record?.asOf),
|
|
3703
|
+
});
|
|
3704
|
+
result = normalizeToolResult(
|
|
3705
|
+
record !== undefined && emit !== undefined
|
|
3706
|
+
? await mcpAuthAls.run(
|
|
3707
|
+
{
|
|
3708
|
+
sessionId: record.sessionId,
|
|
3709
|
+
turnId: callId,
|
|
3710
|
+
callId,
|
|
3711
|
+
toolName,
|
|
3712
|
+
emit,
|
|
3713
|
+
},
|
|
3714
|
+
run
|
|
3715
|
+
)
|
|
3716
|
+
: await run()
|
|
3169
3717
|
);
|
|
3170
3718
|
isError = isErrorEnvelope(result);
|
|
3171
3719
|
}
|
|
@@ -3263,6 +3811,12 @@ export class SessionEngine {
|
|
|
3263
3811
|
if (decision.kind === "run") {
|
|
3264
3812
|
return { answered: false };
|
|
3265
3813
|
}
|
|
3814
|
+
if (decision.kind === "unbounded") {
|
|
3815
|
+
this.logger(
|
|
3816
|
+
`[session] bridged tool ${args.toolName} refused, unbounded_time (${args.sessionId})`
|
|
3817
|
+
);
|
|
3818
|
+
return { answered: true, result: toolErrorResult(decision.message) };
|
|
3819
|
+
}
|
|
3266
3820
|
if (decision.kind === "refuse") {
|
|
3267
3821
|
this.logger(
|
|
3268
3822
|
`[session] bridged tool ${args.toolName} refused, effect unclassified (${args.sessionId})`
|
|
@@ -3585,6 +4139,75 @@ export class SessionEngine {
|
|
|
3585
4139
|
};
|
|
3586
4140
|
}
|
|
3587
4141
|
|
|
4142
|
+
/**
|
|
4143
|
+
* The single newest session visible to `caller` (`GET /v1/sessions?latest=1`).
|
|
4144
|
+
*
|
|
4145
|
+
* Short-circuits the full listing: the local store already holds every
|
|
4146
|
+
* session this host created plus the startup bulk restore, so the common
|
|
4147
|
+
* case answers from memory without a sink round trip. Only when the local
|
|
4148
|
+
* store has nothing for the caller (fresh host, `restore: "off"`) does it
|
|
4149
|
+
* ask the sink — for a bounded newest-written window when the backend
|
|
4150
|
+
* can order by write time, else the full list. A session written by
|
|
4151
|
+
* another replica moments ago can lose to a local one; callers that need
|
|
4152
|
+
* the exact newest row should use the full listing.
|
|
4153
|
+
*/
|
|
4154
|
+
async latestSessionWithSource(
|
|
4155
|
+
caller: AuthContext | null,
|
|
4156
|
+
options?: { includeAll?: boolean }
|
|
4157
|
+
): Promise<{ sessions: SessionSummary[]; source: SessionListSource }> {
|
|
4158
|
+
const includeAll = options?.includeAll ?? false;
|
|
4159
|
+
const newestVisible = (
|
|
4160
|
+
records: readonly SessionRecord[]
|
|
4161
|
+
): SessionRecord | undefined => {
|
|
4162
|
+
let newest: SessionRecord | undefined;
|
|
4163
|
+
for (const record of records) {
|
|
4164
|
+
if (!(includeAll || samePrincipal(record.auth, caller))) {
|
|
4165
|
+
continue;
|
|
4166
|
+
}
|
|
4167
|
+
if (
|
|
4168
|
+
newest === undefined ||
|
|
4169
|
+
record.updatedAt.localeCompare(newest.updatedAt) > 0
|
|
4170
|
+
) {
|
|
4171
|
+
newest = record;
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
return newest;
|
|
4175
|
+
};
|
|
4176
|
+
const local = newestVisible(await this.sessions.list());
|
|
4177
|
+
if (local !== undefined) {
|
|
4178
|
+
return {
|
|
4179
|
+
sessions: this.summarizeSessionRecords([local], caller, includeAll),
|
|
4180
|
+
source: "local",
|
|
4181
|
+
};
|
|
4182
|
+
}
|
|
4183
|
+
if (this.storage?.isCursorHosted === true) {
|
|
4184
|
+
try {
|
|
4185
|
+
const window = await this.storage.listNewestSessionsFromSink(
|
|
4186
|
+
LATEST_SESSION_SINK_WINDOW
|
|
4187
|
+
);
|
|
4188
|
+
if (window !== undefined) {
|
|
4189
|
+
const stored = newestVisible(window);
|
|
4190
|
+
if (stored !== undefined) {
|
|
4191
|
+
return {
|
|
4192
|
+
sessions: this.summarizeSessionRecords(
|
|
4193
|
+
[stored],
|
|
4194
|
+
caller,
|
|
4195
|
+
includeAll
|
|
4196
|
+
),
|
|
4197
|
+
source: "cursor-hosted",
|
|
4198
|
+
};
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
} catch (error) {
|
|
4202
|
+
this.logger(
|
|
4203
|
+
`[agent-sdk] cursor-hosted newest-session lookup failed; falling back to the full list: ${describeError(error)}`
|
|
4204
|
+
);
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
const listed = await this.listSessionsWithSource(caller, options);
|
|
4208
|
+
return { sessions: listed.sessions.slice(0, 1), source: listed.source };
|
|
4209
|
+
}
|
|
4210
|
+
|
|
3588
4211
|
/**
|
|
3589
4212
|
* Durable sink rows plus local records (local wins on the same id when
|
|
3590
4213
|
* newer — covers debounce windows that have not flushed yet). Newest
|
|
@@ -3638,6 +4261,7 @@ export class SessionEngine {
|
|
|
3638
4261
|
: undefined,
|
|
3639
4262
|
createdAt: record.createdAt,
|
|
3640
4263
|
updatedAt: record.updatedAt,
|
|
4264
|
+
dryRun: record.dryRun === true ? true : undefined,
|
|
3641
4265
|
continuationToken:
|
|
3642
4266
|
record.httpContinuationKey !== undefined
|
|
3643
4267
|
? formatContinuationToken(
|
|
@@ -3967,7 +4591,8 @@ export class SessionEngine {
|
|
|
3967
4591
|
channelId: record.channelId,
|
|
3968
4592
|
mode: record.mode,
|
|
3969
4593
|
purpose: record.purpose === "eval" ? "eval" : "live",
|
|
3970
|
-
|
|
4594
|
+
dryRun: record.dryRun === true ? true : undefined,
|
|
4595
|
+
asOf: record.asOf,
|
|
3971
4596
|
auth: record.auth,
|
|
3972
4597
|
continuationKey: record.continuationKey,
|
|
3973
4598
|
...(record.abs === undefined ? {} : { abs: record.abs }),
|
|
@@ -4340,6 +4965,8 @@ export class SessionEngine {
|
|
|
4340
4965
|
async close(): Promise<void> {
|
|
4341
4966
|
this.stopMemoryTelemetry?.();
|
|
4342
4967
|
this.approvals.cancelAll("deny");
|
|
4968
|
+
this.auths.cancelAll("deny");
|
|
4969
|
+
await this.oauthHub.close();
|
|
4343
4970
|
await Promise.allSettled([...this.activeTurns.values()]);
|
|
4344
4971
|
await Promise.allSettled([...this.backgroundWork]);
|
|
4345
4972
|
await Promise.allSettled([...this.dispatchChains.values()]);
|
|
@@ -4357,6 +4984,13 @@ export class SessionEngine {
|
|
|
4357
4984
|
/** Parallel event-stream loads during the startup bulk restore. */
|
|
4358
4985
|
const RESTORE_FETCH_CONCURRENCY = 8;
|
|
4359
4986
|
|
|
4987
|
+
/**
|
|
4988
|
+
* Newest-written sink rows fetched for `GET /v1/sessions?latest=1` when the
|
|
4989
|
+
* local store has nothing for the caller. Wide enough that the caller's own
|
|
4990
|
+
* session is normally inside it even when other principals are busier.
|
|
4991
|
+
*/
|
|
4992
|
+
const LATEST_SESSION_SINK_WINDOW = 50;
|
|
4993
|
+
|
|
4360
4994
|
/** NDJSON payload for a restored event stream; undefined when empty. */
|
|
4361
4995
|
function mergePendingImages(
|
|
4362
4996
|
current: Array<{ data: string; mimeType: string }> | undefined,
|