@cursor/july 0.1.88 → 0.1.90
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/README.md +95 -94
- package/dist/ab.d.ts +1 -1
- package/dist/ab.js +1 -1
- package/dist/artifacts.d.ts +1 -1
- package/dist/artifacts.js +1 -1
- package/dist/bin/agent-serve.js +21 -8
- package/dist/channels.d.ts +1 -1
- package/dist/channels.js +1 -1
- package/dist/connections.d.ts +1 -1
- package/dist/connections.js +1 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.DUOPbN18.js → app.wiNkt6G7.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.FV0R6kOb.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.CmWbGcGk.js → VPLocalSearchBox.ByhUGu47.js} +1 -1
- package/dist/docs/assets/chunks/{theme.S57OeOLA.js → theme.Dx7j_-0n.js} +2 -2
- package/dist/docs/assets/{concepts.md.BXAm6G-C.js → concepts.md.2NfkGjqM.js} +1 -1
- package/dist/docs/assets/{reference_project-layout.md.D3MdHM2z.js → reference_project-layout.md.CueaKpjr.js} +5 -20
- package/dist/docs/assets/reference_project-layout.md.CueaKpjr.lean.js +1 -0
- package/dist/docs/assets/{reference_tools.md.DTg_kEsx.js → reference_tools.md.XmeFP_3d.js} +23 -2
- package/dist/docs/assets/{reference_tools.md.DTg_kEsx.lean.js → reference_tools.md.XmeFP_3d.lean.js} +1 -1
- package/dist/docs/assets/{scaffolding-agents.md.CiGsJ1aw.js → scaffolding-agents.md.BpMFXv2J.js} +1 -1
- package/dist/docs/building-with-agents.html +3 -3
- package/dist/docs/concepts.html +5 -5
- package/dist/docs/deployment.html +3 -3
- package/dist/docs/evals.html +3 -3
- package/dist/docs/example-agents/approval-buddy.html +3 -3
- package/dist/docs/example-agents/benny.html +3 -3
- package/dist/docs/example-agents/bugbot.html +3 -3
- package/dist/docs/example-agents/codebase-wiki.html +3 -3
- package/dist/docs/example-agents/codeowners-review.html +3 -3
- package/dist/docs/example-agents/concierge.html +3 -3
- package/dist/docs/example-agents/fsd.html +3 -3
- package/dist/docs/example-agents/index.html +3 -3
- package/dist/docs/example-agents/knowledge-base.html +3 -3
- package/dist/docs/example-agents/oncall.html +3 -3
- package/dist/docs/example-agents/security-reviewer.html +3 -3
- package/dist/docs/example-agents/slack-agent.html +3 -3
- package/dist/docs/example-agents/weather-agent.html +3 -3
- package/dist/docs/guides/agent-to-agent.html +3 -3
- package/dist/docs/guides/cloud-runtime.html +3 -3
- package/dist/docs/guides/convert-automation.html +3 -3
- package/dist/docs/guides/github.html +3 -3
- package/dist/docs/guides/human-in-the-loop.html +3 -3
- package/dist/docs/guides/mcp-oauth.html +3 -3
- package/dist/docs/guides/opentelemetry.html +3 -3
- package/dist/docs/guides/slack.html +3 -3
- package/dist/docs/guides/webhooks.html +3 -3
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +3 -3
- package/dist/docs/index.html +3 -3
- package/dist/docs/quickstart.html +3 -3
- package/dist/docs/reference/agent-config.html +3 -3
- package/dist/docs/reference/artifacts.html +3 -3
- package/dist/docs/reference/channels.html +3 -3
- package/dist/docs/reference/cli.html +3 -3
- package/dist/docs/reference/connections.html +3 -3
- package/dist/docs/reference/hooks.html +3 -3
- package/dist/docs/reference/http-api.html +3 -3
- package/dist/docs/reference/instructions.html +3 -3
- package/dist/docs/reference/playground.html +3 -3
- package/dist/docs/reference/project-layout.html +8 -23
- package/dist/docs/reference/prompt.html +3 -3
- package/dist/docs/reference/schedules.html +3 -3
- package/dist/docs/reference/sessions.html +3 -3
- package/dist/docs/reference/skills.html +3 -3
- package/dist/docs/reference/subagents.html +3 -3
- package/dist/docs/reference/tools.html +27 -6
- package/dist/docs/scaffolding-agents.html +5 -5
- package/dist/docs/storage.html +3 -3
- package/dist/docs/templates/agentic-owners.html +3 -3
- package/dist/docs/templates/demo.html +3 -3
- package/dist/docs/templates/pr-autofixer.html +3 -3
- package/dist/docs/templates/security-reviewer.html +3 -3
- package/dist/docs/templates/triage.html +3 -3
- package/dist/docs/troubleshooting.html +3 -3
- package/dist/evals.d.ts +1 -1
- package/dist/evals.js +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/internal/advertise-tools.d.ts.map +1 -1
- package/dist/internal/advertise-tools.js +1 -1
- package/dist/internal/cli-ax.js +1 -1
- package/dist/internal/cli-convert-automation.d.ts +1 -1
- package/dist/internal/cli-convert-automation.js +1 -1
- package/dist/internal/cli-cursor.d.ts +12 -4
- package/dist/internal/cli-cursor.d.ts.map +1 -1
- package/dist/internal/cli-cursor.js +11 -4
- package/dist/internal/cli-deploy.d.ts +3 -2
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +84 -9
- package/dist/internal/convert-automation/convert-workflow.d.ts +1 -1
- package/dist/internal/convert-automation/convert-workflow.js +9 -9
- package/dist/internal/convert-automation/types.d.ts +1 -1
- package/dist/internal/convert-automation/types.js +1 -1
- package/dist/internal/cursor/account-mcp.d.ts.map +1 -1
- package/dist/internal/cursor/account-mcp.js +8 -4
- package/dist/internal/cursor/backend-client.d.ts +17 -1
- package/dist/internal/cursor/backend-client.d.ts.map +1 -1
- package/dist/internal/cursor/backend-client.js +42 -7
- package/dist/internal/cursor/credentials.d.ts +35 -9
- package/dist/internal/cursor/credentials.d.ts.map +1 -1
- package/dist/internal/cursor/credentials.js +92 -39
- package/dist/internal/deploy-client.d.ts +42 -1
- package/dist/internal/deploy-client.d.ts.map +1 -1
- package/dist/internal/deploy-client.js +70 -2
- package/dist/internal/discovery.js +1 -1
- package/dist/internal/distribution.d.ts +18 -0
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +19 -0
- package/dist/internal/eval-runner.js +1 -1
- package/dist/internal/grokbot/runner.d.ts +5 -2
- package/dist/internal/grokbot/runner.d.ts.map +1 -1
- package/dist/internal/grokbot/runner.js +10 -6
- package/dist/internal/init-project.d.ts.map +1 -1
- package/dist/internal/init-project.js +8 -1
- package/dist/internal/mcp-endpoint.js +2 -1
- package/dist/internal/sdk-runner.d.ts +5 -0
- package/dist/internal/sdk-runner.d.ts.map +1 -1
- package/dist/internal/sdk-runner.js +10 -5
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +80 -36
- package/dist/internal/session-engine.d.ts +9 -2
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +85 -28
- package/dist/internal/tool-policy.d.ts +33 -1
- package/dist/internal/tool-policy.d.ts.map +1 -1
- package/dist/internal/tool-policy.js +53 -0
- package/dist/playground/assets/index-BpVS-paP.css +1 -0
- package/dist/playground/assets/{index-Bqn91tW4.js → index-DxapiDj_.js} +45 -45
- package/dist/playground/index.html +2 -2
- package/dist/reminders.d.ts +1 -1
- package/dist/reminders.js +1 -1
- package/dist/schedules.d.ts +1 -1
- package/dist/schedules.js +1 -1
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +1 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +1 -1
- package/dist/tools.d.ts +1 -1
- package/dist/tools.js +1 -1
- package/dist/types.d.ts +24 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/docs/concepts.md +2 -1
- package/docs/reference/project-layout.md +5 -20
- package/docs/reference/tools.md +53 -3
- package/docs/scaffolding-agents.md +2 -0
- package/package.json +2 -1
- package/skills/debug/SKILL.md +1 -2
- package/skills/framework-map/SKILL.md +1 -2
- package/src/ab.ts +1 -1
- package/src/artifacts.ts +1 -1
- package/src/bin/agent-serve.ts +21 -7
- package/src/channels.ts +1 -1
- package/src/connections.ts +1 -1
- package/src/evals.ts +1 -1
- package/src/hooks.ts +1 -1
- package/src/index.ts +2 -2
- package/src/internal/advertise-tools.ts +1 -0
- package/src/internal/cli-ax.ts +1 -1
- package/src/internal/cli-convert-automation.ts +1 -1
- package/src/internal/cli-cursor.ts +18 -5
- package/src/internal/cli-deploy.ts +123 -15
- package/src/internal/convert-automation/convert-workflow.ts +9 -9
- package/src/internal/convert-automation/types.ts +1 -1
- package/src/internal/cursor/account-mcp.ts +15 -1
- package/src/internal/cursor/backend-client.ts +62 -4
- package/src/internal/cursor/credentials.ts +87 -24
- package/src/internal/deploy-client.ts +122 -2
- package/src/internal/discovery.ts +1 -1
- package/src/internal/distribution.ts +31 -0
- package/src/internal/eval-runner.ts +1 -1
- package/src/internal/grokbot/runner.ts +15 -6
- package/src/internal/init-project.ts +11 -1
- package/src/internal/mcp-endpoint.ts +2 -1
- package/src/internal/sdk-runner.ts +10 -4
- package/src/internal/server.ts +95 -33
- package/src/internal/session-engine.ts +86 -6
- package/src/internal/tool-policy.ts +80 -1
- package/src/reminders.ts +1 -1
- package/src/schedules.ts +1 -1
- package/src/skills.ts +1 -1
- package/src/storage.ts +1 -1
- package/src/tools.ts +1 -1
- package/src/types.ts +21 -2
- package/dist/docs/assets/chunks/@localSearchIndexroot.CxCtxfDE.js +0 -1
- package/dist/docs/assets/reference_project-layout.md.D3MdHM2z.lean.js +0 -1
- package/dist/playground/assets/index-C0_5hOsf.css +0 -1
- /package/dist/docs/assets/{concepts.md.BXAm6G-C.lean.js → concepts.md.2NfkGjqM.lean.js} +0 -0
- /package/dist/docs/assets/{scaffolding-agents.md.CiGsJ1aw.lean.js → scaffolding-agents.md.BpMFXv2J.lean.js} +0 -0
package/src/internal/server.ts
CHANGED
|
@@ -64,9 +64,11 @@ import {
|
|
|
64
64
|
} from "./cursor/account-mcp.js";
|
|
65
65
|
import { CursorBackendClient } from "./cursor/backend-client.js";
|
|
66
66
|
import {
|
|
67
|
+
cursorBackendUrl,
|
|
67
68
|
cursorExternalApiUrl,
|
|
68
69
|
type ResolvedApiKey,
|
|
69
70
|
resolveApiKey,
|
|
71
|
+
resolveApiKeySync,
|
|
70
72
|
SIGN_IN_HINT,
|
|
71
73
|
} from "./cursor/credentials.js";
|
|
72
74
|
import { CursorGithubCredentialProvider } from "./cursor/github-credentials.js";
|
|
@@ -281,9 +283,9 @@ export async function startServer(
|
|
|
281
283
|
const runSchedules = options.schedules ?? !dev;
|
|
282
284
|
const runReminders = options.reminders ?? !dev;
|
|
283
285
|
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
// agentkit_enabled gate
|
|
286
|
+
// CURSOR_AGENTKIT_VERSION is still the env the SDK reads when building
|
|
287
|
+
// its client-version header (wire prefix remains `agentkit-`). Setting it
|
|
288
|
+
// here makes the internal-only agentkit_enabled gate apply to every turn.
|
|
287
289
|
process.env.CURSOR_AGENTKIT_VERSION ??= packageVersion();
|
|
288
290
|
|
|
289
291
|
const authoredOtel = pickAuthoredOtel(
|
|
@@ -328,10 +330,18 @@ export async function startServer(
|
|
|
328
330
|
|
|
329
331
|
// One resolution for every Cursor credential consumer (SDK runner, account
|
|
330
332
|
// MCP bridge, SCM event relay, GitHub credentials): explicit option →
|
|
331
|
-
// CURSOR_API_KEY →
|
|
333
|
+
// CURSOR_API_KEY → CURSOR_API_KEY_FILE (or the hosted default path) →
|
|
334
|
+
// stored `agent-serve login`. SDK / GitHub / account MCP re-read later;
|
|
335
|
+
// Slack and deployment-event relays capture the key at start.
|
|
332
336
|
const accountKey = await resolveApiKey(
|
|
333
337
|
options.apiKey === undefined ? {} : { apiKey: options.apiKey }
|
|
334
338
|
);
|
|
339
|
+
// First present source wins (we define the order): option → env → file →
|
|
340
|
+
// stored login. Re-read so bind can drop env or file onto a listening process.
|
|
341
|
+
const liveCursorApiKey = (): string | undefined =>
|
|
342
|
+
resolveApiKeySync(
|
|
343
|
+
options.apiKey === undefined ? {} : { apiKey: options.apiKey }
|
|
344
|
+
)?.apiKey;
|
|
335
345
|
|
|
336
346
|
// The SCM relay reads the event stream as the signed-in user, so
|
|
337
347
|
// `cursorEvents` cannot work signed out. Require the credential before any
|
|
@@ -365,15 +375,15 @@ export async function startServer(
|
|
|
365
375
|
const needsGithubAuth =
|
|
366
376
|
cursorGithubConfig.scope !== undefined || githubProxyRequested;
|
|
367
377
|
if (needsGithubAuth) {
|
|
368
|
-
const
|
|
369
|
-
cursorEventsAuth
|
|
370
|
-
(
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if (
|
|
378
|
+
const currentGithubApiKey = (): string | undefined => {
|
|
379
|
+
const fromEvents = cursorEventsAuth?.apiKey.trim();
|
|
380
|
+
if (fromEvents !== undefined && fromEvents !== "") {
|
|
381
|
+
return fromEvents;
|
|
382
|
+
}
|
|
383
|
+
return liveCursorApiKey();
|
|
384
|
+
};
|
|
385
|
+
const githubApiKeyNow = currentGithubApiKey();
|
|
386
|
+
if (githubApiKeyNow === undefined && options.allowAnonymous !== true) {
|
|
377
387
|
throw new Error(
|
|
378
388
|
"githubChannel({ cursorAccount: true }) could not resolve Cursor event authentication."
|
|
379
389
|
);
|
|
@@ -387,20 +397,39 @@ export async function startServer(
|
|
|
387
397
|
logger(
|
|
388
398
|
"[agent-sdk] warning: --allow-anonymous-cursor-github — every admitted HTTP caller can drive sessions holding the signed-in account's repo-scoped GitHub credential; serve this only behind a trusted boundary (e.g. an SSO proxy)"
|
|
389
399
|
);
|
|
400
|
+
if (githubApiKeyNow === undefined) {
|
|
401
|
+
logger(
|
|
402
|
+
"[agent-sdk] GitHub auth: listening unsigned; credentials bind at task via CURSOR_API_KEY or CURSOR_API_KEY_FILE"
|
|
403
|
+
);
|
|
404
|
+
}
|
|
390
405
|
}
|
|
391
406
|
if (cursorGithubConfig.permissions === "contents-write") {
|
|
392
407
|
logger(
|
|
393
408
|
'[agent-sdk] warning: Cursor GitHub credentials include contents:write — the host holds a push-capable token shared with model-driven tools and untrusted webhook content; prefer permissions: "pr-write" unless this agent must push code'
|
|
394
409
|
);
|
|
395
410
|
}
|
|
411
|
+
const githubAuthBaseUrl = cursorExternalApiUrl();
|
|
412
|
+
const requireGithubApiKey = async (): Promise<string> => {
|
|
413
|
+
const apiKey = currentGithubApiKey();
|
|
414
|
+
if (apiKey === undefined) {
|
|
415
|
+
throw new Error(
|
|
416
|
+
`githubChannel({ cursorAccount: true }) has no Cursor credential yet. ${SIGN_IN_HINT}`
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
return apiKey;
|
|
420
|
+
};
|
|
396
421
|
const githubProxyUrl = githubProxyRequested
|
|
397
|
-
? resolveCursorGithubProxyUrl(
|
|
422
|
+
? resolveCursorGithubProxyUrl(githubAuthBaseUrl)
|
|
398
423
|
: undefined;
|
|
399
|
-
if (
|
|
424
|
+
if (
|
|
425
|
+
cursorGithubConfig.scope !== undefined &&
|
|
426
|
+
githubApiKeyNow !== undefined
|
|
427
|
+
) {
|
|
400
428
|
const provider = new CursorGithubCredentialProvider({
|
|
401
429
|
client: new CursorBackendClient({
|
|
402
|
-
backendUrl:
|
|
403
|
-
apiKey:
|
|
430
|
+
backendUrl: githubAuthBaseUrl,
|
|
431
|
+
apiKey: githubApiKeyNow,
|
|
432
|
+
getApiKey: () => currentGithubApiKey() ?? "",
|
|
404
433
|
logger,
|
|
405
434
|
}),
|
|
406
435
|
scope: cursorGithubConfig.scope,
|
|
@@ -420,9 +449,7 @@ export async function startServer(
|
|
|
420
449
|
},
|
|
421
450
|
};
|
|
422
451
|
for (const target of cursorGithubTargets) {
|
|
423
|
-
target.accountRuntime.credentials.bind(
|
|
424
|
-
async () => cursorGithubAuth.apiKey
|
|
425
|
-
);
|
|
452
|
+
target.accountRuntime.credentials.bind(requireGithubApiKey);
|
|
426
453
|
target.accountRuntime.credentials.bindProxyTransport(
|
|
427
454
|
proxyTransport
|
|
428
455
|
);
|
|
@@ -438,8 +465,8 @@ export async function startServer(
|
|
|
438
465
|
} else if (githubProxyUrl !== undefined) {
|
|
439
466
|
// `cursorAccount: true` with no `--repo`: skip the startup token mint
|
|
440
467
|
// (it requires a repo list) and let github-proxy take the repo from
|
|
441
|
-
// each Octokit call. Hosted
|
|
442
|
-
//
|
|
468
|
+
// each Octokit call. Hosted freeze bakes this flag into the template
|
|
469
|
+
// so restore is GitHub-ready once Bind drops the key.
|
|
443
470
|
const proxyTransport = {
|
|
444
471
|
apiBaseUrl: githubProxyUrl,
|
|
445
472
|
headers: {
|
|
@@ -447,16 +474,14 @@ export async function startServer(
|
|
|
447
474
|
},
|
|
448
475
|
};
|
|
449
476
|
for (const target of cursorGithubTargets) {
|
|
450
|
-
target.accountRuntime.credentials.bind(
|
|
451
|
-
async () => cursorGithubAuth.apiKey
|
|
452
|
-
);
|
|
477
|
+
target.accountRuntime.credentials.bind(requireGithubApiKey);
|
|
453
478
|
target.accountRuntime.credentials.bindProxyTransport(proxyTransport);
|
|
454
479
|
}
|
|
455
480
|
// `ctx.host.github` gets the same proxy transport; without this it
|
|
456
481
|
// would fall back to ambient auth (env token / `gh`) or an
|
|
457
482
|
// unauthenticated Octokit, and every host GitHub call would 404.
|
|
458
483
|
hostGithubProxyResolver = createCursorGithubProxyHostResolver({
|
|
459
|
-
getApiKey:
|
|
484
|
+
getApiKey: requireGithubApiKey,
|
|
460
485
|
transport: proxyTransport,
|
|
461
486
|
});
|
|
462
487
|
logger(
|
|
@@ -496,7 +521,12 @@ export async function startServer(
|
|
|
496
521
|
}
|
|
497
522
|
const sdkRunner =
|
|
498
523
|
options.runner ??
|
|
499
|
-
new CursorSdkRunner({
|
|
524
|
+
new CursorSdkRunner({
|
|
525
|
+
...(options.apiKey === undefined || options.apiKey.trim() === ""
|
|
526
|
+
? {}
|
|
527
|
+
: { apiKey: options.apiKey }),
|
|
528
|
+
logger,
|
|
529
|
+
});
|
|
500
530
|
// Grok Bot turns run on Cursor's hosted Grok Bot (Sand) harness through
|
|
501
531
|
// the /v0/grokbot session API; the SDK side is a thin remote client. The
|
|
502
532
|
// dispatching runner is only interposed when a mount needs it. An
|
|
@@ -521,7 +551,16 @@ export async function startServer(
|
|
|
521
551
|
if (hasGrokBotMounts && options.runner === undefined) {
|
|
522
552
|
runner = new RuntimeDispatchingRunner({
|
|
523
553
|
sdkRunner,
|
|
524
|
-
grokbot: {
|
|
554
|
+
grokbot: {
|
|
555
|
+
...(options.apiKey === undefined || options.apiKey.trim() === ""
|
|
556
|
+
? {}
|
|
557
|
+
: { apiKey: options.apiKey }),
|
|
558
|
+
// `/v0/grokbot` lives on the external API (api.cursor.com), not
|
|
559
|
+
// api2. Prefer that default over the account key's backendUrl.
|
|
560
|
+
// Do not pass stored login as apiKey: that would pin it as an
|
|
561
|
+
// option and beat a later CURSOR_API_KEY_FILE bind.
|
|
562
|
+
backendUrl: cursorExternalApiUrl(),
|
|
563
|
+
},
|
|
525
564
|
});
|
|
526
565
|
logger(
|
|
527
566
|
"[agent-sdk] grokbot runtime: remote client (turns run on the hosted Grok Bot harness)"
|
|
@@ -559,15 +598,21 @@ export async function startServer(
|
|
|
559
598
|
}
|
|
560
599
|
let accountBridge: CursorAccountMcpBridge | undefined;
|
|
561
600
|
if (accountRefsBySlug.size > 0) {
|
|
562
|
-
if (accountKey === undefined) {
|
|
601
|
+
if (accountKey === undefined && options.allowAnonymous !== true) {
|
|
563
602
|
throw new Error(
|
|
564
603
|
`This project has Cursor account MCP connections (defineConnection({ cursorAccount: true })) but the host is not signed in to Cursor. ${SIGN_IN_HINT}`
|
|
565
604
|
);
|
|
566
605
|
}
|
|
606
|
+
if (accountKey === undefined) {
|
|
607
|
+
logger(
|
|
608
|
+
"[agent-sdk] Cursor account MCP: listening unsigned; credentials bind at task via CURSOR_API_KEY or CURSOR_API_KEY_FILE"
|
|
609
|
+
);
|
|
610
|
+
}
|
|
567
611
|
accountBridge = new CursorAccountMcpBridge({
|
|
568
612
|
client: new CursorBackendClient({
|
|
569
|
-
backendUrl: accountKey
|
|
570
|
-
apiKey: accountKey
|
|
613
|
+
backendUrl: accountKey?.backendUrl ?? cursorBackendUrl(),
|
|
614
|
+
apiKey: accountKey?.apiKey ?? "",
|
|
615
|
+
getApiKey: () => liveCursorApiKey() ?? "",
|
|
571
616
|
logger,
|
|
572
617
|
}),
|
|
573
618
|
logger,
|
|
@@ -1035,8 +1080,13 @@ export async function startServer(
|
|
|
1035
1080
|
? undefined
|
|
1036
1081
|
: { authorization: `Bearer ${options.authToken}` };
|
|
1037
1082
|
if (options.cloudToolsUrl !== undefined && headers === undefined) {
|
|
1038
|
-
|
|
1039
|
-
|
|
1083
|
+
if (options.allowAnonymous !== true) {
|
|
1084
|
+
throw new Error(
|
|
1085
|
+
`Agent "${runtime.project.name}" needs a Cursor API key to authenticate --cloud-tools-url. ${SIGN_IN_HINT}`
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
logger(
|
|
1089
|
+
`[agent-sdk] --cloud-tools-url: listening unsigned; Bearer is taken from the key present at start`
|
|
1040
1090
|
);
|
|
1041
1091
|
}
|
|
1042
1092
|
runtime.engine.setCloudServerToolsConnection({
|
|
@@ -2509,6 +2559,12 @@ function startCursorDeploymentRelay(input: {
|
|
|
2509
2559
|
return undefined;
|
|
2510
2560
|
}
|
|
2511
2561
|
if (accountKey === undefined) {
|
|
2562
|
+
if (input.options.allowAnonymous === true) {
|
|
2563
|
+
logger(
|
|
2564
|
+
"[agent-sdk] deployment-events: skipped while unsigned; long-poll relays need a key at start"
|
|
2565
|
+
);
|
|
2566
|
+
return undefined;
|
|
2567
|
+
}
|
|
2512
2568
|
throw new Error(
|
|
2513
2569
|
`This project has channels subscribed to deployment events (deploymentsChannel({ deploySourceUris })) but the host is not signed in to Cursor. ${SIGN_IN_HINT}`
|
|
2514
2570
|
);
|
|
@@ -2591,6 +2647,12 @@ async function startCursorSlackRelay(input: {
|
|
|
2591
2647
|
return undefined;
|
|
2592
2648
|
}
|
|
2593
2649
|
if (accountKey === undefined) {
|
|
2650
|
+
if (options.allowAnonymous === true) {
|
|
2651
|
+
logger(
|
|
2652
|
+
"[agent-sdk] cursor-slack: skipped while unsigned; long-poll relays need a key at start"
|
|
2653
|
+
);
|
|
2654
|
+
return undefined;
|
|
2655
|
+
}
|
|
2594
2656
|
throw new Error(
|
|
2595
2657
|
`This project has Slack channels using the Cursor account connection (slackChannel({ cursorAccount: true })) but the host is not signed in to Cursor. ${SIGN_IN_HINT}`
|
|
2596
2658
|
);
|
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
isHostOauthConnectionTransport,
|
|
56
56
|
isSymbolicConnectionTransport,
|
|
57
57
|
type JsonValue,
|
|
58
|
+
type McpToolAnnotations,
|
|
58
59
|
type PendingApproval,
|
|
59
60
|
type ReceiveInput,
|
|
60
61
|
type ResolvedAgent,
|
|
@@ -2480,6 +2481,14 @@ export class SessionEngine {
|
|
|
2480
2481
|
const decision = decideToolCall({
|
|
2481
2482
|
dryRun: args.record.dryRun === true,
|
|
2482
2483
|
effect: args.tool.definition.effect,
|
|
2484
|
+
...(args.tool.advertised === true
|
|
2485
|
+
? {
|
|
2486
|
+
mcpTool: {
|
|
2487
|
+
name: args.tool.name,
|
|
2488
|
+
annotations: args.tool.annotations,
|
|
2489
|
+
},
|
|
2490
|
+
}
|
|
2491
|
+
: {}),
|
|
2483
2492
|
dryRunResult: args.tool.definition.dryRunResult,
|
|
2484
2493
|
input: validation.value,
|
|
2485
2494
|
});
|
|
@@ -2490,6 +2499,17 @@ export class SessionEngine {
|
|
|
2490
2499
|
);
|
|
2491
2500
|
return normalizeToolResult(decision.result);
|
|
2492
2501
|
}
|
|
2502
|
+
if (decision.kind === "refuse") {
|
|
2503
|
+
this.logger(
|
|
2504
|
+
`[session] tool ${args.tool.name} refused, effect unclassified (${toolCallId})`
|
|
2505
|
+
);
|
|
2506
|
+
// The coverage signal: a run reports how much of itself was runnable.
|
|
2507
|
+
args.emit({
|
|
2508
|
+
type: "action.effect_unclassified",
|
|
2509
|
+
data: { callId: toolCallId, toolName: args.tool.name },
|
|
2510
|
+
});
|
|
2511
|
+
return toolErrorResult(decision.message);
|
|
2512
|
+
}
|
|
2493
2513
|
const needsGate = await evaluateNeedsApproval(
|
|
2494
2514
|
args.tool.definition.needsApproval,
|
|
2495
2515
|
validation.value
|
|
@@ -2778,6 +2798,9 @@ export class SessionEngine {
|
|
|
2778
2798
|
const decision = decideToolCall({
|
|
2779
2799
|
dryRun: record?.dryRun === true,
|
|
2780
2800
|
effect: tool.definition.effect,
|
|
2801
|
+
...(tool.advertised === true
|
|
2802
|
+
? { mcpTool: { name: tool.name, annotations: tool.annotations } }
|
|
2803
|
+
: {}),
|
|
2781
2804
|
dryRunResult: tool.definition.dryRunResult,
|
|
2782
2805
|
input: validation.value,
|
|
2783
2806
|
});
|
|
@@ -2798,7 +2821,17 @@ export class SessionEngine {
|
|
|
2798
2821
|
}).catch(() => {});
|
|
2799
2822
|
};
|
|
2800
2823
|
try {
|
|
2801
|
-
if (decision.kind === "
|
|
2824
|
+
if (decision.kind === "refuse") {
|
|
2825
|
+
this.logger(
|
|
2826
|
+
`[session] tool ${toolName} refused, effect unclassified (direct ${callId})`
|
|
2827
|
+
);
|
|
2828
|
+
emit?.({
|
|
2829
|
+
type: "action.effect_unclassified",
|
|
2830
|
+
data: { callId, toolName },
|
|
2831
|
+
});
|
|
2832
|
+
result = normalizeToolResult(toolErrorResult(decision.message));
|
|
2833
|
+
isError = true;
|
|
2834
|
+
} else if (decision.kind === "answer") {
|
|
2802
2835
|
this.logger(
|
|
2803
2836
|
`[session] tool ${toolName} answered by host (direct ${callId})`
|
|
2804
2837
|
);
|
|
@@ -2896,28 +2929,75 @@ export class SessionEngine {
|
|
|
2896
2929
|
* bridged reads too; resolving effects from a server's own listing is the
|
|
2897
2930
|
* next unit's work.
|
|
2898
2931
|
*/
|
|
2899
|
-
bridgedCallAnswer(args: {
|
|
2932
|
+
async bridgedCallAnswer(args: {
|
|
2900
2933
|
sessionId: string;
|
|
2934
|
+
connectionName: string;
|
|
2901
2935
|
toolName: string;
|
|
2902
|
-
}):
|
|
2936
|
+
}): Promise<
|
|
2937
|
+
{ answered: true; result: SDKCustomToolResult } | { answered: false }
|
|
2938
|
+
> {
|
|
2903
2939
|
// Read off the in-flight turn's context rather than the session store:
|
|
2904
2940
|
// the bridge already refuses a call with no active turn, so the record is
|
|
2905
2941
|
// in memory. That keeps this free for the sessions that are not dry runs,
|
|
2906
|
-
// which is every session today
|
|
2907
|
-
|
|
2942
|
+
// which is every session today — the listing below is only paid inside a
|
|
2943
|
+
// dry run.
|
|
2944
|
+
const context = this.activeToolContexts.get(args.sessionId);
|
|
2945
|
+
if (context?.record.dryRun !== true) {
|
|
2946
|
+
return { answered: false };
|
|
2947
|
+
}
|
|
2948
|
+
// A bridged tool carries no `defineTool`, so its effect comes from the
|
|
2949
|
+
// server's own listing annotations; a tool the listing cannot classify
|
|
2950
|
+
// refuses rather than being stubbed. A failed listing classifies nothing,
|
|
2951
|
+
// which lands on the same refusal — never on a run.
|
|
2952
|
+
const annotations = await this.bridgedToolAnnotations(
|
|
2953
|
+
args.connectionName,
|
|
2954
|
+
args.toolName
|
|
2955
|
+
);
|
|
2908
2956
|
const decision = decideToolCall({
|
|
2909
|
-
dryRun:
|
|
2957
|
+
dryRun: true,
|
|
2910
2958
|
effect: undefined,
|
|
2959
|
+
mcpTool: { name: args.toolName, annotations },
|
|
2911
2960
|
});
|
|
2912
2961
|
if (decision.kind === "run") {
|
|
2913
2962
|
return { answered: false };
|
|
2914
2963
|
}
|
|
2964
|
+
if (decision.kind === "refuse") {
|
|
2965
|
+
this.logger(
|
|
2966
|
+
`[session] bridged tool ${args.toolName} refused, effect unclassified (${args.sessionId})`
|
|
2967
|
+
);
|
|
2968
|
+
context.emit({
|
|
2969
|
+
type: "action.effect_unclassified",
|
|
2970
|
+
data: {
|
|
2971
|
+
callId: newToolCallId(args.toolName),
|
|
2972
|
+
toolName: args.toolName,
|
|
2973
|
+
connection: args.connectionName,
|
|
2974
|
+
},
|
|
2975
|
+
});
|
|
2976
|
+
return { answered: true, result: toolErrorResult(decision.message) };
|
|
2977
|
+
}
|
|
2915
2978
|
this.logger(
|
|
2916
2979
|
`[session] bridged tool ${args.toolName} answered by host (${args.sessionId})`
|
|
2917
2980
|
);
|
|
2918
2981
|
return { answered: true, result: normalizeToolResult(decision.result) };
|
|
2919
2982
|
}
|
|
2920
2983
|
|
|
2984
|
+
/**
|
|
2985
|
+
* The bounded annotations a connection's listing declares for one tool, or
|
|
2986
|
+
* undefined when the tool is unlisted, unannotated, or the listing fails —
|
|
2987
|
+
* all of which the policy reads as unclassified, the refusing state.
|
|
2988
|
+
*/
|
|
2989
|
+
private async bridgedToolAnnotations(
|
|
2990
|
+
connectionName: string,
|
|
2991
|
+
toolName: string
|
|
2992
|
+
): Promise<McpToolAnnotations | undefined> {
|
|
2993
|
+
try {
|
|
2994
|
+
const listed = await this.host.mcp.listTools(connectionName);
|
|
2995
|
+
return listed.find((tool) => tool.name === toolName)?.annotations;
|
|
2996
|
+
} catch {
|
|
2997
|
+
return undefined;
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
|
|
2921
3001
|
// ==========================================================================
|
|
2922
3002
|
// Events: append, dispatch to channel handlers + hooks
|
|
2923
3003
|
// ==========================================================================
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type {
|
|
13
|
+
McpToolAnnotations,
|
|
13
14
|
ToolEffect,
|
|
14
15
|
ToolEffectDeclaration,
|
|
15
16
|
ToolExecuteResult,
|
|
@@ -20,7 +21,14 @@ export type ToolDecision =
|
|
|
20
21
|
/** Run the tool body. */
|
|
21
22
|
| { kind: "run" }
|
|
22
23
|
/** Do not run it; return this to the model instead. */
|
|
23
|
-
| { kind: "answer"; result: ToolExecuteResult }
|
|
24
|
+
| { kind: "answer"; result: ToolExecuteResult }
|
|
25
|
+
/**
|
|
26
|
+
* Do not run it; fail the call with this message. Only ever produced in a
|
|
27
|
+
* dry run, for an MCP tool nothing classifies: stubbing it would let the
|
|
28
|
+
* run reach a confident conclusion from no data, and running it could leak
|
|
29
|
+
* a real write — refusing is the one failure that is loud.
|
|
30
|
+
*/
|
|
31
|
+
| { kind: "refuse"; message: string };
|
|
24
32
|
|
|
25
33
|
/** What a policy decision needs to know about the call. */
|
|
26
34
|
export interface ToolCallFacts {
|
|
@@ -28,6 +36,14 @@ export interface ToolCallFacts {
|
|
|
28
36
|
dryRun: boolean;
|
|
29
37
|
/** The tool's declaration, if it made one. */
|
|
30
38
|
effect: ToolEffectDeclaration<never> | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Present when the call targets a tool nobody authored — an advertised MCP
|
|
41
|
+
* passthrough or a bridged MCP call. Consulted only when {@link effect} is
|
|
42
|
+
* undeclared: the server's own annotations classify the call, and a tool
|
|
43
|
+
* they cannot classify refuses in a dry run instead of being stubbed as a
|
|
44
|
+
* write the way an undeclared authored tool is.
|
|
45
|
+
*/
|
|
46
|
+
mcpTool?: { name: string; annotations?: McpToolAnnotations };
|
|
31
47
|
/** What a stubbed write should answer with, if the tool supplied one. */
|
|
32
48
|
dryRunResult?: ToolExecuteResult | ((input: never) => ToolExecuteResult);
|
|
33
49
|
/**
|
|
@@ -64,11 +80,74 @@ export function resolveEffect(
|
|
|
64
80
|
return declared === "read" ? "read" : "write";
|
|
65
81
|
}
|
|
66
82
|
|
|
83
|
+
/**
|
|
84
|
+
* The effect an MCP server's own annotations declare, fail-closed.
|
|
85
|
+
*
|
|
86
|
+
* `readOnlyHint: true` without `destructiveHint: true` is a read; either
|
|
87
|
+
* negative signal is a write; anything else — absent annotations, or
|
|
88
|
+
* annotations that carry no effect signal (a bare `title`) — is `undefined`,
|
|
89
|
+
* unclassified. Mirrors `@anysphere/mcp-core/mcp-tool-annotations`
|
|
90
|
+
* (`classifyMcpToolAnnotationRisk`), which this package cannot import (npm
|
|
91
|
+
* publish, workspace-only dependency); keep the two in sync.
|
|
92
|
+
*/
|
|
93
|
+
export function classifyAnnotationsEffect(
|
|
94
|
+
annotations: McpToolAnnotations | undefined
|
|
95
|
+
): ToolEffect | undefined {
|
|
96
|
+
if (annotations === undefined) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
if (
|
|
100
|
+
annotations.destructiveHint !== true &&
|
|
101
|
+
annotations.readOnlyHint === true
|
|
102
|
+
) {
|
|
103
|
+
return "read";
|
|
104
|
+
}
|
|
105
|
+
if (
|
|
106
|
+
annotations.readOnlyHint === false ||
|
|
107
|
+
annotations.destructiveHint === true
|
|
108
|
+
) {
|
|
109
|
+
return "write";
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* What a dry run answers when nothing classifies an MCP tool. Deliberately
|
|
116
|
+
* reads as a strict API rather than disclosing the session's posture (see
|
|
117
|
+
* {@link ACKNOWLEDGED}), and names what is missing so the fix — the server
|
|
118
|
+
* annotating, or a server-side classification entry — is one step away.
|
|
119
|
+
*/
|
|
120
|
+
function unclassifiedRefusal(toolName: string): string {
|
|
121
|
+
return (
|
|
122
|
+
`Tool "${toolName}" is not available in this session: its MCP server ` +
|
|
123
|
+
`does not declare whether it reads or writes (readOnlyHint/` +
|
|
124
|
+
`destructiveHint), and no server-side classification exists for it, so ` +
|
|
125
|
+
`this session cannot tell whether calling it would change anything ` +
|
|
126
|
+
`outside the session. Use a tool that declares its effect.`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
67
130
|
/** Decide what to do with one tool call. Pure; no I/O, no session state. */
|
|
68
131
|
export function decideToolCall(facts: ToolCallFacts): ToolDecision {
|
|
69
132
|
if (!facts.dryRun) {
|
|
70
133
|
return { kind: "run" };
|
|
71
134
|
}
|
|
135
|
+
if (facts.effect === undefined && facts.mcpTool !== undefined) {
|
|
136
|
+
const effect = classifyAnnotationsEffect(facts.mcpTool.annotations);
|
|
137
|
+
if (effect === undefined) {
|
|
138
|
+
return {
|
|
139
|
+
kind: "refuse",
|
|
140
|
+
message: unclassifiedRefusal(facts.mcpTool.name),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (effect === "read") {
|
|
144
|
+
return { kind: "run" };
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
kind: "answer",
|
|
148
|
+
result: dryRunAnswer(facts.dryRunResult, facts.input),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
72
151
|
if (resolveEffect(facts.effect, facts.input) === "read") {
|
|
73
152
|
return { kind: "run" };
|
|
74
153
|
}
|
package/src/reminders.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Runtime reminders — per-session durable wakes.
|
|
3
3
|
*
|
|
4
4
|
* ```ts
|
|
5
|
-
* import type { ReminderCreateInput } from "@
|
|
5
|
+
* import type { ReminderCreateInput } from "@cursor/july/reminders";
|
|
6
6
|
*
|
|
7
7
|
* // Recurring ≈ setInterval; one-shot ≈ setTimeout.
|
|
8
8
|
* await host.reminders?.create({
|
package/src/schedules.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* handler form hands work off to a channel (and can call MCP via `args.host.mcp`):
|
|
7
7
|
*
|
|
8
8
|
* ```ts
|
|
9
|
-
* import { defineSchedule } from "@
|
|
9
|
+
* import { defineSchedule } from "@cursor/july/schedules";
|
|
10
10
|
*
|
|
11
11
|
* export default defineSchedule({
|
|
12
12
|
* cron: "0 9 * * 1-5",
|
package/src/skills.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* content or inline sibling files:
|
|
8
8
|
*
|
|
9
9
|
* ```ts
|
|
10
|
-
* import { defineSkill } from "@
|
|
10
|
+
* import { defineSkill } from "@cursor/july/skills";
|
|
11
11
|
*
|
|
12
12
|
* export default defineSkill({
|
|
13
13
|
* description: "Research unfamiliar topics before answering.",
|
package/src/storage.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* not as required-or-lose-history hooks:
|
|
17
17
|
*
|
|
18
18
|
* ```ts
|
|
19
|
-
* import { defineStorage } from "@
|
|
19
|
+
* import { defineStorage } from "@cursor/july/storage";
|
|
20
20
|
*
|
|
21
21
|
* export default defineStorage({
|
|
22
22
|
* put: (key, value) => db.upsert(key, value),
|
package/src/tools.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* name the model sees. Export the definition as the module's default export:
|
|
6
6
|
*
|
|
7
7
|
* ```ts
|
|
8
|
-
* import { defineTool } from "@
|
|
8
|
+
* import { defineTool } from "@cursor/july/tools";
|
|
9
9
|
* import { z } from "zod";
|
|
10
10
|
*
|
|
11
11
|
* // Runs in-process on the agent-serve host (default). Authored MCP
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Public types for @
|
|
2
|
+
* Public types for @cursor/july.
|
|
3
3
|
*
|
|
4
4
|
* The framework is filesystem-first: customers author an agent as files under
|
|
5
5
|
* an `agent/` directory (markdown for prose, TypeScript for typed behavior),
|
|
@@ -1439,6 +1439,17 @@ export type SessionEventPayload =
|
|
|
1439
1439
|
parentCallId?: string;
|
|
1440
1440
|
};
|
|
1441
1441
|
}
|
|
1442
|
+
| {
|
|
1443
|
+
/**
|
|
1444
|
+
* A dry-run call was refused because nothing classifies the tool's
|
|
1445
|
+
* effect: the MCP server sent no readOnlyHint/destructiveHint and no
|
|
1446
|
+
* server-side classification exists for it. The per-run count of these
|
|
1447
|
+
* is the coverage signal — how much of a backtest was actually
|
|
1448
|
+
* runnable, reported rather than assumed.
|
|
1449
|
+
*/
|
|
1450
|
+
type: "action.effect_unclassified";
|
|
1451
|
+
data: { callId: string; toolName: string; connection?: string };
|
|
1452
|
+
}
|
|
1442
1453
|
| {
|
|
1443
1454
|
type: "subagent.called";
|
|
1444
1455
|
data: { callId: string; name?: string; description?: string };
|
|
@@ -2620,6 +2631,14 @@ export interface DiscoveredTool {
|
|
|
2620
2631
|
* classification can read them; absent is the fail-closed state.
|
|
2621
2632
|
*/
|
|
2622
2633
|
annotations?: McpToolAnnotations;
|
|
2634
|
+
/**
|
|
2635
|
+
* True for a 1:1 MCP passthrough synthesized from an advertised
|
|
2636
|
+
* connection's listing. The policy treats these differently from authored
|
|
2637
|
+
* tools when nothing declares an effect: an undeclared authored tool is
|
|
2638
|
+
* stubbed as a write in a dry run, an unclassifiable advertised tool is
|
|
2639
|
+
* refused — see `decideToolCall`.
|
|
2640
|
+
*/
|
|
2641
|
+
advertised?: true;
|
|
2623
2642
|
/**
|
|
2624
2643
|
* Agent tools: script body materialized under
|
|
2625
2644
|
* `.agent-serve/tools/<name>.sh`.
|
|
@@ -2671,7 +2690,7 @@ export interface DiscoveredHook {
|
|
|
2671
2690
|
/** Live A/B metrics experiment (`agent/ab.ts` or `agent/ab/<name>.ts`). */
|
|
2672
2691
|
export interface DiscoveredAB {
|
|
2673
2692
|
name: string;
|
|
2674
|
-
/** Branded `defineAB({ ... })` config (see `@
|
|
2693
|
+
/** Branded `defineAB({ ... })` config (see `@cursor/july/ab`). */
|
|
2675
2694
|
definition: BrandedDefinition<"ab">;
|
|
2676
2695
|
}
|
|
2677
2696
|
|