@cursor/july 0.1.12 → 0.1.14
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/dist/bin/agent-serve.js +32 -9
- package/dist/channels/github/api.d.ts.map +1 -1
- package/dist/channels/github/api.js +11 -4
- package/dist/channels/github/cursor-account.d.ts +21 -1
- package/dist/channels/github/cursor-account.d.ts.map +1 -1
- package/dist/channels/github/cursor-account.js +29 -1
- package/dist/channels/github/github-channel.d.ts.map +1 -1
- package/dist/channels/github/github-channel.js +1 -1
- package/dist/channels/github/instrument.d.ts +20 -0
- package/dist/channels/github/instrument.d.ts.map +1 -0
- package/dist/channels/slack/api.d.ts +9 -0
- package/dist/channels/slack/api.d.ts.map +1 -1
- package/dist/channels/slack/api.js +40 -7
- package/dist/channels/slack/eval-directive.d.ts +3 -0
- package/dist/channels/slack/eval-directive.d.ts.map +1 -1
- package/dist/channels/slack/eval-directive.js +20 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.Dh_JkJjI.js → app.CKFFLL4Z.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.DGWkvXJc.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.CptQNLkR.js → VPLocalSearchBox.BXk19JBN.js} +1 -1
- package/dist/docs/assets/chunks/{theme.DWflH-LQ.js → theme.BXSPBMyn.js} +2 -2
- package/dist/docs/assets/{evals.md.DAgEc_hL.js → evals.md.DYOjkRCX.js} +10 -4
- package/dist/docs/assets/{evals.md.DAgEc_hL.lean.js → evals.md.DYOjkRCX.lean.js} +1 -1
- package/dist/docs/assets/{reference_cli.md.B74NrVzb.js → reference_cli.md.DnYfr5V2.js} +4 -1
- package/dist/docs/assets/{reference_http-api.md.Dx_nmDG6.js → reference_http-api.md.BncLd3PZ.js} +2 -2
- package/dist/docs/assets/{reference_http-api.md.Dx_nmDG6.lean.js → reference_http-api.md.BncLd3PZ.lean.js} +1 -1
- package/dist/docs/building-with-agents.html +3 -3
- package/dist/docs/concepts.html +3 -3
- package/dist/docs/deployment.html +3 -3
- package/dist/docs/evals.html +13 -7
- 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/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/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/channels.html +3 -3
- package/dist/docs/reference/cli.html +8 -5
- package/dist/docs/reference/connections.html +3 -3
- package/dist/docs/reference/hooks.html +3 -3
- package/dist/docs/reference/http-api.html +5 -5
- package/dist/docs/reference/instructions.html +3 -3
- package/dist/docs/reference/playground.html +3 -3
- package/dist/docs/reference/project-layout.html +3 -3
- 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 +3 -3
- package/dist/docs/scaffolding-agents.html +3 -3
- package/dist/docs/storage.html +3 -3
- package/dist/docs/troubleshooting.html +3 -3
- package/dist/evals.d.ts +41 -2
- package/dist/evals.d.ts.map +1 -1
- package/dist/evals.js +53 -16
- package/dist/internal/cli-ax.d.ts +4 -0
- package/dist/internal/cli-ax.d.ts.map +1 -1
- package/dist/internal/cli-ax.js +163 -33
- package/dist/internal/cli-deploy.d.ts +8 -0
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +92 -12
- package/dist/internal/cli-mcp-oauth.d.ts +14 -2
- package/dist/internal/cli-mcp-oauth.d.ts.map +1 -1
- package/dist/internal/cli-mcp-oauth.js +95 -2
- package/dist/internal/cursor/backend-client.d.ts +34 -0
- package/dist/internal/cursor/backend-client.d.ts.map +1 -1
- package/dist/internal/cursor/backend-client.js +73 -9
- package/dist/internal/cursor/credentials.d.ts +7 -1
- package/dist/internal/cursor/credentials.d.ts.map +1 -1
- package/dist/internal/cursor/credentials.js +9 -3
- package/dist/internal/cursor-account-mcp-auth.d.ts +55 -0
- package/dist/internal/cursor-account-mcp-auth.d.ts.map +1 -0
- package/dist/internal/cursor-account-mcp-auth.js +175 -0
- package/dist/internal/deploy-client.d.ts +6 -0
- package/dist/internal/deploy-client.d.ts.map +1 -1
- package/dist/internal/deploy-client.js +13 -0
- package/dist/internal/eval-run-store.d.ts +15 -0
- package/dist/internal/eval-run-store.d.ts.map +1 -1
- package/dist/internal/eval-run-store.js +87 -8
- package/dist/internal/eval-runner.d.ts +12 -0
- package/dist/internal/eval-runner.d.ts.map +1 -1
- package/dist/internal/eval-runner.js +97 -22
- package/dist/internal/evals-client.d.ts +9 -1
- package/dist/internal/evals-client.d.ts.map +1 -1
- package/dist/internal/evals-client.js +43 -0
- package/dist/internal/http-channel.d.ts.map +1 -1
- package/dist/internal/http-channel.js +74 -7
- package/dist/internal/map-pool.d.ts +6 -1
- package/dist/internal/map-pool.d.ts.map +1 -1
- package/dist/internal/map-pool.js +8 -1
- package/dist/internal/mcp-host.d.ts +13 -0
- package/dist/internal/mcp-host.d.ts.map +1 -1
- package/dist/internal/mcp-host.js +68 -13
- package/dist/internal/run-client.d.ts +5 -1
- package/dist/internal/run-client.d.ts.map +1 -1
- package/dist/internal/run-client.js +26 -5
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +57 -4
- package/dist/internal/session-engine.d.ts +13 -0
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +49 -0
- package/dist/internal/storage-coordinator.d.ts +7 -0
- package/dist/internal/storage-coordinator.d.ts.map +1 -1
- package/dist/internal/storage-coordinator.js +29 -4
- package/dist/playground/assets/index-CuXb4j4V.css +1 -0
- package/dist/playground/assets/{index-1dzluIcW.js → index-DeyYpOit.js} +46 -46
- package/dist/playground/index.html +2 -2
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/evals.md +46 -11
- package/docs/reference/cli.md +3 -0
- package/docs/reference/http-api.md +2 -1
- package/package.json +1 -1
- package/src/bin/agent-serve.ts +38 -8
- package/src/channels/github/api.ts +12 -1
- package/src/channels/github/cursor-account.ts +51 -1
- package/src/channels/github/github-channel.ts +4 -1
- package/src/channels/slack/api.ts +57 -8
- package/src/channels/slack/eval-directive.ts +24 -1
- package/src/evals.ts +104 -25
- package/src/internal/cli-ax.ts +192 -16
- package/src/internal/cli-deploy.ts +140 -13
- package/src/internal/cli-mcp-oauth.ts +122 -3
- package/src/internal/cursor/backend-client.ts +104 -1
- package/src/internal/cursor/credentials.ts +10 -2
- package/src/internal/cursor-account-mcp-auth.ts +241 -0
- package/src/internal/deploy-client.ts +16 -0
- package/src/internal/eval-run-store.ts +89 -6
- package/src/internal/eval-runner.ts +113 -18
- package/src/internal/evals-client.ts +81 -0
- package/src/internal/http-channel.ts +87 -6
- package/src/internal/map-pool.ts +8 -1
- package/src/internal/mcp-host.ts +74 -16
- package/src/internal/run-client.ts +32 -1
- package/src/internal/server.ts +69 -2
- package/src/internal/session-engine.ts +58 -0
- package/src/internal/storage-coordinator.ts +32 -6
- package/src/types.ts +10 -0
- package/dist/docs/assets/chunks/@localSearchIndexroot.Er_R50vY.js +0 -1
- package/dist/playground/assets/index-kZjeJs1v.css +0 -1
- /package/dist/docs/assets/{reference_cli.md.B74NrVzb.lean.js → reference_cli.md.DnYfr5V2.lean.js} +0 -0
|
@@ -85,6 +85,48 @@ const mcpToolSchema = z.object({
|
|
|
85
85
|
inputSchema: z.unknown().optional(),
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
const getAvailableMcpServersResponseSchema = z.object({
|
|
89
|
+
servers: z
|
|
90
|
+
.array(
|
|
91
|
+
z.object({
|
|
92
|
+
id: z.number().optional(),
|
|
93
|
+
name: z.string().optional(),
|
|
94
|
+
isTeamServer: z.boolean().optional(),
|
|
95
|
+
serverIdentifier: z.string().optional(),
|
|
96
|
+
})
|
|
97
|
+
)
|
|
98
|
+
.optional(),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/** One connector row from the account's dashboard MCP listing. */
|
|
102
|
+
export interface AvailableMcpServer {
|
|
103
|
+
id: number;
|
|
104
|
+
name: string;
|
|
105
|
+
isTeamServer: boolean;
|
|
106
|
+
serverIdentifier?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const checkHttpMcpStatusResponseSchema = z.object({
|
|
110
|
+
statuses: z
|
|
111
|
+
.array(
|
|
112
|
+
z.object({
|
|
113
|
+
id: z.number().optional(),
|
|
114
|
+
requiresAuth: z.boolean().optional(),
|
|
115
|
+
authUrl: z.string().optional(),
|
|
116
|
+
error: z.string().optional(),
|
|
117
|
+
})
|
|
118
|
+
)
|
|
119
|
+
.optional(),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
/** Per-connector OAuth status (SA-scoped when `serviceAccountId` was sent). */
|
|
123
|
+
export interface McpConnectorStatus {
|
|
124
|
+
id: number;
|
|
125
|
+
requiresAuth: boolean;
|
|
126
|
+
authUrl?: string;
|
|
127
|
+
error?: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
88
130
|
const listSandMcpToolsResponseSchema = z.object({
|
|
89
131
|
servers: z
|
|
90
132
|
.array(
|
|
@@ -180,6 +222,7 @@ async function dashboardRpc(args: {
|
|
|
180
222
|
bearer: string;
|
|
181
223
|
body: unknown;
|
|
182
224
|
timeoutMs: number;
|
|
225
|
+
extraHeaders?: Record<string, string>;
|
|
183
226
|
}): Promise<Response> {
|
|
184
227
|
try {
|
|
185
228
|
return await args.fetchImpl(
|
|
@@ -193,6 +236,7 @@ async function dashboardRpc(args: {
|
|
|
193
236
|
"x-cursor-client-version": CLIENT_VERSION,
|
|
194
237
|
"x-cursor-client-type": "agent-serve",
|
|
195
238
|
"x-request-id": randomUUID(),
|
|
239
|
+
...args.extraHeaders,
|
|
196
240
|
},
|
|
197
241
|
body: JSON.stringify(args.body ?? {}),
|
|
198
242
|
signal: AbortSignal.timeout(args.timeoutMs),
|
|
@@ -309,6 +353,58 @@ export class CursorBackendClient {
|
|
|
309
353
|
return toCallResult(parsed.result ?? {});
|
|
310
354
|
}
|
|
311
355
|
|
|
356
|
+
/**
|
|
357
|
+
* The account's dashboard MCP connector listing (id + name per server).
|
|
358
|
+
* `teamId` pins the ambient team so team-scoped connectors resolve for
|
|
359
|
+
* multi-team accounts.
|
|
360
|
+
*/
|
|
361
|
+
async getAvailableMcpServers(options?: {
|
|
362
|
+
teamId?: number;
|
|
363
|
+
}): Promise<AvailableMcpServer[]> {
|
|
364
|
+
const raw = await this.rpc("GetAvailableMcpServers", {}, LIST_TIMEOUT_MS, {
|
|
365
|
+
teamId: options?.teamId,
|
|
366
|
+
});
|
|
367
|
+
const parsed = getAvailableMcpServersResponseSchema.parse(raw);
|
|
368
|
+
return (parsed.servers ?? [])
|
|
369
|
+
.filter((server) => server.id !== undefined && server.id > 0)
|
|
370
|
+
.map((server) => ({
|
|
371
|
+
id: server.id!,
|
|
372
|
+
name: server.name ?? "",
|
|
373
|
+
isTeamServer: server.isTeamServer === true,
|
|
374
|
+
serverIdentifier: server.serverIdentifier,
|
|
375
|
+
}));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Probe connectors for a service account and mint OAuth `authUrl`s where
|
|
380
|
+
* authorization is needed. This is the dashboard's Authenticate flow: the
|
|
381
|
+
* caller (a team member) opens the returned URL, the cursor.com callback
|
|
382
|
+
* stores the token backend-side against the service account, and repeat
|
|
383
|
+
* probes flip `requiresAuth` off.
|
|
384
|
+
*/
|
|
385
|
+
async checkHttpMcpStatus(args: {
|
|
386
|
+
serverIds: number[];
|
|
387
|
+
oauthRedirectUri: string;
|
|
388
|
+
serviceAccountId: string;
|
|
389
|
+
}): Promise<McpConnectorStatus[]> {
|
|
390
|
+
const raw = await this.rpc(
|
|
391
|
+
"CheckHttpMcpStatus",
|
|
392
|
+
{
|
|
393
|
+
serverIds: args.serverIds,
|
|
394
|
+
oauthRedirectUri: args.oauthRedirectUri,
|
|
395
|
+
serviceAccountId: args.serviceAccountId,
|
|
396
|
+
},
|
|
397
|
+
LIST_TIMEOUT_MS
|
|
398
|
+
);
|
|
399
|
+
const parsed = checkHttpMcpStatusResponseSchema.parse(raw);
|
|
400
|
+
return (parsed.statuses ?? []).map((status) => ({
|
|
401
|
+
id: status.id ?? 0,
|
|
402
|
+
requiresAuth: status.requiresAuth === true,
|
|
403
|
+
authUrl: status.authUrl,
|
|
404
|
+
error: status.error,
|
|
405
|
+
}));
|
|
406
|
+
}
|
|
407
|
+
|
|
312
408
|
/** Mint a short-lived GitHub token scoped to the requested repositories. */
|
|
313
409
|
async getGithubToken(
|
|
314
410
|
repos: readonly string[],
|
|
@@ -356,8 +452,13 @@ export class CursorBackendClient {
|
|
|
356
452
|
private async rpc(
|
|
357
453
|
method: string,
|
|
358
454
|
body: unknown,
|
|
359
|
-
timeoutMs = LIST_TIMEOUT_MS
|
|
455
|
+
timeoutMs = LIST_TIMEOUT_MS,
|
|
456
|
+
options?: { teamId?: number }
|
|
360
457
|
): Promise<unknown> {
|
|
458
|
+
const extraHeaders =
|
|
459
|
+
options?.teamId === undefined
|
|
460
|
+
? undefined
|
|
461
|
+
: { "x-cursor-team-id": String(options.teamId) };
|
|
361
462
|
const bearer = await this.getAccessToken();
|
|
362
463
|
const first = await dashboardRpc({
|
|
363
464
|
fetchImpl: this.fetchImpl,
|
|
@@ -366,6 +467,7 @@ export class CursorBackendClient {
|
|
|
366
467
|
bearer,
|
|
367
468
|
body,
|
|
368
469
|
timeoutMs,
|
|
470
|
+
extraHeaders,
|
|
369
471
|
});
|
|
370
472
|
if (first.status !== 401) {
|
|
371
473
|
return parseRpcResponse(method, first);
|
|
@@ -385,6 +487,7 @@ export class CursorBackendClient {
|
|
|
385
487
|
bearer: retryBearer,
|
|
386
488
|
body,
|
|
387
489
|
timeoutMs,
|
|
490
|
+
extraHeaders,
|
|
388
491
|
});
|
|
389
492
|
if (second.status === 401 && this.token?.value === retryBearer) {
|
|
390
493
|
// The freshly minted token was rejected too; do not keep serving it.
|
|
@@ -26,10 +26,18 @@ export const DEFAULT_CURSOR_BACKEND_URL = "https://api2.cursor.sh";
|
|
|
26
26
|
export const DEFAULT_CURSOR_EXTERNAL_API_URL = "https://api.cursor.com";
|
|
27
27
|
const DEFAULT_CURSOR_WEBSITE_URL = "https://cursor.com";
|
|
28
28
|
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Base URL for main-backend-only routes (key exchange, DashboardService —
|
|
31
|
+
* these 404 on api.cursor.com). Hosted pods pin `CURSOR_API_BASE_URL` to the
|
|
32
|
+
* external API, so `AGENT_SERVE_CURSOR_BACKEND_URL` re-pins this family
|
|
33
|
+
* there. Deliberately namespaced: pod env propagates to the SDK harness,
|
|
34
|
+
* which reads the generic `CURSOR_BACKEND_URL` for its own hosts.
|
|
35
|
+
*/
|
|
30
36
|
export function cursorBackendUrl(): string {
|
|
31
37
|
return trimTrailingSlash(
|
|
32
|
-
process.env.
|
|
38
|
+
process.env.AGENT_SERVE_CURSOR_BACKEND_URL ??
|
|
39
|
+
process.env.CURSOR_API_BASE_URL ??
|
|
40
|
+
DEFAULT_CURSOR_BACKEND_URL
|
|
33
41
|
);
|
|
34
42
|
}
|
|
35
43
|
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service-account connector authorization for
|
|
3
|
+
* `defineConnection({ cursorAccount: true })` on hosted deployments.
|
|
4
|
+
*
|
|
5
|
+
* Hosted engines authenticate as their deployment's dedicated service
|
|
6
|
+
* account, whose connector OAuth tokens live backend-side. This flow is the
|
|
7
|
+
* dashboard's Authenticate button as a CLI loop: probe the connectors via
|
|
8
|
+
* `CheckHttpMcpStatus` (which mints a complete OAuth `authUrl` per connector
|
|
9
|
+
* needing auth), open each URL for the human to grant, and poll until the
|
|
10
|
+
* backend reports the token stored. Nothing token-shaped ever touches this
|
|
11
|
+
* process — the cursor.com callback stores tokens against the service
|
|
12
|
+
* account. Used by `agentkit mcp oauth <connection>` and the post-deploy
|
|
13
|
+
* hook in `cli-deploy.ts`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
AvailableMcpServer,
|
|
18
|
+
CursorBackendClient,
|
|
19
|
+
McpConnectorStatus,
|
|
20
|
+
} from "./cursor/backend-client.js";
|
|
21
|
+
import { LOG_PREFIX } from "./distribution.js";
|
|
22
|
+
import { openBrowser } from "./mcp-oauth.js";
|
|
23
|
+
|
|
24
|
+
const DEFAULT_POLL_INTERVAL_MS = 3_000;
|
|
25
|
+
const DEFAULT_POLL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
26
|
+
|
|
27
|
+
/** Same default the dashboard uses; MCP_OAUTH_REDIRECT_URI overrides for dev. */
|
|
28
|
+
export function mcpOauthRedirectUri(): string {
|
|
29
|
+
const override = process.env.MCP_OAUTH_REDIRECT_URI;
|
|
30
|
+
if (override !== undefined && override.trim() !== "") {
|
|
31
|
+
return override.trim();
|
|
32
|
+
}
|
|
33
|
+
return "https://www.cursor.com/agents/mcp/oauth/callback";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AuthorizeCursorAccountConnectorsArgs {
|
|
37
|
+
backendClient: Pick<
|
|
38
|
+
CursorBackendClient,
|
|
39
|
+
"getAvailableMcpServers" | "checkHttpMcpStatus"
|
|
40
|
+
>;
|
|
41
|
+
teamId: number;
|
|
42
|
+
slug: string;
|
|
43
|
+
/** The deployment's dedicated service account (`sa_…`). */
|
|
44
|
+
serviceAccountId: string;
|
|
45
|
+
/** Connection name, for log lines. */
|
|
46
|
+
connectionName: string;
|
|
47
|
+
/** The connection's `servers` filter; undefined = every team connector. */
|
|
48
|
+
servers?: string[] | undefined;
|
|
49
|
+
/** Open auth URLs and poll for completion (TTY); false = report only. */
|
|
50
|
+
interactive: boolean;
|
|
51
|
+
out: (text: string) => void;
|
|
52
|
+
err: (text: string) => void;
|
|
53
|
+
/** Test seams. */
|
|
54
|
+
openBrowserImpl?: (url: string) => void;
|
|
55
|
+
sleep?: (ms: number) => Promise<void>;
|
|
56
|
+
pollIntervalMs?: number;
|
|
57
|
+
pollTimeoutMs?: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface AuthorizeCursorAccountConnectorsResult {
|
|
61
|
+
/** Connectors with a stored credential after the flow. */
|
|
62
|
+
authorized: string[];
|
|
63
|
+
/** Connectors still requiring authorization. */
|
|
64
|
+
pending: string[];
|
|
65
|
+
/** Connectors the backend could not probe. */
|
|
66
|
+
errored: Array<{ name: string; error: string }>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function sleepMs(ms: number): Promise<void> {
|
|
70
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Authorize the deployment service account for the connection's connectors.
|
|
75
|
+
* Already-authorized connectors are skipped; the rest get a browser OAuth
|
|
76
|
+
* each (sequentially), polled until the backend stores the token.
|
|
77
|
+
*/
|
|
78
|
+
export async function authorizeCursorAccountConnectors(
|
|
79
|
+
args: AuthorizeCursorAccountConnectorsArgs
|
|
80
|
+
): Promise<AuthorizeCursorAccountConnectorsResult> {
|
|
81
|
+
const { backendClient, teamId, serviceAccountId, connectionName, out, err } =
|
|
82
|
+
args;
|
|
83
|
+
const open = args.openBrowserImpl ?? openBrowser;
|
|
84
|
+
const sleep = args.sleep ?? sleepMs;
|
|
85
|
+
const pollIntervalMs = args.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
86
|
+
const pollTimeoutMs = args.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
|
|
87
|
+
const redirectUri = mcpOauthRedirectUri();
|
|
88
|
+
|
|
89
|
+
// Team-scoped connectors only: the hosted bridge resolves the SA's team
|
|
90
|
+
// servers, so personal connectors can never apply to a deployment.
|
|
91
|
+
const available = (
|
|
92
|
+
await backendClient.getAvailableMcpServers({ teamId })
|
|
93
|
+
).filter((server) => server.isTeamServer);
|
|
94
|
+
|
|
95
|
+
const selected = selectConnectors(available, args.servers);
|
|
96
|
+
for (const missing of selected.unmatched) {
|
|
97
|
+
err(
|
|
98
|
+
`${LOG_PREFIX} Connection "${connectionName}" lists connector ${JSON.stringify(missing)}, but the team has no such connector (dashboard → MCP).\n`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (selected.servers.length === 0) {
|
|
102
|
+
err(
|
|
103
|
+
`${LOG_PREFIX} No matching team connectors for connection "${connectionName}".\n`
|
|
104
|
+
);
|
|
105
|
+
return { authorized: [], pending: [], errored: [] };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const nameById = new Map(
|
|
109
|
+
selected.servers.map((server) => [server.id, server.name])
|
|
110
|
+
);
|
|
111
|
+
const statuses = await backendClient.checkHttpMcpStatus({
|
|
112
|
+
serverIds: selected.servers.map((server) => server.id),
|
|
113
|
+
oauthRedirectUri: redirectUri,
|
|
114
|
+
serviceAccountId,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const result: AuthorizeCursorAccountConnectorsResult = {
|
|
118
|
+
authorized: [],
|
|
119
|
+
pending: [],
|
|
120
|
+
errored: [],
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
for (const status of statuses) {
|
|
124
|
+
const name = nameById.get(status.id) ?? `#${status.id}`;
|
|
125
|
+
if (!status.requiresAuth) {
|
|
126
|
+
if (status.error !== undefined && status.error !== "") {
|
|
127
|
+
err(`${LOG_PREFIX} ${name}: ${status.error}\n`);
|
|
128
|
+
result.errored.push({ name, error: status.error });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
out(`${LOG_PREFIX} ${name}: already authorized ✓\n`);
|
|
132
|
+
result.authorized.push(name);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (status.authUrl === undefined || status.authUrl === "") {
|
|
136
|
+
const error = status.error ?? "no authorization URL was minted";
|
|
137
|
+
err(`${LOG_PREFIX} ${name}: ${error}\n`);
|
|
138
|
+
result.errored.push({ name, error });
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (!args.interactive) {
|
|
142
|
+
result.pending.push(name);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
out(
|
|
147
|
+
`${LOG_PREFIX} ${name}: authorization needed. You'll authorize ${name} as yourself; ` +
|
|
148
|
+
`anyone who can talk to this agent can act with that access.\n` +
|
|
149
|
+
`${LOG_PREFIX} Opening (or open manually):\n ${status.authUrl}\n`
|
|
150
|
+
);
|
|
151
|
+
open(status.authUrl);
|
|
152
|
+
|
|
153
|
+
const authorized = await pollUntilAuthorized({
|
|
154
|
+
backendClient,
|
|
155
|
+
serverId: status.id,
|
|
156
|
+
serviceAccountId,
|
|
157
|
+
redirectUri,
|
|
158
|
+
sleep,
|
|
159
|
+
pollIntervalMs,
|
|
160
|
+
pollTimeoutMs,
|
|
161
|
+
});
|
|
162
|
+
if (authorized) {
|
|
163
|
+
out(`${LOG_PREFIX} ${name}: authorized ✓\n`);
|
|
164
|
+
result.authorized.push(name);
|
|
165
|
+
} else {
|
|
166
|
+
err(
|
|
167
|
+
`${LOG_PREFIX} ${name}: still waiting on authorization; giving up for now.\n`
|
|
168
|
+
);
|
|
169
|
+
result.pending.push(name);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function selectConnectors(
|
|
177
|
+
available: AvailableMcpServer[],
|
|
178
|
+
filter: string[] | undefined
|
|
179
|
+
): { servers: AvailableMcpServer[]; unmatched: string[] } {
|
|
180
|
+
if (filter === undefined || filter.length === 0) {
|
|
181
|
+
return { servers: available, unmatched: [] };
|
|
182
|
+
}
|
|
183
|
+
const servers: AvailableMcpServer[] = [];
|
|
184
|
+
const seen = new Set<number>();
|
|
185
|
+
const unmatched: string[] = [];
|
|
186
|
+
for (const name of filter) {
|
|
187
|
+
const wanted = name.trim().toLowerCase();
|
|
188
|
+
const matches = available.filter(
|
|
189
|
+
(server) =>
|
|
190
|
+
server.name.toLowerCase() === wanted ||
|
|
191
|
+
server.serverIdentifier?.toLowerCase() === wanted
|
|
192
|
+
);
|
|
193
|
+
if (matches.length === 0) {
|
|
194
|
+
unmatched.push(name);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
for (const match of matches) {
|
|
198
|
+
if (!seen.has(match.id)) {
|
|
199
|
+
seen.add(match.id);
|
|
200
|
+
servers.push(match);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { servers, unmatched };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function pollUntilAuthorized(args: {
|
|
208
|
+
backendClient: Pick<CursorBackendClient, "checkHttpMcpStatus">;
|
|
209
|
+
serverId: number;
|
|
210
|
+
serviceAccountId: string;
|
|
211
|
+
redirectUri: string;
|
|
212
|
+
sleep: (ms: number) => Promise<void>;
|
|
213
|
+
pollIntervalMs: number;
|
|
214
|
+
pollTimeoutMs: number;
|
|
215
|
+
}): Promise<boolean> {
|
|
216
|
+
const deadline = Date.now() + args.pollTimeoutMs;
|
|
217
|
+
// Each unauthorized probe is a full backend connect to the provider, so
|
|
218
|
+
// back off while the human works through the OAuth pages.
|
|
219
|
+
let intervalMs = args.pollIntervalMs;
|
|
220
|
+
const maxIntervalMs = Math.max(args.pollIntervalMs, 15_000);
|
|
221
|
+
while (Date.now() < deadline) {
|
|
222
|
+
await args.sleep(intervalMs);
|
|
223
|
+
intervalMs = Math.min(intervalMs * 1.5, maxIntervalMs);
|
|
224
|
+
let statuses: McpConnectorStatus[];
|
|
225
|
+
try {
|
|
226
|
+
statuses = await args.backendClient.checkHttpMcpStatus({
|
|
227
|
+
serverIds: [args.serverId],
|
|
228
|
+
oauthRedirectUri: args.redirectUri,
|
|
229
|
+
serviceAccountId: args.serviceAccountId,
|
|
230
|
+
});
|
|
231
|
+
} catch {
|
|
232
|
+
// Transient probe errors; keep polling until the deadline.
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const status = statuses.find((entry) => entry.id === args.serverId);
|
|
236
|
+
if (status !== undefined && !status.requiresAuth) {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
@@ -175,6 +175,22 @@ export interface DeploymentDetail extends Deployment {
|
|
|
175
175
|
engineAccess?: EngineAccess | null;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
/**
|
|
179
|
+
* The deployment's dedicated service-account id (`sa_…`), or undefined for
|
|
180
|
+
* legacy rows that predate service-account-backed deploys. Normalizes the
|
|
181
|
+
* wire shape (string in prod, number in old fixtures, null when unset).
|
|
182
|
+
*/
|
|
183
|
+
export function deploymentServiceAccountId(
|
|
184
|
+
deployment: Pick<Deployment, "serviceAccountId">
|
|
185
|
+
): string | undefined {
|
|
186
|
+
const raw = deployment.serviceAccountId;
|
|
187
|
+
if (raw === null || raw === undefined) {
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
const id = String(raw).trim();
|
|
191
|
+
return id === "" ? undefined : id;
|
|
192
|
+
}
|
|
193
|
+
|
|
178
194
|
export interface StopAccepted {
|
|
179
195
|
id: number;
|
|
180
196
|
status: string;
|
|
@@ -63,10 +63,13 @@ const INTERRUPTED_BATCH_ERROR =
|
|
|
63
63
|
"Eval run interrupted (serve process exited before completion)";
|
|
64
64
|
const INTERRUPTED_CASE_ERROR =
|
|
65
65
|
"Eval case interrupted (serve process exited before completion)";
|
|
66
|
+
const CANCELLED_BATCH_ERROR = "Eval run cancelled";
|
|
67
|
+
const CANCELLED_CASE_ERROR = "Eval case cancelled";
|
|
66
68
|
|
|
67
69
|
export class EvalRunStore {
|
|
68
70
|
private readonly runs = new Map<string, EvalRunSnapshot>();
|
|
69
71
|
private activeRunId: string | undefined;
|
|
72
|
+
private readonly abortByRunId = new Map<string, AbortController>();
|
|
70
73
|
private hydratePromise: Promise<void> | undefined;
|
|
71
74
|
private hydrated = false;
|
|
72
75
|
/** Filled after listen — e.g. `http://127.0.0.1:3000/weather-agent`. */
|
|
@@ -178,6 +181,8 @@ export class EvalRunStore {
|
|
|
178
181
|
};
|
|
179
182
|
this.runs.set(runId, snapshot);
|
|
180
183
|
this.activeRunId = runId;
|
|
184
|
+
const abort = new AbortController();
|
|
185
|
+
this.abortByRunId.set(runId, abort);
|
|
181
186
|
this.pruneOldRuns(config);
|
|
182
187
|
this.persist(snapshot);
|
|
183
188
|
this.logger(
|
|
@@ -188,6 +193,7 @@ export class EvalRunStore {
|
|
|
188
193
|
baseUrl: loopbackUrl,
|
|
189
194
|
evals: selected,
|
|
190
195
|
config,
|
|
196
|
+
signal: abort.signal,
|
|
191
197
|
...(input.bearerToken === undefined
|
|
192
198
|
? {}
|
|
193
199
|
: { bearerToken: input.bearerToken }),
|
|
@@ -198,6 +204,33 @@ export class EvalRunStore {
|
|
|
198
204
|
return snapshot;
|
|
199
205
|
}
|
|
200
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Cancel a running batch by Eval ID (`runId`). Stops starting new cases and
|
|
209
|
+
* aborts in-flight turns. Idempotent when already cancelled.
|
|
210
|
+
*/
|
|
211
|
+
async cancel(runId: string): Promise<EvalRunSnapshot> {
|
|
212
|
+
await this.hydrate();
|
|
213
|
+
const run = this.runs.get(runId);
|
|
214
|
+
if (run === undefined) {
|
|
215
|
+
throw new EvalRunUnknownError(runId);
|
|
216
|
+
}
|
|
217
|
+
if (run.status === "cancelled") {
|
|
218
|
+
return run;
|
|
219
|
+
}
|
|
220
|
+
if (run.status !== "running") {
|
|
221
|
+
throw new EvalRunNotRunningError(runId, run.status);
|
|
222
|
+
}
|
|
223
|
+
const abort = this.abortByRunId.get(runId);
|
|
224
|
+
abort?.abort();
|
|
225
|
+
markCancelledRun(run);
|
|
226
|
+
this.persist(run);
|
|
227
|
+
if (this.activeRunId === runId) {
|
|
228
|
+
this.activeRunId = undefined;
|
|
229
|
+
}
|
|
230
|
+
this.logger(`[agentkit] eval run ${runId}: cancelled`);
|
|
231
|
+
return run;
|
|
232
|
+
}
|
|
233
|
+
|
|
201
234
|
private sortedRuns(): EvalRunSnapshot[] {
|
|
202
235
|
return [...this.runs.values()].sort((a, b) =>
|
|
203
236
|
b.startedAt.localeCompare(a.startedAt)
|
|
@@ -306,9 +339,11 @@ export class EvalRunStore {
|
|
|
306
339
|
if (run === undefined || run.status !== "running") {
|
|
307
340
|
return;
|
|
308
341
|
}
|
|
342
|
+
this.abortByRunId.get(activeRunId)?.abort();
|
|
309
343
|
markInterruptedRun(run);
|
|
310
344
|
this.persist(run);
|
|
311
345
|
this.activeRunId = undefined;
|
|
346
|
+
this.abortByRunId.delete(activeRunId);
|
|
312
347
|
}
|
|
313
348
|
|
|
314
349
|
private async execute(
|
|
@@ -320,6 +355,7 @@ export class EvalRunStore {
|
|
|
320
355
|
bearerToken?: string;
|
|
321
356
|
verbose?: boolean;
|
|
322
357
|
timeoutMs?: number;
|
|
358
|
+
signal: AbortSignal;
|
|
323
359
|
}
|
|
324
360
|
): Promise<void> {
|
|
325
361
|
if (this.runs.get(runId) === undefined) {
|
|
@@ -330,6 +366,7 @@ export class EvalRunStore {
|
|
|
330
366
|
projectRoot: this.projectRoot,
|
|
331
367
|
baseUrl: opts.baseUrl,
|
|
332
368
|
discovered: { evals: opts.evals, config: opts.config },
|
|
369
|
+
signal: opts.signal,
|
|
333
370
|
...(opts.bearerToken === undefined
|
|
334
371
|
? {}
|
|
335
372
|
: { bearerToken: opts.bearerToken }),
|
|
@@ -337,7 +374,7 @@ export class EvalRunStore {
|
|
|
337
374
|
...(opts.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
|
|
338
375
|
onCaseStart: (id) => {
|
|
339
376
|
const run = this.runs.get(runId);
|
|
340
|
-
if (run === undefined) {
|
|
377
|
+
if (run === undefined || run.status === "cancelled") {
|
|
341
378
|
return;
|
|
342
379
|
}
|
|
343
380
|
const c = run.cases.find((x) => x.id === id);
|
|
@@ -387,7 +424,7 @@ export class EvalRunStore {
|
|
|
387
424
|
},
|
|
388
425
|
});
|
|
389
426
|
const run = this.runs.get(runId);
|
|
390
|
-
if (run === undefined) {
|
|
427
|
+
if (run === undefined || run.status === "cancelled") {
|
|
391
428
|
return;
|
|
392
429
|
}
|
|
393
430
|
run.status = "completed";
|
|
@@ -398,15 +435,23 @@ export class EvalRunStore {
|
|
|
398
435
|
);
|
|
399
436
|
} catch (error) {
|
|
400
437
|
const run = this.runs.get(runId);
|
|
401
|
-
if (run === undefined) {
|
|
438
|
+
if (run === undefined || run.status === "cancelled") {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
442
|
+
if (opts.signal.aborted || message === "cancelled") {
|
|
443
|
+
markCancelledRun(run);
|
|
444
|
+
this.persist(run);
|
|
445
|
+
this.logger(`[agentkit] eval run ${runId}: cancelled`);
|
|
402
446
|
return;
|
|
403
447
|
}
|
|
404
448
|
run.status = "failed";
|
|
405
449
|
run.finishedAt = new Date().toISOString();
|
|
406
|
-
run.error =
|
|
450
|
+
run.error = message;
|
|
407
451
|
this.persist(run);
|
|
408
452
|
this.logger(`[agentkit] eval run ${runId}: failed — ${run.error}`);
|
|
409
453
|
} finally {
|
|
454
|
+
this.abortByRunId.delete(runId);
|
|
410
455
|
if (this.activeRunId === runId) {
|
|
411
456
|
this.activeRunId = undefined;
|
|
412
457
|
}
|
|
@@ -422,6 +467,7 @@ export class EvalRunStore {
|
|
|
422
467
|
if (c === undefined) {
|
|
423
468
|
return;
|
|
424
469
|
}
|
|
470
|
+
const alreadyCounted = c.status === "done";
|
|
425
471
|
c.status = "done";
|
|
426
472
|
c.ok = result.ok;
|
|
427
473
|
c.assertions = result.assertions;
|
|
@@ -443,6 +489,10 @@ export class EvalRunStore {
|
|
|
443
489
|
if (result.tools !== undefined) {
|
|
444
490
|
c.tools = result.tools;
|
|
445
491
|
}
|
|
492
|
+
// Batch already finalized by cancel — keep case details, skip recount.
|
|
493
|
+
if (run.status === "cancelled" || alreadyCounted) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
446
496
|
run.summary.done += 1;
|
|
447
497
|
if (result.ok) {
|
|
448
498
|
run.summary.passed += 1;
|
|
@@ -463,8 +513,24 @@ function markInterruptedRun(run: EvalRunSnapshot): void {
|
|
|
463
513
|
c.error = c.error ?? INTERRUPTED_CASE_ERROR;
|
|
464
514
|
}
|
|
465
515
|
}
|
|
466
|
-
|
|
467
|
-
|
|
516
|
+
reconcileRunSummary(run);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function markCancelledRun(run: EvalRunSnapshot): void {
|
|
520
|
+
run.status = "cancelled";
|
|
521
|
+
run.finishedAt = run.finishedAt ?? new Date().toISOString();
|
|
522
|
+
run.error = CANCELLED_BATCH_ERROR;
|
|
523
|
+
for (const c of run.cases) {
|
|
524
|
+
if (c.status === "pending" || c.status === "running") {
|
|
525
|
+
c.status = "done";
|
|
526
|
+
c.ok = false;
|
|
527
|
+
c.error = CANCELLED_CASE_ERROR;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
reconcileRunSummary(run);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function reconcileRunSummary(run: EvalRunSnapshot): void {
|
|
468
534
|
let passed = 0;
|
|
469
535
|
let failed = 0;
|
|
470
536
|
for (const c of run.cases) {
|
|
@@ -536,3 +602,20 @@ export class EvalRunEmptyError extends Error {
|
|
|
536
602
|
this.name = "EvalRunEmptyError";
|
|
537
603
|
}
|
|
538
604
|
}
|
|
605
|
+
|
|
606
|
+
export class EvalRunUnknownError extends Error {
|
|
607
|
+
constructor(readonly runId: string) {
|
|
608
|
+
super(`Unknown eval run ${runId}`);
|
|
609
|
+
this.name = "EvalRunUnknownError";
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export class EvalRunNotRunningError extends Error {
|
|
614
|
+
constructor(
|
|
615
|
+
readonly runId: string,
|
|
616
|
+
readonly status: EvalRunSnapshot["status"]
|
|
617
|
+
) {
|
|
618
|
+
super(`Eval run ${runId} is not running (status: ${status})`);
|
|
619
|
+
this.name = "EvalRunNotRunningError";
|
|
620
|
+
}
|
|
621
|
+
}
|