@intentic/sandbox-contract 1.227.0 → 1.228.1
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/agent-catalog.d.ts +2 -2
- package/dist/agent-catalog.d.ts.map +1 -1
- package/dist/agent-catalog.js +26 -1
- package/dist/agent-catalog.js.map +1 -1
- package/dist/contracts/agents.contract.d.ts +4 -0
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +2 -2
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/capabilities.contract.d.ts +7 -0
- package/dist/contracts/capabilities.contract.d.ts.map +1 -1
- package/dist/contracts/cursor.contract.d.ts +63 -0
- package/dist/contracts/cursor.contract.d.ts.map +1 -0
- package/dist/contracts/cursor.contract.js +49 -0
- package/dist/contracts/cursor.contract.js.map +1 -0
- package/dist/contracts/providers.contract.d.ts +1 -0
- package/dist/contracts/providers.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +14 -14
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/schemas.d.ts +180 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +23 -1
- package/dist/schemas.js.map +1 -1
- package/package.json +6 -6
- package/src/agent-catalog.test.ts +2 -2
- package/src/agent-catalog.ts +123 -13
- package/src/contracts/agents.contract.ts +2 -1
- package/src/contracts/cursor.contract.ts +74 -0
- package/src/events.ts +10 -5
- package/src/index.ts +3 -0
- package/src/schemas.ts +82 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentic/sandbox-contract",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.228.1",
|
|
4
4
|
"description": "oRPC wire contract for the intentic sandbox daemon, shared by the daemon and its browser client",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -89,16 +89,16 @@
|
|
|
89
89
|
"@orpc/contract": "1.14.13",
|
|
90
90
|
"tslib": "2.8.1",
|
|
91
91
|
"zod": "4.4.3",
|
|
92
|
-
"@intentic/constants": "1.
|
|
93
|
-
"@intentic/
|
|
94
|
-
"@intentic/
|
|
92
|
+
"@intentic/constants": "1.228.1",
|
|
93
|
+
"@intentic/extension-manifest": "1.228.1",
|
|
94
|
+
"@intentic/registry": "1.228.1"
|
|
95
95
|
},
|
|
96
96
|
"devDependencies": {
|
|
97
97
|
"@types/node": "24.13.2",
|
|
98
98
|
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
99
99
|
"vitest": "4.1.10",
|
|
100
|
-
"@intentic/
|
|
101
|
-
"@intentic/
|
|
100
|
+
"@intentic/tsconfig": "0.0.0",
|
|
101
|
+
"@intentic/testing": "0.0.0"
|
|
102
102
|
},
|
|
103
103
|
"scripts": {
|
|
104
104
|
"build": "tsgo",
|
|
@@ -39,9 +39,9 @@ describe("every provider/harness pair declares what it can do", () => {
|
|
|
39
39
|
|
|
40
40
|
// Nothing may be left to inference: a `permissions` a surface can't read, or a runtime nobody serves,
|
|
41
41
|
// is the drift this record exists to end.
|
|
42
|
-
expect(["claude-code", "codex", "opencode", "opencode-gemini", "acp", "pi"]).toContain(capabilities.runtime);
|
|
42
|
+
expect(["claude-code", "codex", "cursor", "opencode", "opencode-gemini", "acp", "pi"]).toContain(capabilities.runtime);
|
|
43
43
|
expect(["modes", "plan"]).toContain(capabilities.permissions);
|
|
44
|
-
expect(["full", "browser", "http", "none"]).toContain(capabilities.mcp);
|
|
44
|
+
expect(["full", "tools", "browser", "http", "none"]).toContain(capabilities.mcp);
|
|
45
45
|
// Every runtime executes SOMETHING: a record listing no backend would hide the shell every loop has.
|
|
46
46
|
expect(capabilities.execution).toContain("shell");
|
|
47
47
|
for (const backend of capabilities.execution) {
|
package/src/agent-catalog.ts
CHANGED
|
@@ -30,6 +30,10 @@ export const PROVIDERS: readonly { label: string; value: NativeProvider }[] = [
|
|
|
30
30
|
// and that channel vends Claude and GPT-OSS models alongside Gemini's own (see gemini-models.ts). A section
|
|
31
31
|
// headed "Gemini" holding Claude Opus would be a lie; "Google" is what the whole list has in common.
|
|
32
32
|
{ label: "Google", value: "gemini" },
|
|
33
|
+
// Cursor's own agent runtime, driven through the SDK Anysphere publishes, on the user's Cursor subscription.
|
|
34
|
+
// Like Google above, the label names the ACCOUNT rather than a model family: the channel vends Anthropic,
|
|
35
|
+
// OpenAI and xAI models alongside Cursor's own Composer, and no model name covers that list.
|
|
36
|
+
{ label: "Cursor", value: "cursor" },
|
|
33
37
|
];
|
|
34
38
|
|
|
35
39
|
// What it COSTS to unlock a provider, and what the user connects to do it, the axis the picker groups on, since
|
|
@@ -52,6 +56,11 @@ export const PROVIDER_ACCESS: Record<NativeProvider, ProviderAccess> = {
|
|
|
52
56
|
grok: { kind: "subscription", requirement: "SuperGrok subscription", runs: "Grok" },
|
|
53
57
|
kimi: { kind: "subscription", requirement: "Kimi Code subscription", runs: "Kimi Code" },
|
|
54
58
|
gemini: { kind: "free", requirement: "Google sign-in", runs: "Gemini, Claude and GPT-OSS under Claude Code" },
|
|
59
|
+
// A `subscription` like the first four, and the requirement names the PLAN rather than the account, because
|
|
60
|
+
// a free Cursor account signs in perfectly and still cannot run a turn here: the SDK behind this provider is
|
|
61
|
+
// gated to the paid tiers. Saying "Cursor account" would send someone to a sign-in that ends in a refusal
|
|
62
|
+
// they had no way to predict.
|
|
63
|
+
cursor: { kind: "subscription", requirement: "Cursor Pro subscription", runs: "Cursor Agent" },
|
|
55
64
|
};
|
|
56
65
|
|
|
57
66
|
/* THE PROVIDERS THAT COST NOTHING, derived from the table above rather than named a second time, and read by
|
|
@@ -59,14 +68,14 @@ export const PROVIDER_ACCESS: Record<NativeProvider, ProviderAccess> = {
|
|
|
59
68
|
*
|
|
60
69
|
* The distinction is worth the export. `accessBadge` answers "what does this row cost" for a row the user is
|
|
61
70
|
* already looking at; this answers "which row should a user who has connected nothing be shown FIRST", which is
|
|
62
|
-
* the connect gate's whole job. Ranking the one free channel
|
|
71
|
+
* the connect gate's whole job. Ranking the one free channel last among equal buttons is how a user with
|
|
63
72
|
* no subscription concluded the product needed one. Deriving the list keeps that promotion honest: a channel
|
|
64
73
|
* that stops being free stops being promoted, from one edit to PROVIDER_ACCESS. */
|
|
65
74
|
export const FREE_PROVIDERS: readonly NativeProvider[] = NATIVE_PROVIDERS.filter((provider) => PROVIDER_ACCESS[provider].kind === "free");
|
|
66
75
|
export const isFreeProvider = (provider: AgentProvider): boolean => FREE_PROVIDERS.includes(provider as NativeProvider);
|
|
67
76
|
|
|
68
77
|
/* WHOSE ALLOWANCE A TURN ON THIS PROVIDER SPENDS, as the subject of a sentence, a third naming of the same
|
|
69
|
-
*
|
|
78
|
+
* six ids, and the third is not redundancy. PROVIDERS names the RUNTIME the user picks ("Claude Code", "Kimi
|
|
70
79
|
* Code") and PROVIDER_ACCESS.requirement names the thing they CONNECT ("Claude subscription", "Google sign-in");
|
|
71
80
|
* neither reads as English in "… usage limit reached", and neither is what a spent quota belongs to.
|
|
72
81
|
*
|
|
@@ -80,6 +89,10 @@ export const PROVIDER_VENDOR: Record<NativeProvider, string> = {
|
|
|
80
89
|
grok: "xAI",
|
|
81
90
|
kimi: "Kimi Code",
|
|
82
91
|
gemini: "Google",
|
|
92
|
+
// The plan that gets billed is Cursor's, whichever vendor's model actually answered. A Cursor turn on Claude
|
|
93
|
+
// Opus spends Cursor's included usage and Anthropic has no part in it, the same reasoning that makes a
|
|
94
|
+
// `gemini` turn say "Google" above.
|
|
95
|
+
cursor: "Cursor",
|
|
83
96
|
};
|
|
84
97
|
|
|
85
98
|
// What a turn on this provider costs at the MARGIN, ordering the same three kinds by the only question a
|
|
@@ -190,8 +203,8 @@ export const HARNESSES: readonly { label: string; value: AgentHarness }[] = [
|
|
|
190
203
|
|
|
191
204
|
/* WHAT A PROVIDER/HARNESS PAIR CAN ACTUALLY DO, one declaration, read by both sides of the wire.
|
|
192
205
|
*
|
|
193
|
-
*
|
|
194
|
-
* loop, Codex app-server, OpenCode, any ACP agent, and Pi's RPC surface. They do NOT do the same things, and for a long time
|
|
206
|
+
* Six runtimes serve turns behind one seam (AgentRequest in, AgentEvent frames out): the Claude Code Agent SDK
|
|
207
|
+
* loop, Codex app-server, OpenCode, Cursor's own loop run in-process, any ACP agent, and Pi's RPC surface. They do NOT do the same things, and for a long time
|
|
195
208
|
* the only thing that said so was a comment inside each adapter, "Ignores the Claude-only request fields",
|
|
196
209
|
* which no surface above it could read. So the composer offered "Ask before each file edit" on a runtime whose
|
|
197
210
|
* every tool call is pre-approved, and offered a reasoning-effort scale to a runtime that drops the field.
|
|
@@ -217,7 +230,7 @@ export interface AgentCapabilities {
|
|
|
217
230
|
// `opencode-gemini` is the OpenCode loop pointed at Gemini rather than at xAI, and it is a SEPARATE runtime
|
|
218
231
|
// id from `opencode` on purpose: adapter health is keyed by this field (adapter-health.ts), so sharing one
|
|
219
232
|
// would make Grok's xAI credential decide whether the picker greys out Gemini, and the reverse.
|
|
220
|
-
readonly runtime: "claude-code" | "codex" | "opencode" | "opencode-gemini" | "acp" | "pi";
|
|
233
|
+
readonly runtime: "claude-code" | "codex" | "opencode" | "opencode-gemini" | "acp" | "pi" | "cursor";
|
|
221
234
|
// Mid-turn injection (the SteeringQueue behind /agent/steer). Needs the SDK's streaming-input mode.
|
|
222
235
|
readonly steering: boolean;
|
|
223
236
|
// How much of the permission-mode axis the runtime honours. "modes" = every PermissionMode, with per-tool
|
|
@@ -226,12 +239,20 @@ export interface AgentCapabilities {
|
|
|
226
239
|
readonly permissions: "modes" | "plan";
|
|
227
240
|
// Can stop mid-turn and ask the user a multiple-choice question (`question` frames).
|
|
228
241
|
readonly questions: boolean;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
242
|
+
/* Which of the turn's tools reach the agent. "full" = http MCP tools + in-process SDK servers + plugin
|
|
243
|
+
* checkouts + the browser servers; "tools" = all of that EXCEPT plugin checkouts; "browser" = the
|
|
244
|
+
* process-backed browser servers alone; "http" = the http MCP tools alone, and only if the agent advertises
|
|
245
|
+
* http MCP support; "none" = the runtime has no seam for them at all. Keeping the partial answers distinct
|
|
246
|
+
* matters: a runtime that can drive a connected account must not be described as tool-less, and one that
|
|
247
|
+
* cannot host daemon-side SDK servers must not claim full.
|
|
248
|
+
*
|
|
249
|
+
* "tools" exists for the Cursor runtime and would have been a lie either way without it. Cursor's SDK takes
|
|
250
|
+
* stdio AND http/sse MCP servers, and its `customTools` run host callbacks in this process, which is the
|
|
251
|
+
* seam an in-process SDK server needs, so calling it "browser" would understate it by three whole
|
|
252
|
+
* categories. What it genuinely cannot host is a Claude Code PLUGIN checkout: that is a directory layout the
|
|
253
|
+
* Agent SDK loads, not a protocol, and no other runtime will ever read one. So the gap is real, permanent
|
|
254
|
+
* and worth its own word rather than being rounded to "full". */
|
|
255
|
+
readonly mcp: "full" | "tools" | "browser" | "http" | "none";
|
|
235
256
|
/* WHICH EXECUTION BACKENDS THE RUNTIME HOSTS, the ways a turn RUNS things, as opposed to the tools it is
|
|
236
257
|
* handed. "shell" is the runtime's own command tool (Bash on the Claude Code loop, each foreign loop's
|
|
237
258
|
* equivalent); "js" is the sandbox's JavaScript backend (execution/ in the daemon): the model writes a
|
|
@@ -468,7 +489,7 @@ const ACP: AgentCapabilities = {
|
|
|
468
489
|
secrets: "none",
|
|
469
490
|
};
|
|
470
491
|
|
|
471
|
-
/* THE PI CAPABILITY ID IS RESERVED, the same way the
|
|
492
|
+
/* THE PI CAPABILITY ID IS RESERVED, the same way the six native ids are: an `agent`-kind capability installed
|
|
472
493
|
* under it is served over Pi's own RPC protocol rather than ACP. Pi closed ACP support deliberately (its RPC
|
|
473
494
|
* mode is the embedding surface), and the two want different records. A bare id rather than a namespace like
|
|
474
495
|
* `endpoint/`, because there is exactly one Pi runtime to name; capabilitiesOf still answers from the id alone,
|
|
@@ -504,6 +525,86 @@ const PI: AgentCapabilities = {
|
|
|
504
525
|
secrets: "none",
|
|
505
526
|
};
|
|
506
527
|
|
|
528
|
+
/* CURSOR'S OWN AGENT RUNTIME, driven through `@cursor/sdk`, the SDK Anysphere publishes, in this daemon's own
|
|
529
|
+
* process. The second-richest row in this file after the Claude Code loop, and the reason is the SDK rather
|
|
530
|
+
* than the vendor: it is an EMBEDDING surface, not a CLI wrapped in a pipe, so most of the seams the other
|
|
531
|
+
* foreign runtimes lack are simply function arguments here.
|
|
532
|
+
*
|
|
533
|
+
* WHY NOT THROUGH OPENCODE, which is already in this image and already serves two providers. Every Cursor
|
|
534
|
+
* bridge for OpenCode is a community reverse-engineering of Cursor's private agent RPC or a localhost shim
|
|
535
|
+
* around its CLI, and the OPENCODE record above is the weakest in this file. Routing Cursor through it would
|
|
536
|
+
* have capped a first-party SDK at Grok's ceiling and made the row depend on a third party's spare time.
|
|
537
|
+
*
|
|
538
|
+
* WHY THE HARNESS AXIS DOESN'T APPLY, the same way it doesn't for Gemini, and for the mirror-image reason.
|
|
539
|
+
* Gemini has no Claude Code route because Google refuses that traffic; Cursor has none because there is no
|
|
540
|
+
* translator route at all, CLIProxyAPI does not serve Cursor as a provider (asked for repeatedly upstream and
|
|
541
|
+
* closed as not planned), and Cursor publishes no OpenAI-compatible endpoint on a subscription. The SDK IS the
|
|
542
|
+
* only door, so `capabilitiesOf` answers this record whatever harness the client sent.
|
|
543
|
+
*
|
|
544
|
+
* The three axes below that read weaker than they could are deliberate, not unfinished: see the notes on each. */
|
|
545
|
+
const CURSOR: AgentCapabilities = {
|
|
546
|
+
runtime: "cursor",
|
|
547
|
+
// The SDK's Run can be cancelled but not written to mid-flight: a second `send` on a busy agent is an
|
|
548
|
+
// AgentBusyError, not an injection. So the steering queue has nowhere to go and the composer hides it.
|
|
549
|
+
steering: false,
|
|
550
|
+
/* Cursor's OWN plan mode (`mode: "agent" | "plan"`), not this repo's two-phase emulation, which is the
|
|
551
|
+
* better version of the same bargain: the model is put in a read-only posture by the vendor rather than
|
|
552
|
+
* being asked to behave.
|
|
553
|
+
*
|
|
554
|
+
* Not "modes", and that is the honest half. The hook seam below can gate shell, MCP, file reads and file
|
|
555
|
+
* edits, which is most of the tool surface but not all of it, and a per-tool posture with a silent gap in
|
|
556
|
+
* it is worse than one that says where it stops. */
|
|
557
|
+
permissions: "plan",
|
|
558
|
+
/* TRUE BECAUSE WE SUPPLY THE TOOL, not because Cursor's own askQuestion is wired. That one is put in
|
|
559
|
+
* `disallowedTools`: in a headless run it has been reported to answer itself with a fabricated "Questions
|
|
560
|
+
* skipped by the user", which is the single worst failure shape available here, an agent acting on consent
|
|
561
|
+
* nobody gave. The ask tool the daemon registers through `customTools` runs in this process, parks on a
|
|
562
|
+
* real card, and cannot invent an answer because it is the thing that receives one. */
|
|
563
|
+
questions: true,
|
|
564
|
+
// stdio + http/sse MCP servers, plus host callbacks through `customTools` (which is where the browser stack
|
|
565
|
+
// and the in-process SDK servers land). Everything but a Claude Code plugin checkout, see the axis note.
|
|
566
|
+
mcp: "tools",
|
|
567
|
+
execution: ["shell"],
|
|
568
|
+
/* Cursor publishes effort as MODEL PARAMETERS rather than as one scale (`ModelListItem.parameters` /
|
|
569
|
+
* `variants` → `ModelSelection.params`), so the shared tiers are mapped onto whatever the selected model
|
|
570
|
+
* declares, and a model that declares none simply offers no control. True here because the axis is
|
|
571
|
+
* forwardable at all; which tiers exist is the live catalog's answer, not this record's. */
|
|
572
|
+
effort: true,
|
|
573
|
+
fastMode: false,
|
|
574
|
+
/* "cwd", and this is the one place the SDK's in-process design costs something. A namespace is built around
|
|
575
|
+
* a CHILD the daemon spawns (that is how the Claude Code loop and Codex app-server get theirs); Cursor's
|
|
576
|
+
* loop runs inside the daemon, whose own /work must stay the shared checkout, so an isolated conversation
|
|
577
|
+
* gets its worktree by working directory and the turn is told where its tree is (turn-preamble.ts). */
|
|
578
|
+
isolation: "cwd",
|
|
579
|
+
// Cursor's commands are files on disk (`.cursor/commands`), which the SDK loads but does not publish back,
|
|
580
|
+
// so there is no list to hand the `/` popover.
|
|
581
|
+
commands: false,
|
|
582
|
+
// The SDK runs its shell in-process; there is no tmux session for the terminal panel to attach to.
|
|
583
|
+
terminals: false,
|
|
584
|
+
// The SDK throws typed errors (RateLimitError and friends) rather than dissolving a refusal into prose, so
|
|
585
|
+
// the adapter can file the coded frames auto-resume keys off.
|
|
586
|
+
recovery: true,
|
|
587
|
+
/* "append", the OpenCode answer, reached by a completely different road. There is no system-prompt argument
|
|
588
|
+
* on `Agent.create`; what there is, is the `beforeSubmitPrompt` hook, whose reply carries
|
|
589
|
+
* `additional_context` that is folded into the request. So the owner's prompt and the persona note DO reach
|
|
590
|
+
* the model, on top of Cursor's own base prompt, and nothing can replace that base. */
|
|
591
|
+
instructions: "append",
|
|
592
|
+
/* THE FULL HOOK TIER, the only foreign runtime that reaches it. Cursor reads `.cursor/hooks.json` in its
|
|
593
|
+
* local runtime, and `beforeShellExecution` answers with `allow` / `deny` / `ask` plus the messages that
|
|
594
|
+
* explain it, with `failClosed` available so a crashed gate blocks instead of waving the command through.
|
|
595
|
+
*
|
|
596
|
+
* What earns "hooks" rather than "approval" is that a HOLD can genuinely park: the hook is a process the
|
|
597
|
+
* daemon wrote, so it blocks on the card and the vendor is simply waiting on a script, exactly the shape
|
|
598
|
+
* that makes the Claude Code loop's PreToolUse hook able to stop and ask. The vendor never decides which
|
|
599
|
+
* calls to raise, either, which is the caveat the "approval" tier carries and this one does not. */
|
|
600
|
+
rulebook: "hooks",
|
|
601
|
+
/* "none", and structurally so, like every other foreign runtime. Masking needs a seam that rewrites what
|
|
602
|
+
* the model READS after a tool ran; Cursor's `afterShellExecution` fires with the output but its reply is
|
|
603
|
+
* discarded upstream, and `beforeReadFile` sees the content only to allow or deny it. Both are gates, not
|
|
604
|
+
* filters, so there is nothing here to substitute a reference back into. */
|
|
605
|
+
secrets: "none",
|
|
606
|
+
};
|
|
607
|
+
|
|
507
608
|
// The pair → its record. An `endpoint/<id>` provider is a model API the user configured, driven BY the Claude
|
|
508
609
|
// Code loop on either harness, so it gets that loop's full ceiling, which is the entire point of routing a
|
|
509
610
|
// model through it rather than adopting a second runtime. The reserved `pi` id is the Pi coding agent on its
|
|
@@ -528,6 +629,13 @@ export const capabilitiesOf = (provider: AgentProvider, harness: AgentHarness):
|
|
|
528
629
|
if (provider === "gemini") {
|
|
529
630
|
return OPENCODE_GEMINI;
|
|
530
631
|
}
|
|
632
|
+
// Cursor ignores the harness for the mirror of Gemini's reason: there is no route to it but its own SDK. No
|
|
633
|
+
// translator serves Cursor and Cursor publishes no model endpoint on a subscription, so "Cursor under Claude
|
|
634
|
+
// Code" names a road that does not exist. Answering this record whatever was asked for is what keeps that a
|
|
635
|
+
// fact of the catalog rather than a rule each surface has to remember.
|
|
636
|
+
if (provider === "cursor") {
|
|
637
|
+
return CURSOR;
|
|
638
|
+
}
|
|
531
639
|
if (isEndpointProvider(provider)) {
|
|
532
640
|
return CLAUDE_CODE;
|
|
533
641
|
}
|
|
@@ -565,7 +673,9 @@ export const limitationsOf = (capabilities: AgentCapabilities): string[] => [
|
|
|
565
673
|
? ["MCP tools only: no plugins or browser"]
|
|
566
674
|
: capabilities.mcp === "browser"
|
|
567
675
|
? ["browser tools only: no plugins or other MCP tools"]
|
|
568
|
-
:
|
|
676
|
+
: capabilities.mcp === "tools"
|
|
677
|
+
? ["no plugins: every other tool reaches it"]
|
|
678
|
+
: []),
|
|
569
679
|
...(capabilities.execution.includes("js") ? [] : ["no code runs, its shell is the one way to execute"]),
|
|
570
680
|
...(capabilities.effort ? [] : ["no effort control"]),
|
|
571
681
|
...(capabilities.commands ? [] : ["no slash commands"]),
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
AgentResumeAfterOutageSchema,
|
|
14
14
|
AgentSearchQuerySchema,
|
|
15
15
|
AgentSearchResultSchema,
|
|
16
|
+
AgentsArchivedSchema,
|
|
16
17
|
AgentsListSchema,
|
|
17
18
|
AgentsMovedSchema,
|
|
18
19
|
AgentsRemovedSchema,
|
|
@@ -233,7 +234,7 @@ export const agentsContract = {
|
|
|
233
234
|
"The gentle counterpart to discarding. Commits whatever the conversation still has in progress onto its own branch, releases its working copy, and keeps the entry and the record. It leaves the live fleet and joins the archive. Refused for a conversation that is running.",
|
|
234
235
|
})
|
|
235
236
|
.input(AgentArchiveSchema)
|
|
236
|
-
.output(
|
|
237
|
+
.output(AgentsArchivedSchema),
|
|
237
238
|
unarchive: oc
|
|
238
239
|
.route({
|
|
239
240
|
method: "POST",
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { oc } from "@orpc/contract";
|
|
2
|
+
import {
|
|
3
|
+
AccountIdSchema,
|
|
4
|
+
AccountListQuerySchema,
|
|
5
|
+
AccountRenameSchema,
|
|
6
|
+
CursorLoginCancelSchema,
|
|
7
|
+
CursorLoginStartSchema,
|
|
8
|
+
OauthAccountListSchema,
|
|
9
|
+
OauthAccountSchema,
|
|
10
|
+
OkSchema,
|
|
11
|
+
} from "../schemas.js";
|
|
12
|
+
|
|
13
|
+
/* Cursor subscription sign-in, the sandbox owns the credential and the platform never sees it, the same bargain
|
|
14
|
+
* claude.contract.ts strikes. What differs is who finishes the handshake.
|
|
15
|
+
*
|
|
16
|
+
* Claude's flow is start → the browser gets a code → `exchange` hands it back. Cursor's PKCE verifier is
|
|
17
|
+
* redeemable on its own, so it never leaves the daemon: `start` begins the flow, keeps the verifier in memory,
|
|
18
|
+
* polls Cursor until the browser completes the sign-in, mints a 90-day user key and writes it as an account.
|
|
19
|
+
* The caller opens the URL and then watches `accounts`, exactly as it does for a device login, which is why
|
|
20
|
+
* there is no `exchange` route here and a `cancel` one instead.
|
|
21
|
+
*
|
|
22
|
+
* The model catalog is deliberately absent, as it is for every provider: it is the one question they all answer
|
|
23
|
+
* identically, so it lives on the shared /providers/{provider}/models route (providers.contract.ts). */
|
|
24
|
+
export const cursorContract = {
|
|
25
|
+
start: oc
|
|
26
|
+
.route({
|
|
27
|
+
method: "POST",
|
|
28
|
+
path: "/cursor/login/start",
|
|
29
|
+
summary: "Begin connecting a Cursor account",
|
|
30
|
+
description:
|
|
31
|
+
"Hands back the page to sign in on. The sandbox finishes the handshake itself and stores the credential, so nothing has to be pasted back: watch the account list instead.",
|
|
32
|
+
})
|
|
33
|
+
.output(CursorLoginStartSchema),
|
|
34
|
+
cancel: oc
|
|
35
|
+
.route({
|
|
36
|
+
method: "POST",
|
|
37
|
+
path: "/cursor/login/cancel",
|
|
38
|
+
summary: "Abandon a Cursor sign-in",
|
|
39
|
+
description: "Stops waiting on a sign-in nobody completed. An abandoned attempt also expires on its own.",
|
|
40
|
+
})
|
|
41
|
+
.input(CursorLoginCancelSchema)
|
|
42
|
+
.output(OkSchema),
|
|
43
|
+
// Shaped like Claude's, including `force`, so the two account lists render through one component. Cursor
|
|
44
|
+
// publishes no account-wide headroom today, so `usage` stays absent on these rows and they draw a dot rather
|
|
45
|
+
// than a ring, which is what an unmeasured account has always meant here.
|
|
46
|
+
accounts: oc
|
|
47
|
+
.route({
|
|
48
|
+
method: "GET",
|
|
49
|
+
path: "/cursor/accounts",
|
|
50
|
+
summary: "Connected Cursor accounts",
|
|
51
|
+
description:
|
|
52
|
+
"Each connected account, and whether its stored key is still good. Cursor publishes no plan-wide allowance, so these rows carry no usage reading.",
|
|
53
|
+
})
|
|
54
|
+
.input(AccountListQuerySchema)
|
|
55
|
+
.output(OauthAccountListSchema),
|
|
56
|
+
rename: oc
|
|
57
|
+
.route({
|
|
58
|
+
method: "POST",
|
|
59
|
+
path: "/cursor/account/rename",
|
|
60
|
+
summary: "Rename a Cursor account",
|
|
61
|
+
description: "Changes the label one account shows under, so several are tellable apart.",
|
|
62
|
+
})
|
|
63
|
+
.input(AccountRenameSchema)
|
|
64
|
+
.output(OauthAccountSchema),
|
|
65
|
+
disconnect: oc
|
|
66
|
+
.route({
|
|
67
|
+
method: "POST",
|
|
68
|
+
path: "/cursor/account/disconnect",
|
|
69
|
+
summary: "Disconnect a Cursor account",
|
|
70
|
+
description: "Clears the stored key for one account. The others stay connected.",
|
|
71
|
+
})
|
|
72
|
+
.input(AccountIdSchema)
|
|
73
|
+
.output(OkSchema),
|
|
74
|
+
};
|
package/src/events.ts
CHANGED
|
@@ -879,11 +879,16 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
879
879
|
* is, so nothing is re-run and nothing asks the user to reconnect, the one recovery that looks
|
|
880
880
|
* plausible and is guaranteed to waste their time. */
|
|
881
881
|
"claude-not-entitled",
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
882
|
+
/* The model provider itself failed transiently: 500/502/503, a 529 at capacity, a dropped
|
|
883
|
+
* socket, and the harness's own in-turn retries did not outlast it. Nothing about the workspace
|
|
884
|
+
* or the request is wrong, so the daemon remembers the turn and re-runs it on an escalating
|
|
885
|
+
* backoff (provider-health.ts): the frame is a notice about a turn that is coming back, and
|
|
886
|
+
* reaches the client as a plain failure only once the attempts are spent.
|
|
887
|
+
*
|
|
888
|
+
* ONE 4xx JOINS THEM, the provider refusing a request PARAMETER nothing here sends (its own
|
|
889
|
+
* cache-retention default, or one a proxy added). It wears a client error's status code and is
|
|
890
|
+
* still a provider fault: there is no request of the user's to fix, and the same send goes
|
|
891
|
+
* through moments later, so it recovers the same way (agent/failure-sentences.ts). */
|
|
887
892
|
"provider-outage",
|
|
888
893
|
// The platform-owned free-trial pool failed after its bounded key walk. Unlike provider-outage,
|
|
889
894
|
// this is never auto-resumed: failed calls are refunded and the user's message is held to retry.
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { capabilitiesContract } from "./contracts/capabilities.contract.js";
|
|
|
8
8
|
import { choresContract } from "./contracts/chores.contract.js";
|
|
9
9
|
import { ciContract } from "./contracts/ci.contract.js";
|
|
10
10
|
import { claudeContract } from "./contracts/claude.contract.js";
|
|
11
|
+
import { cursorContract } from "./contracts/cursor.contract.js";
|
|
11
12
|
import { draftsContract } from "./contracts/drafts.contract.js";
|
|
12
13
|
import { endpointsContract } from "./contracts/endpoints.contract.js";
|
|
13
14
|
import { exitContract } from "./contracts/exit.contract.js";
|
|
@@ -46,6 +47,7 @@ export { capabilitiesContract } from "./contracts/capabilities.contract.js";
|
|
|
46
47
|
export { choresContract } from "./contracts/chores.contract.js";
|
|
47
48
|
export { ciContract } from "./contracts/ci.contract.js";
|
|
48
49
|
export { claudeContract } from "./contracts/claude.contract.js";
|
|
50
|
+
export { cursorContract } from "./contracts/cursor.contract.js";
|
|
49
51
|
export { draftsContract } from "./contracts/drafts.contract.js";
|
|
50
52
|
export { endpointsContract, type TrialHealth, TrialStatusSchema, type TrialStatusResponse } from "./contracts/endpoints.contract.js";
|
|
51
53
|
export { exitContract } from "./contracts/exit.contract.js";
|
|
@@ -132,6 +134,7 @@ export const sandboxContract = {
|
|
|
132
134
|
chores: choresContract,
|
|
133
135
|
ci: ciContract,
|
|
134
136
|
claude: claudeContract,
|
|
137
|
+
cursor: cursorContract,
|
|
135
138
|
drafts: draftsContract,
|
|
136
139
|
endpoints: endpointsContract,
|
|
137
140
|
extensions: extensionsContract,
|
package/src/schemas.ts
CHANGED
|
@@ -54,7 +54,7 @@ export const RepoParamSchema = z.object({
|
|
|
54
54
|
// ACP (Agent Client Protocol).
|
|
55
55
|
// Kept as a bare string on the wire (not an enum) so an unknown id is a clean error frame from the agent
|
|
56
56
|
// route, the same bet RepoParamSchema makes, and adding an ACP agent needs no contract change.
|
|
57
|
-
export const NATIVE_PROVIDERS = ["claude", "codex", "grok", "kimi", "gemini"] as const;
|
|
57
|
+
export const NATIVE_PROVIDERS = ["claude", "codex", "grok", "kimi", "gemini", "cursor"] as const;
|
|
58
58
|
export type NativeProvider = (typeof NATIVE_PROVIDERS)[number];
|
|
59
59
|
export const AgentProviderSchema = z.string().min(1);
|
|
60
60
|
export type AgentProvider = z.infer<typeof AgentProviderSchema>;
|
|
@@ -1367,6 +1367,22 @@ export const AgentsMovedSchema = z.object({
|
|
|
1367
1367
|
),
|
|
1368
1368
|
});
|
|
1369
1369
|
export type AgentsMoved = z.infer<typeof AgentsMovedSchema>;
|
|
1370
|
+
/* AN ARCHIVE ALSO REPORTS WHAT IT COULD NOT DO, which is the half that used to go missing. Releasing a working
|
|
1371
|
+
* copy is git work, and it can fail for reasons no press can fix: the repository behind a checkout was deleted
|
|
1372
|
+
* from the workspace, a checkout is locked. Those agents stay on the board, so the answer carries them and the
|
|
1373
|
+
* sentence each failed with, otherwise the only true thing the caller could say about an archive that moved
|
|
1374
|
+
* nothing was "there was nothing to archive": to a user looking straight at the card it refused. */
|
|
1375
|
+
export const AgentsArchivedSchema = AgentsMovedSchema.extend({
|
|
1376
|
+
failed: z
|
|
1377
|
+
.array(
|
|
1378
|
+
z.object({
|
|
1379
|
+
id: z.string().describe("Which conversation stayed on the board."),
|
|
1380
|
+
reason: z.string().describe("Why its working copy could not be released, in the words the failure came with."),
|
|
1381
|
+
}),
|
|
1382
|
+
)
|
|
1383
|
+
.describe("The conversations this press could not put away, each with the reason, so the board can say it instead of reporting silence."),
|
|
1384
|
+
});
|
|
1385
|
+
export type AgentsArchived = z.infer<typeof AgentsArchivedSchema>;
|
|
1370
1386
|
// What a purge actually deleted. Ids, not summaries: these agents no longer exist anywhere, there is nothing
|
|
1371
1387
|
// left to show and nothing to put back, so the only thing the caller can do with the answer is drop those rows
|
|
1372
1388
|
// and count them. No revision either: archived agents are already off the broadcast roster (see `list`), so a
|
|
@@ -1981,6 +1997,33 @@ export const AuthorizeChallengeSchema = z.object({
|
|
|
1981
1997
|
verifier: z.string().describe("Keep this and send it back when finishing. It is what proves the code that comes back belongs to this handshake."),
|
|
1982
1998
|
state: z.string().describe("The handshake's own id, sent back with it."),
|
|
1983
1999
|
});
|
|
2000
|
+
|
|
2001
|
+
/* CURSOR'S SIGN-IN START. A third login shape, and the reason it is not one of the two above is where the
|
|
2002
|
+
* SECRET lives during the handshake.
|
|
2003
|
+
*
|
|
2004
|
+
* Claude's is paste-back: the browser receives a code and the caller hands it plus its verifier to `exchange`,
|
|
2005
|
+
* so the handshake's proof has to travel on the wire and AuthorizeChallengeSchema carries it. Cursor's PKCE
|
|
2006
|
+
* verifier must never leave the process that generated it, anyone holding it can redeem the login and mint a
|
|
2007
|
+
* durable key, so the daemon starts the whole flow, keeps the verifier in memory, polls Cursor itself, and
|
|
2008
|
+
* writes the account when it lands. Nothing redeemable is on this shape at all.
|
|
2009
|
+
*
|
|
2010
|
+
* Which makes it behave like a DEVICE flow from the caller's side (open the page, then watch the account list),
|
|
2011
|
+
* except that there is no one-time code to display: the login page is addressed to this handshake already. So
|
|
2012
|
+
* DeviceStartSchema's `code` would be a permanently blank field on every card, and TranslatorStartSchema's
|
|
2013
|
+
* `state` a value nothing sends back. `handshake` is neither, it is a cancellation handle. */
|
|
2014
|
+
export const CursorLoginStartSchema = z.object({
|
|
2015
|
+
url: z.string().describe("The page to open and sign in on. It is already addressed to this attempt, so there is no code to type."),
|
|
2016
|
+
handshake: z
|
|
2017
|
+
.string()
|
|
2018
|
+
.describe(
|
|
2019
|
+
"This attempt's id, for abandoning it. Not a credential and not redeemable: the proof that finishes the sign-in never leaves the sandbox.",
|
|
2020
|
+
),
|
|
2021
|
+
expiresAt: z.number().describe("When this attempt stops being answerable, in milliseconds, so a card can stop waiting instead of spinning."),
|
|
2022
|
+
});
|
|
2023
|
+
export type CursorLoginStart = z.infer<typeof CursorLoginStartSchema>;
|
|
2024
|
+
// Abandon a sign-in nobody completed, so the daemon stops polling Cursor for it. Ordinary tidiness rather than
|
|
2025
|
+
// a security boundary: an unanswered attempt also times out on its own (see `expiresAt`).
|
|
2026
|
+
export const CursorLoginCancelSchema = z.object({ handshake: z.string().min(1).describe("Which attempt to stop waiting on.") });
|
|
1984
2027
|
// xAI Grok (via OpenCode) uses subscription OAuth via the headless device-code method. `start` returns the
|
|
1985
2028
|
// `url` the user opens (xAI's verification_uri_complete, which pre-fills the code) and `code`, the same
|
|
1986
2029
|
// one-time code, surfaced so the card matches x.ai exactly. There is no paste-back: OpenCode polls to
|
|
@@ -4977,11 +5020,48 @@ export const EndpointConfigSchema = z.object({
|
|
|
4977
5020
|
*
|
|
4978
5021
|
* `gpu` mirrors the docker card's option and rides the same allowlisted `--gpus=all` directive: the ASK lives
|
|
4979
5022
|
* here, what became of it is SANDBOX_GPU, stamped by the runner (see the docker handler's gpuState). "on"/"off"
|
|
4980
|
-
* rather than a boolean for the manifest-wide reason DockerConfigSchema gives.
|
|
5023
|
+
* rather than a boolean for the manifest-wide reason DockerConfigSchema gives.
|
|
5024
|
+
*
|
|
5025
|
+
* `context`/`contextTokens` are HOW MUCH CONVERSATION the server holds, the `model`/`url` pair's shape for the
|
|
5026
|
+
* same reason: a short list of rungs anyone can choose between, and one escape hatch for a person who knows the
|
|
5027
|
+
* exact number they want. Resolved to a single token count in exactly one place (the daemon's
|
|
5028
|
+
* endpoints/local-model.ts localModelWindow), because the flag llama-server is started with and the number the
|
|
5029
|
+
* card promises must never be two opinions. */
|
|
5030
|
+
export const LOCAL_MODEL_WINDOWS = ["16384", "32768", "65536", "131072"] as const;
|
|
5031
|
+
export type LocalModelWindow = (typeof LOCAL_MODEL_WINDOWS)[number];
|
|
5032
|
+
|
|
5033
|
+
/* THE RUNG A CARD WITH NO OPINION LANDS ON, and the one number in this block that is a product decision rather
|
|
5034
|
+
* than an arithmetic one.
|
|
5035
|
+
*
|
|
5036
|
+
* It is 65,536 because this sandbox runs a TOOL-CALLING AGENT LOOP, and that loop's own fixed cost, its
|
|
5037
|
+
* instructions plus one JSON schema per tool it can call, times every capability the owner has connected, is
|
|
5038
|
+
* tens of thousands of tokens before the user has typed anything (agent/context-budget.ts holds the measurement
|
|
5039
|
+
* and the refusal built on it). A window that cannot hold that cost is not a smaller version of the product; it
|
|
5040
|
+
* is a model whose every real turn is refused, which is what the previous flat 32,768 shipped: a 27B model,
|
|
5041
|
+
* seventeen gigabytes downloaded, and a first message that died on `36216 tokens exceeds 32768`.
|
|
5042
|
+
*
|
|
5043
|
+
* So the default is the smallest rung a full turn fits in, and the smaller rungs stay on the list because they
|
|
5044
|
+
* are honestly useful: pinned as the quick model (titles, commit messages) a window this size is waste, and the
|
|
5045
|
+
* gigabyte it gives back is the difference between running one of these models on an eight-gigabyte laptop and
|
|
5046
|
+
* not. What each rung costs in memory is the card's job to say (capability-catalog): roughly 2 GB of quantized
|
|
5047
|
+
* cache per 32k of window, on top of the weights. */
|
|
5048
|
+
export const LOCAL_MODEL_WINDOW_DEFAULT: LocalModelWindow = "65536";
|
|
5049
|
+
|
|
5050
|
+
/* THE BOUNDS ON THE TYPED NUMBER, and they are bounds against a TYPO rather than against a preference. Below
|
|
5051
|
+
* the floor there is no conversation left to have once the loop's own instructions land; above the ceiling is a
|
|
5052
|
+
* number no GGUF on offer was trained for, and llama-server would spend minutes reserving a cache for it before
|
|
5053
|
+
* failing. Everything between is the owner's call: their machine, their memory. */
|
|
5054
|
+
export const LOCAL_MODEL_WINDOW_MIN = 2048;
|
|
5055
|
+
export const LOCAL_MODEL_WINDOW_MAX = 1_048_576;
|
|
5056
|
+
|
|
4981
5057
|
export const LocalModelConfigSchema = z.object({
|
|
4982
5058
|
model: z.string().min(1),
|
|
4983
5059
|
gpu: z.enum(["on", "off"]).default("off"),
|
|
4984
5060
|
url: z.url().optional(),
|
|
5061
|
+
context: z.union([z.enum(LOCAL_MODEL_WINDOWS), z.literal("custom")]).default(LOCAL_MODEL_WINDOW_DEFAULT),
|
|
5062
|
+
// Coerced because it arrives from a text field as a string, the ssh card's `port` precedent, and only read
|
|
5063
|
+
// when `context` is "custom" (the `url`/`model` relationship exactly).
|
|
5064
|
+
contextTokens: z.coerce.number().int().min(LOCAL_MODEL_WINDOW_MIN).max(LOCAL_MODEL_WINDOW_MAX).optional(),
|
|
4985
5065
|
});
|
|
4986
5066
|
export type LocalModelConfig = z.infer<typeof LocalModelConfigSchema>;
|
|
4987
5067
|
/* THE SANDBOX WALLET, a USDC balance the agent can spend on x402-payable endpoints, under owner policy.
|