@intentic/sandbox-contract 1.226.1 → 1.228.0
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/agent.contract.d.ts +19 -2
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.d.ts +62 -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/endpoints.contract.d.ts +1 -0
- package/dist/contracts/endpoints.contract.d.ts.map +1 -1
- package/dist/contracts/extensions.contract.d.ts +1 -0
- package/dist/contracts/extensions.contract.d.ts.map +1 -1
- package/dist/contracts/logs.contract.d.ts +17 -0
- package/dist/contracts/logs.contract.d.ts.map +1 -1
- package/dist/contracts/logs.contract.js +10 -1
- package/dist/contracts/logs.contract.js.map +1 -1
- package/dist/contracts/providers.contract.d.ts +2 -0
- package/dist/contracts/providers.contract.d.ts.map +1 -1
- package/dist/contracts/sessions.contract.d.ts +3 -0
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +19 -0
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +21 -14
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/events.d.ts +46 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +14 -0
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +271 -70
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/prompt-complexity.d.ts +9 -1
- package/dist/prompt-complexity.d.ts.map +1 -1
- package/dist/prompt-complexity.js +13 -5
- package/dist/prompt-complexity.js.map +1 -1
- package/dist/request-id.d.ts +3 -0
- package/dist/request-id.d.ts.map +1 -0
- package/dist/request-id.js +3 -0
- package/dist/request-id.js.map +1 -0
- package/dist/schemas.d.ts +268 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +79 -1
- package/dist/schemas.js.map +1 -1
- package/dist/starter.d.ts +3 -0
- package/dist/starter.d.ts.map +1 -0
- package/dist/starter.js +3 -0
- package/dist/starter.js.map +1 -0
- package/package.json +6 -5
- package/src/agent-catalog.test.ts +2 -2
- package/src/agent-catalog.ts +123 -13
- package/src/contract-lock.test.ts +8 -2
- package/src/contracts/agents.contract.ts +2 -1
- package/src/contracts/cursor.contract.ts +74 -0
- package/src/contracts/logs.contract.ts +23 -2
- package/src/events.ts +62 -6
- package/src/index.ts +5 -0
- package/src/prompt-complexity.test.ts +69 -0
- package/src/prompt-complexity.ts +102 -39
- package/src/request-id.ts +41 -0
- package/src/schemas.ts +276 -2
- package/src/starter.ts +13 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"starter.d.ts","sourceRoot":"","sources":["../src/starter.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,YAAY,SAAS,CAAC;AACnC,eAAO,MAAM,WAAW,YAAY,CAAC"}
|
package/dist/starter.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"starter.js","sourceRoot":"","sources":["../src/starter.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC;AACnC,MAAM,CAAC,MAAM,WAAW,GAAG,SAAS,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentic/sandbox-contract",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.228.0",
|
|
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,15 +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/extension-manifest": "1.
|
|
94
|
-
"@intentic/registry": "1.
|
|
92
|
+
"@intentic/constants": "1.228.0",
|
|
93
|
+
"@intentic/extension-manifest": "1.228.0",
|
|
94
|
+
"@intentic/registry": "1.228.0"
|
|
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/tsconfig": "0.0.0"
|
|
100
|
+
"@intentic/tsconfig": "0.0.0",
|
|
101
|
+
"@intentic/testing": "0.0.0"
|
|
101
102
|
},
|
|
102
103
|
"scripts": {
|
|
103
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"]),
|
|
@@ -5,8 +5,14 @@ import { currentLock } from "./contract-lock.js";
|
|
|
5
5
|
/* The committed lock and the code must say the same thing: see contract-lock.ts for what the pair buys.
|
|
6
6
|
*
|
|
7
7
|
* This is the HALF that runs everywhere the tests run; the other half (a shrunk lock needs a declared break)
|
|
8
|
-
* lives in prepass.mjs, which has git and this suite does not.
|
|
9
|
-
|
|
8
|
+
* lives in prepass.mjs, which has git and this suite does not.
|
|
9
|
+
*
|
|
10
|
+
* ITS OWN BUDGET, because the default one is a HANG DETECTOR and this test does real work: it serializes every
|
|
11
|
+
* schema this package exports, ~500 of them, to JSON Schema. That is ~100ms with the machine to itself and it
|
|
12
|
+
* measured 8.8s on a runner running every package's suite at once, so vitest's 5s default failed it as a hang
|
|
13
|
+
* over a contract that had not moved: green on a box, red on a busy runner, the trap _tools/testing/src/vitest
|
|
14
|
+
* .ts is written against. 30s is well clear of the work and still reports a genuine hang inside half a minute. */
|
|
15
|
+
test("contract.lock.json matches the schemas this package exports", { timeout: 30_000 }, () => {
|
|
10
16
|
const committed: unknown = JSON.parse(readFileSync(new URL("../contract.lock.json", import.meta.url), "utf8"));
|
|
11
17
|
expect(
|
|
12
18
|
currentLock(),
|
|
@@ -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
|
+
};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { oc } from "@orpc/contract";
|
|
2
|
-
import { LogReadQuerySchema, LogReadSchema, LogsListSchema } from "../schemas.js";
|
|
2
|
+
import { ClientDiagnosticsAcceptedSchema, ClientDiagnosticsReportSchema, LogReadQuerySchema, LogReadSchema, LogsListSchema } from "../schemas.js";
|
|
3
3
|
|
|
4
4
|
// Daemon-owned debug logs (historyRoot/logs): terminal pipe-pane captures, intentic CLI run logs, daemon.log.
|
|
5
|
-
//
|
|
5
|
+
// Reads are read-only by design, the files are written by the daemon/tmux only, so the record stays
|
|
6
|
+
// trustworthy. `report` is the one write, and the exception that proves the rule: the browser is the only
|
|
7
|
+
// witness to its own crashes, and a record it cannot reach is a record that has nothing to say about them.
|
|
6
8
|
export const logsContract = {
|
|
7
9
|
list: oc
|
|
8
10
|
.route({
|
|
@@ -22,4 +24,23 @@ export const logsContract = {
|
|
|
22
24
|
})
|
|
23
25
|
.input(LogReadQuerySchema)
|
|
24
26
|
.output(LogReadSchema),
|
|
27
|
+
/* What the browser saw. The counterpart to every other route here: those serve what the daemon wrote down,
|
|
28
|
+
* this accepts what only the browser could have known.
|
|
29
|
+
*
|
|
30
|
+
* A WRITE from the client, which every other log route deliberately is not, and the trust argument is the
|
|
31
|
+
* inverse of theirs: the reads are trustworthy because only the daemon writes them, and this is trustworthy
|
|
32
|
+
* only about ITSELF. So it lands in its own file, never daemon.log, and its lines say plainly that a browser
|
|
33
|
+
* said them. It floors at viewer rather than the maintainer the logs prefix takes, because a viewer whose
|
|
34
|
+
* page just white-screened is exactly who needs to be able to report it, and cannot raise their own role to
|
|
35
|
+
* do it. */
|
|
36
|
+
report: oc
|
|
37
|
+
.route({
|
|
38
|
+
method: "POST",
|
|
39
|
+
path: "/logs/client",
|
|
40
|
+
summary: "Report what the browser saw",
|
|
41
|
+
description:
|
|
42
|
+
"Errors the app caught, stalls it measured, and recoveries it performed, written to a log of their own. The browser is the only witness to these, so without it a bug someone hit in their own browser leaves no record at all.",
|
|
43
|
+
})
|
|
44
|
+
.input(ClientDiagnosticsReportSchema)
|
|
45
|
+
.output(ClientDiagnosticsAcceptedSchema),
|
|
25
46
|
};
|
package/src/events.ts
CHANGED
|
@@ -387,6 +387,19 @@ export const RestoredMessageSchema = z.object({
|
|
|
387
387
|
.describe(
|
|
388
388
|
"A person wrote this in the agent's voice, with no turn behind it. Marked for the human re-reading the conversation months later, so their own words do not pass as the agent's. The agent itself never sees the mark.",
|
|
389
389
|
),
|
|
390
|
+
/* THE ONE-PRESS OFFER A RECORDED NOTICE CARRIES (notice rows only), named rather than inferred from its
|
|
391
|
+
* words. Only `tierHold` today: the line saying this turn ran on a cheaper model, whose offer is "keep this
|
|
392
|
+
* chat on my pick".
|
|
393
|
+
*
|
|
394
|
+
* It is on the wire because the offer has to survive a reopen, and a reopened tab has only the record. The
|
|
395
|
+
* chat's other one-press notices are drawn live and never recorded, so they never needed this; a routed turn
|
|
396
|
+
* is different precisely because the whole point of recording it is that somebody reads it LATER. A KIND, not
|
|
397
|
+
* a callback, exactly as the live ones are: the reader decides what the press does and whether the offer
|
|
398
|
+
* still stands (a chat already holding its pick shows a settled sentence, not a stale button). */
|
|
399
|
+
noticeAction: z
|
|
400
|
+
.enum(["tierHold"])
|
|
401
|
+
.optional()
|
|
402
|
+
.describe("A one-press follow-up this recorded notice offers, by name. The chat decides what it does and whether it still applies."),
|
|
390
403
|
});
|
|
391
404
|
export type RestoredMessage = z.infer<typeof RestoredMessageSchema>;
|
|
392
405
|
|
|
@@ -667,6 +680,33 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
667
680
|
// wasn't asked for.
|
|
668
681
|
reason: z.string().optional(),
|
|
669
682
|
}),
|
|
683
|
+
/* WHAT THE COMPLEXITY JUDGE MADE OF THIS TURN, emitted once at turn start on every judged turn (that is,
|
|
684
|
+
* whenever settings.autoTier is not "off"), for the same reason fast_mode exists: a mechanism that can
|
|
685
|
+
* change what a turn runs on fails silently unless the daemon says what it decided. One tiny frame per
|
|
686
|
+
* turn, deliberately on the standard verdicts too, because the client's composer preview needs the
|
|
687
|
+
* conversation's LAST verdict (prompt-complexity.ts `afterHardTurn`) and a frame only on the interesting
|
|
688
|
+
* turns would leave it guessing on the common ones.
|
|
689
|
+
*
|
|
690
|
+
* `tier`/`score`/`rules` are the verdict verbatim (judgeComplexity): the rules are the named-feature
|
|
691
|
+
* vocabulary of ComplexityRule, carried as strings so a frame from a build with a rule this client hasn't
|
|
692
|
+
* heard of still parses. `model` is present only when a substitution actually applies to THIS turn, which
|
|
693
|
+
* is `routed` (mode on, verdict fast, something cheaper published) or `held` (the same turn the user pinned
|
|
694
|
+
* to their pick, see AgentTurn.tierHold): measure mode never names one because naming it would cost the
|
|
695
|
+
* catalog read shadow mode exists to avoid.
|
|
696
|
+
*
|
|
697
|
+
* `routed` is what HAPPENED, never implied by the verdict: a fast verdict in measure mode, under a hold, or
|
|
698
|
+
* with nothing cheaper published all run the user's own pick and say `routed: false`. */
|
|
699
|
+
z.object({
|
|
700
|
+
kind: z.literal("tier"),
|
|
701
|
+
tier: z.enum(["fast", "standard"]),
|
|
702
|
+
score: z.number(),
|
|
703
|
+
rules: z.array(z.string()),
|
|
704
|
+
// The cheaper model this turn ran on (routed) or would have run on (held). Absent otherwise.
|
|
705
|
+
model: z.string().optional(),
|
|
706
|
+
routed: z.boolean(),
|
|
707
|
+
// The user pinned this turn to their pick (AgentTurn.tierHold), so a fast verdict moved nothing.
|
|
708
|
+
held: z.boolean().optional(),
|
|
709
|
+
}),
|
|
670
710
|
/* The turn is alive but WAITING on the provider: a request failed transiently (5xx, 529, a dropped socket)
|
|
671
711
|
* and the harness is retrying it inside this same turn. A status, not a failure, nothing has been lost and
|
|
672
712
|
* the turn may still finish normally, so the client renders it where "thinking" goes rather than in the
|
|
@@ -677,7 +717,10 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
677
717
|
* a hang. The one action a user takes against an apparent hang is Stop, which is the only action that
|
|
678
718
|
* actually loses the work, so the wait has to be visible, with its own next-attempt clock.
|
|
679
719
|
*
|
|
680
|
-
* `attempt
|
|
720
|
+
* `attempt` is the harness's own counter and `maxAttempts` is the bound that will actually be honoured,
|
|
721
|
+
* which on the Claude path is the daemon's own cap on how deep a storm may get rather than the harness's
|
|
722
|
+
* far longer budget (MAX_IN_TURN_RETRIES in sdk-stream.ts, which ends the turn at the cap and hands the
|
|
723
|
+
* waiting to the outage breaker). `nextAttemptAt` (epoch ms) is when it will try
|
|
681
724
|
* again, so the readout counts down instead of freezing on a number nobody can interpret. BOTH are optional
|
|
682
725
|
* for the same reason, which is that each runtime publishes a different half of the wait and none of them
|
|
683
726
|
* publishes all of it: Claude's harness reports the delay and the bound, Codex says which attempt it is on
|
|
@@ -836,11 +879,16 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
836
879
|
* is, so nothing is re-run and nothing asks the user to reconnect, the one recovery that looks
|
|
837
880
|
* plausible and is guaranteed to waste their time. */
|
|
838
881
|
"claude-not-entitled",
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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). */
|
|
844
892
|
"provider-outage",
|
|
845
893
|
// The platform-owned free-trial pool failed after its bounded key walk. Unlike provider-outage,
|
|
846
894
|
// this is never auto-resumed: failed calls are refunded and the user's message is held to retry.
|
|
@@ -855,6 +903,14 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
855
903
|
"unknown-command",
|
|
856
904
|
"grok-model-invalid",
|
|
857
905
|
"codex-model-invalid",
|
|
906
|
+
/* THE MODEL CANNOT HOLD A TURN OF THIS AGENT LOOP, so the daemon refused before sending
|
|
907
|
+
* (agent/context-budget.ts). Its own code because none of the neighbours describes it: nothing is
|
|
908
|
+
* disconnected, nothing is spent, nothing comes back on a clock, and re-sending the same request
|
|
909
|
+
* at the same model fails identically forever. What changes the outcome is the model or the
|
|
910
|
+
* server's context flag, so the message names both and the client HOLDS the words: they never
|
|
911
|
+
* reached anything, and losing them to a configuration fact would be the one part of this that
|
|
912
|
+
* was our fault. */
|
|
913
|
+
"context-window-too-small",
|
|
858
914
|
"subscription-required",
|
|
859
915
|
"agent-busy",
|
|
860
916
|
])
|
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";
|
|
@@ -60,6 +62,7 @@ export { hostContract } from "./contracts/host.contract.js";
|
|
|
60
62
|
export { intenticContract } from "./contracts/intentic.contract.js";
|
|
61
63
|
export { inventoryContract } from "./contracts/inventory.contract.js";
|
|
62
64
|
export { logsContract } from "./contracts/logs.contract.js";
|
|
65
|
+
export { REQUEST_ID_EVIDENCE_ROUTE, REQUEST_ID_HEADER } from "./request-id.js";
|
|
63
66
|
export { loopsContract } from "./contracts/loops.contract.js";
|
|
64
67
|
export { panelsContract } from "./contracts/panels.contract.js";
|
|
65
68
|
export { portsContract } from "./contracts/ports.contract.js";
|
|
@@ -113,6 +116,7 @@ export * from "./output-fields.js";
|
|
|
113
116
|
export * from "./publish-drafts.js";
|
|
114
117
|
export * from "./schemas.js";
|
|
115
118
|
export * from "./search-globs.js";
|
|
119
|
+
export * from "./starter.js";
|
|
116
120
|
export * from "./terminal-protocol.js";
|
|
117
121
|
export * from "./title.js";
|
|
118
122
|
export * from "./versions.js";
|
|
@@ -130,6 +134,7 @@ export const sandboxContract = {
|
|
|
130
134
|
chores: choresContract,
|
|
131
135
|
ci: ciContract,
|
|
132
136
|
claude: claudeContract,
|
|
137
|
+
cursor: cursorContract,
|
|
133
138
|
drafts: draftsContract,
|
|
134
139
|
endpoints: endpointsContract,
|
|
135
140
|
extensions: extensionsContract,
|