@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.
Files changed (70) hide show
  1. package/dist/agent-catalog.d.ts +2 -2
  2. package/dist/agent-catalog.d.ts.map +1 -1
  3. package/dist/agent-catalog.js +26 -1
  4. package/dist/agent-catalog.js.map +1 -1
  5. package/dist/contracts/agent.contract.d.ts +19 -2
  6. package/dist/contracts/agent.contract.d.ts.map +1 -1
  7. package/dist/contracts/agents.contract.d.ts +62 -0
  8. package/dist/contracts/agents.contract.d.ts.map +1 -1
  9. package/dist/contracts/agents.contract.js +2 -2
  10. package/dist/contracts/agents.contract.js.map +1 -1
  11. package/dist/contracts/capabilities.contract.d.ts +7 -0
  12. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  13. package/dist/contracts/cursor.contract.d.ts +63 -0
  14. package/dist/contracts/cursor.contract.d.ts.map +1 -0
  15. package/dist/contracts/cursor.contract.js +49 -0
  16. package/dist/contracts/cursor.contract.js.map +1 -0
  17. package/dist/contracts/endpoints.contract.d.ts +1 -0
  18. package/dist/contracts/endpoints.contract.d.ts.map +1 -1
  19. package/dist/contracts/extensions.contract.d.ts +1 -0
  20. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  21. package/dist/contracts/logs.contract.d.ts +17 -0
  22. package/dist/contracts/logs.contract.d.ts.map +1 -1
  23. package/dist/contracts/logs.contract.js +10 -1
  24. package/dist/contracts/logs.contract.js.map +1 -1
  25. package/dist/contracts/providers.contract.d.ts +2 -0
  26. package/dist/contracts/providers.contract.d.ts.map +1 -1
  27. package/dist/contracts/sessions.contract.d.ts +3 -0
  28. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  29. package/dist/contracts/settings.contract.d.ts +19 -0
  30. package/dist/contracts/settings.contract.d.ts.map +1 -1
  31. package/dist/contracts/system.contract.d.ts +21 -14
  32. package/dist/contracts/system.contract.d.ts.map +1 -1
  33. package/dist/events.d.ts +46 -0
  34. package/dist/events.d.ts.map +1 -1
  35. package/dist/events.js +14 -0
  36. package/dist/events.js.map +1 -1
  37. package/dist/index.d.ts +271 -70
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +5 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/prompt-complexity.d.ts +9 -1
  42. package/dist/prompt-complexity.d.ts.map +1 -1
  43. package/dist/prompt-complexity.js +13 -5
  44. package/dist/prompt-complexity.js.map +1 -1
  45. package/dist/request-id.d.ts +3 -0
  46. package/dist/request-id.d.ts.map +1 -0
  47. package/dist/request-id.js +3 -0
  48. package/dist/request-id.js.map +1 -0
  49. package/dist/schemas.d.ts +268 -1
  50. package/dist/schemas.d.ts.map +1 -1
  51. package/dist/schemas.js +79 -1
  52. package/dist/schemas.js.map +1 -1
  53. package/dist/starter.d.ts +3 -0
  54. package/dist/starter.d.ts.map +1 -0
  55. package/dist/starter.js +3 -0
  56. package/dist/starter.js.map +1 -0
  57. package/package.json +6 -5
  58. package/src/agent-catalog.test.ts +2 -2
  59. package/src/agent-catalog.ts +123 -13
  60. package/src/contract-lock.test.ts +8 -2
  61. package/src/contracts/agents.contract.ts +2 -1
  62. package/src/contracts/cursor.contract.ts +74 -0
  63. package/src/contracts/logs.contract.ts +23 -2
  64. package/src/events.ts +62 -6
  65. package/src/index.ts +5 -0
  66. package/src/prompt-complexity.test.ts +69 -0
  67. package/src/prompt-complexity.ts +102 -39
  68. package/src/request-id.ts +41 -0
  69. package/src/schemas.ts +276 -2
  70. package/src/starter.ts +13 -0
@@ -0,0 +1,3 @@
1
+ export declare const STARTER_REPO = "site";
2
+ export declare const STARTER_APP = "landing";
3
+ //# sourceMappingURL=starter.d.ts.map
@@ -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"}
@@ -0,0 +1,3 @@
1
+ export const STARTER_REPO = "site";
2
+ export const STARTER_APP = "landing";
3
+ //# sourceMappingURL=starter.js.map
@@ -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.226.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.226.1",
93
- "@intentic/extension-manifest": "1.226.1",
94
- "@intentic/registry": "1.226.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) {
@@ -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 fifth among five equal buttons is how a user with
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
- * five ids, and the third is not redundancy. PROVIDERS names the RUNTIME the user picks ("Claude Code", "Kimi
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
- * Five runtimes serve turns behind one seam (AgentRequest in, AgentEvent frames out): the Claude Code Agent SDK
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
- // Which of the turn's tools reach the agent. "full" = http MCP tools + in-process SDK servers + plugin
230
- // checkouts + the browser servers; "browser" = the process-backed browser servers alone; "http" = the http
231
- // MCP tools alone, and only if the agent advertises http MCP support; "none" = the runtime has no seam for
232
- // them at all. Keeping the partial answers distinct matters: a runtime that can drive a connected account
233
- // must not be described as tool-less, and one that cannot host daemon-side SDK servers must not claim full.
234
- readonly mcp: "full" | "browser" | "http" | "none";
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 five native ids are: an `agent`-kind capability installed
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
- test("contract.lock.json matches the schemas this package exports", () => {
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(AgentsMovedSchema),
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
- // Read-only by design, the files are written by the daemon/tmux only, so the record stays trustworthy.
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`/`maxAttempts` are the harness's own counters; `nextAttemptAt` (epoch ms) is when it will try
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
- // The model provider itself failed transiently: 500/502/503, a 529 at capacity, a dropped
840
- // socket, and the harness's own in-turn retries did not outlast it. Nothing about the workspace
841
- // or the request is wrong, so the daemon remembers the turn and re-runs it on an escalating
842
- // backoff (provider-health.ts): the frame is a notice about a turn that is coming back, and
843
- // reaches the client as a plain failure only once the attempts are spent.
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,