@intentic/sandbox-contract 1.166.0 → 1.168.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 (67) hide show
  1. package/dist/agent-catalog.d.ts +20 -3
  2. package/dist/agent-catalog.d.ts.map +1 -1
  3. package/dist/agent-catalog.js +81 -6
  4. package/dist/agent-catalog.js.map +1 -1
  5. package/dist/contracts/agent.contract.d.ts +36 -8
  6. package/dist/contracts/agent.contract.d.ts.map +1 -1
  7. package/dist/contracts/agent.contract.js +1 -2
  8. package/dist/contracts/agent.contract.js.map +1 -1
  9. package/dist/contracts/agents.contract.d.ts +55 -39
  10. package/dist/contracts/agents.contract.d.ts.map +1 -1
  11. package/dist/contracts/agents.contract.js.map +1 -1
  12. package/dist/contracts/automations.contract.d.ts +1 -1
  13. package/dist/contracts/extensions.contract.d.ts +4 -0
  14. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  15. package/dist/contracts/gate.contract.d.ts +1 -1
  16. package/dist/contracts/kimi.contract.d.ts +0 -70
  17. package/dist/contracts/kimi.contract.d.ts.map +1 -1
  18. package/dist/contracts/kimi.contract.js +1 -5
  19. package/dist/contracts/kimi.contract.js.map +1 -1
  20. package/dist/contracts/sessions.contract.d.ts +1 -39
  21. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  22. package/dist/contracts/settings.contract.d.ts +29 -5
  23. package/dist/contracts/settings.contract.d.ts.map +1 -1
  24. package/dist/contracts/system.contract.d.ts +78 -4
  25. package/dist/contracts/system.contract.d.ts.map +1 -1
  26. package/dist/contracts/system.contract.js +9 -2
  27. package/dist/contracts/system.contract.js.map +1 -1
  28. package/dist/contracts/translator.contract.d.ts +47 -0
  29. package/dist/contracts/translator.contract.d.ts.map +1 -1
  30. package/dist/contracts/translator.contract.js.map +1 -1
  31. package/dist/events.d.ts +92 -159
  32. package/dist/events.d.ts.map +1 -1
  33. package/dist/events.js +37 -4
  34. package/dist/events.js.map +1 -1
  35. package/dist/index.d.ts +267 -181
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/schemas.d.ts +269 -44
  40. package/dist/schemas.d.ts.map +1 -1
  41. package/dist/schemas.js +92 -25
  42. package/dist/schemas.js.map +1 -1
  43. package/dist/title.d.ts.map +1 -1
  44. package/dist/title.js.map +1 -1
  45. package/dist/workspace-state.d.ts +9 -0
  46. package/dist/workspace-state.d.ts.map +1 -0
  47. package/dist/workspace-state.js +71 -0
  48. package/dist/workspace-state.js.map +1 -0
  49. package/package.json +2 -2
  50. package/src/agent-catalog.test.ts +118 -0
  51. package/src/agent-catalog.ts +183 -24
  52. package/src/contracts/agent.contract.ts +1 -14
  53. package/src/contracts/agents.contract.ts +8 -6
  54. package/src/contracts/gate.contract.ts +2 -2
  55. package/src/contracts/kimi.contract.ts +4 -21
  56. package/src/contracts/system.contract.ts +24 -1
  57. package/src/contracts/translator.contract.ts +4 -6
  58. package/src/events.test.ts +32 -0
  59. package/src/events.ts +105 -26
  60. package/src/index.ts +1 -0
  61. package/src/model-order.ts +1 -1
  62. package/src/quick-model.test.ts +1 -1
  63. package/src/schemas.test.ts +8 -4
  64. package/src/schemas.ts +296 -111
  65. package/src/title.ts +6 -2
  66. package/src/workspace-state.test.ts +129 -0
  67. package/src/workspace-state.ts +160 -0
@@ -1,4 +1,4 @@
1
- import type { AgentHarness, AgentProvider, Model, NativeProvider } from "./schemas.js";
1
+ import { type AgentHarness, type AgentProvider, type Model, NATIVE_PROVIDERS, type NativeProvider, type PermissionMode } from "./schemas.js";
2
2
 
3
3
  /* The provider / harness / model catalog every picker shares (the chat menu, the automations dialog) — pure
4
4
  * data keyed by the wire vocabulary in schemas.ts, so the surfaces can't drift. Live state stays with the
@@ -42,10 +42,27 @@ export const PROVIDER_ACCESS: Record<NativeProvider, ProviderAccess> = {
42
42
  claude: { kind: "subscription", requirement: "Claude subscription", runs: "Claude Code" },
43
43
  codex: { kind: "subscription", requirement: "ChatGPT subscription", runs: "Codex" },
44
44
  grok: { kind: "subscription", requirement: "SuperGrok subscription", runs: "Grok" },
45
- kimi: { kind: "key", requirement: "Moonshot API key", runs: "Kimi Code" },
45
+ kimi: { kind: "subscription", requirement: "Kimi Code subscription", runs: "Kimi Code" },
46
46
  gemini: { kind: "free", requirement: "Google sign-in", runs: "Gemini, Claude and GPT-OSS under Claude Code" },
47
47
  };
48
48
 
49
+ /* WHOSE ALLOWANCE A TURN ON THIS PROVIDER SPENDS, as the subject of a sentence — a third naming of the same
50
+ * five ids, and the third is not redundancy. PROVIDERS names the RUNTIME the user picks ("Claude Code", "Kimi
51
+ * Code") and PROVIDER_ACCESS.requirement names the thing they CONNECT ("Claude subscription", "Google sign-in");
52
+ * neither reads as English in "… usage limit reached", and neither is what a spent quota belongs to.
53
+ *
54
+ * The routed providers are why this can't be inferred from the harness: a `gemini` turn drives Claude Opus 4.6
55
+ * through Google's Antigravity channel on a plain Google sign-in, so the quota that refuses it is Google's and
56
+ * Anthropic has no part in it. Saying "Claude usage limit reached" there sends the user to check the wrong
57
+ * account — and to a reset that is days out on a pool they never touched. */
58
+ export const PROVIDER_VENDOR: Record<NativeProvider, string> = {
59
+ claude: "Claude",
60
+ codex: "ChatGPT",
61
+ grok: "xAI",
62
+ kimi: "Kimi Code",
63
+ gemini: "Google",
64
+ };
65
+
49
66
  // What a turn on this provider costs at the MARGIN, ordering the same three kinds by the only question a
50
67
  // helper spending the user's money on their behalf has to answer: free is free; a subscription is already paid
51
68
  // but has a quota the user watches; a key is metered, so every call is real money. Deliberately not folded into
@@ -60,6 +77,21 @@ export const accessFor = (provider: AgentProvider): ProviderAccess | undefined =
60
77
  // static fallback.
61
78
  export const providerLabel = (provider: AgentProvider): string => PROVIDERS.find((p) => p.value === provider)?.label ?? provider;
62
79
 
80
+ /* Whether a plan-limit reading for this provider is OBTAINABLE at all — one fact, on the wire, because both
81
+ * halves need it and they need the same answer. The daemon reads it to decide what to even ask upstream for
82
+ * (usage/translator-usage.ts); the browser reads it to say WHY an account shows no meter, which is the
83
+ * difference between "this plan publishes nothing" and "we haven't measured yet" — two states that look
84
+ * identical as a blank row and mean opposite things.
85
+ *
86
+ * Three can be read, by two mechanisms that stop at the daemon's readers: Claude's rides its own turn (the
87
+ * OAuth usage endpoint, agent.ts), ChatGPT's and Google's are pulled through the translator's
88
+ * credential-scoped api-call. Grok is absent because xAI's usable billing data needs a subject id CLIProxyAPI
89
+ * keeps out of its auth-file listing, and the fallback probe spends a token to answer. Kimi is absent because
90
+ * it publishes no quota endpoint — the bundled translator knows only its chat and OAuth routes. Adding either
91
+ * is adding a reader and its name here, and nothing else. */
92
+ export const PLAN_LIMIT_PROVIDERS: readonly NativeProvider[] = ["claude", "codex", "gemini"];
93
+ export const reportsPlanLimits = (provider: AgentProvider): boolean => PLAN_LIMIT_PROVIDERS.includes(provider as NativeProvider);
94
+
63
95
  // The harness (agentic loop) a turn runs on, orthogonal to the provider. `native` = the provider's own runtime;
64
96
  // `claude-code` = the Claude Code loop for any provider (codex/grok then route through the translator). Only
65
97
  // surfaced for codex/grok — claude is always its own Claude Code loop, and kimi/gemini have no native runtime
@@ -69,20 +101,152 @@ export const HARNESSES: readonly { label: string; value: AgentHarness }[] = [
69
101
  { label: "Claude Code", value: "claude-code" },
70
102
  ];
71
103
 
72
- // Whether a turn on this provider/harness pair ACTUALLY runs the Claude Code Agent SDK loop which is not the
73
- // same question as `harness === "claude-code"`. Claude is always its own Claude Code loop, and kimi/gemini have
74
- // no native runtime at all (Moonshot speaks the Anthropic protocol directly; Google is re-served through the
75
- // translator), so all three run it whatever harness the client happened to send; only codex/grok have a native
76
- // runtime to switch away from. An ACP agent runs its own loop and is never one of these.
77
- //
78
- // Everything the SDK loop owns keys off this: the SteeringQueue that makes mid-turn injection possible, and the
79
- // session store `/sessions/:id` reads a finished conversation's transcript back out of. Both sides of the wire
80
- // answer it here so a provider that gains (or loses) a native runtime is one edit, not a hunt for the literals.
81
- export const runsClaudeCode = (provider: AgentProvider, harness: AgentHarness): boolean =>
82
- provider === "claude" ||
83
- provider === "kimi" ||
84
- provider === "gemini" ||
85
- ((provider === "codex" || provider === "grok") && harness === "claude-code");
104
+ /* WHAT A PROVIDER/HARNESS PAIR CAN ACTUALLY DO one declaration, read by both sides of the wire.
105
+ *
106
+ * Four runtimes serve turns behind one seam (AgentRequest in, AgentEvent frames out): the Claude Code Agent SDK
107
+ * loop, Codex's exec surface, OpenCode, and any ACP agent. They do NOT do the same things, and for a long time
108
+ * the only thing that said so was a comment inside each adapter "Ignores the Claude-only request fields" —
109
+ * which no surface above it could read. So the composer offered "Ask before each file edit" on a runtime whose
110
+ * every tool call is pre-approved, and offered a reasoning-effort scale to a runtime that drops the field.
111
+ *
112
+ * A capability is listed here only if something READS it: the daemon gates a seam on it, the composer hides or
113
+ * clamps a control by it, or `limitationsOf` tells the user about it. That is the whole point — an ability the
114
+ * matrix claims and nothing consults is how the drift started.
115
+ *
116
+ * Adding a provider is a row here, not a hunt for literals; agent-catalog.test.ts walks PROVIDERS × HARNESSES
117
+ * and demands one, so a pair can never be silently absent. */
118
+ export interface AgentCapabilities {
119
+ // Which agentic loop actually serves the turn — the question "is the harness `claude-code`" only looks like.
120
+ // Claude is always its own Claude Code loop, and kimi/gemini have no native runtime at all (both are
121
+ // re-served through the translator), so all three run it whatever harness the client sent; only codex/grok
122
+ // have a native runtime to switch away from. Names the session store a finished conversation's transcript is
123
+ // backfilled from, too.
124
+ readonly runtime: "claude-code" | "codex" | "opencode" | "acp";
125
+ // Mid-turn injection (the SteeringQueue behind /agent/steer). Needs the SDK's streaming-input mode.
126
+ readonly steering: boolean;
127
+ // How much of the permission-mode axis the runtime honours. "modes" = every PermissionMode, with per-tool
128
+ // permission cards and `mode` frames when the agent moves itself; "plan" = propose-then-approve or run, and
129
+ // nothing in between — the container is the isolation boundary and every tool call is pre-approved.
130
+ readonly permissions: "modes" | "plan";
131
+ // Can stop mid-turn and ask the user a multiple-choice question (`question` frames).
132
+ readonly questions: boolean;
133
+ // Which of the turn's tools reach the agent. "full" = http MCP tools + in-process SDK servers + plugin
134
+ // checkouts + the browser servers; "http" = the http MCP tools alone, and only if the agent advertises http
135
+ // MCP support; "none" = the runtime has no seam for them at all.
136
+ readonly mcp: "full" | "http" | "none";
137
+ // Reasoning-effort selection is forwarded to the model.
138
+ readonly effort: boolean;
139
+ // How an isolated conversation's worktree is enforced. "namespace" = the worktree IS /work inside the turn's
140
+ // mount namespace (with the tool-input rewrite as the fallback when the container can't build one); "cwd" =
141
+ // the turn is merely cwd'd into the worktree, so an absolute /work path still reaches the shared checkout —
142
+ // which is why those turns are told where their tree is (turn-preamble.ts).
143
+ readonly isolation: "namespace" | "cwd";
144
+ // Publishes its slash commands (`commands` frames) for the composer's `/` popover.
145
+ readonly commands: boolean;
146
+ // Runs its shell in a tmux session the terminal panel can attach to (`terminal` frames).
147
+ readonly terminals: boolean;
148
+ // Fails with the coded frames the daemon's auto-resume keys off (rate_limit, provider-outage), so a turn the
149
+ // provider killed is re-run once the breaker says the provider is back (turn-resume.ts).
150
+ readonly recovery: boolean;
151
+ }
152
+
153
+ // The Claude Code Agent SDK loop — the ceiling every other runtime is measured against, and the only one that
154
+ // owns the whole request: permission callbacks, the ask tool, plugins, hooks, and the spawn seam a mount
155
+ // namespace needs.
156
+ const CLAUDE_CODE: AgentCapabilities = {
157
+ runtime: "claude-code",
158
+ steering: true,
159
+ permissions: "modes",
160
+ questions: true,
161
+ mcp: "full",
162
+ effort: true,
163
+ isolation: "namespace",
164
+ commands: true,
165
+ terminals: true,
166
+ recovery: true,
167
+ };
168
+
169
+ // Codex's exec surface: item-level events, no approval channel, no MCP seam through the SDK constructor we use.
170
+ // Reasoning effort IS forwarded (modelReasoningEffort). `codex app-server` is the upgrade path for the first two.
171
+ const CODEX: AgentCapabilities = {
172
+ runtime: "codex",
173
+ steering: false,
174
+ permissions: "plan",
175
+ questions: false,
176
+ mcp: "none",
177
+ effort: true,
178
+ isolation: "cwd",
179
+ commands: false,
180
+ terminals: false,
181
+ recovery: false,
182
+ };
183
+
184
+ // OpenCode (the Grok runtime): its own agentic loop, its own tools, allow-all permissions. It takes a model id
185
+ // and a prompt — no effort scale, no tools of ours, no command list.
186
+ const OPENCODE: AgentCapabilities = {
187
+ runtime: "opencode",
188
+ steering: false,
189
+ permissions: "plan",
190
+ questions: false,
191
+ mcp: "none",
192
+ effort: false,
193
+ isolation: "cwd",
194
+ commands: false,
195
+ terminals: false,
196
+ recovery: false,
197
+ };
198
+
199
+ // Any agent speaking the Agent Client Protocol: a documented floor rather than the native ceiling. It publishes
200
+ // commands, runs its terminals in the conversation's tmux session, and takes our http MCP tools when it says it
201
+ // can — but it owns its own model, effort and permission posture.
202
+ const ACP: AgentCapabilities = {
203
+ runtime: "acp",
204
+ steering: false,
205
+ permissions: "plan",
206
+ questions: false,
207
+ mcp: "http",
208
+ effort: false,
209
+ isolation: "cwd",
210
+ commands: true,
211
+ terminals: true,
212
+ recovery: false,
213
+ };
214
+
215
+ // The pair → its record. An id that names no native provider is an installed `agent`-kind capability, served
216
+ // over ACP.
217
+ export const capabilitiesOf = (provider: AgentProvider, harness: AgentHarness): AgentCapabilities => {
218
+ if (provider === "codex") {
219
+ return harness === "claude-code" ? CLAUDE_CODE : CODEX;
220
+ }
221
+ if (provider === "grok") {
222
+ return harness === "claude-code" ? CLAUDE_CODE : OPENCODE;
223
+ }
224
+ return (NATIVE_PROVIDERS as readonly string[]).includes(provider) ? CLAUDE_CODE : ACP;
225
+ };
226
+
227
+ // Which permission modes a runtime can actually be put in. Under "plan" every other mode collapses onto the
228
+ // autonomous posture the runtime already runs, so offering them would be offering four names for two behaviours.
229
+ export const modesFor = (capabilities: AgentCapabilities): readonly PermissionMode[] =>
230
+ capabilities.permissions === "modes" ? ["default", "acceptEdits", "plan", "bypassPermissions"] : ["plan", "bypassPermissions"];
231
+
232
+ // The mode a selection falls back to when the runtime can't hold it — the same shape as clampEffort, and for the
233
+ // same reason: a provider switch must not leave the composer showing a posture nothing applies.
234
+ export const clampMode = (mode: PermissionMode, capabilities: AgentCapabilities): PermissionMode =>
235
+ modesFor(capabilities).includes(mode) ? mode : "bypassPermissions";
236
+
237
+ // What this pair does NOT do, phrased for the person about to send a message to it — the honest half of the
238
+ // picker, and the reason the record carries axes the daemon itself never branches on. Empty ⇒ the full ceiling.
239
+ export const limitationsOf = (capabilities: AgentCapabilities): string[] => [
240
+ ...(capabilities.permissions === "plan" ? ["no per-tool approvals"] : []),
241
+ ...(capabilities.questions ? [] : ["no clarifying questions"]),
242
+ ...(capabilities.steering ? [] : ["no mid-turn steering"]),
243
+ ...(capabilities.mcp === "none" ? ["no MCP tools or plugins"] : capabilities.mcp === "http" ? ["MCP tools only — no plugins or browser"] : []),
244
+ ...(capabilities.effort ? [] : ["no effort control"]),
245
+ ...(capabilities.commands ? [] : ["no slash commands"]),
246
+ ...(capabilities.terminals ? [] : ["no terminal panel"]),
247
+ ...(capabilities.isolation === "namespace" ? [] : ["worktree by working directory only"]),
248
+ ...(capabilities.recovery ? [] : ["no auto-resume after an outage"]),
249
+ ];
86
250
 
87
251
  // Claude's compile-time model floor, shared by the daemon's catalog (claude-models.ts — its last rung, reached
88
252
  // only before either live source has ever answered) and by the web's pre-load list, so the two can't name
@@ -114,13 +278,8 @@ export const modelsFor = (provider: AgentProvider): CatalogOption[] => {
114
278
  // the only constrained tier and it fails two ways: no non-Claude scale HAS it, and Claude's API rejects it
115
279
  // outright when extended thinking is disabled ("effort 'max' is not supported when thinking is disabled on this
116
280
  // model" — a 400 that kills the turn before the model sees it, surfacing only as the SDK's `unknown` error
117
- // category). Every point where a selection can land on an invalid pair restore from storage, provider switch,
118
- // thinking toggle, the picker's own option list runs the pair through here, so the combination is unreachable
119
- // rather than merely discouraged.
281
+ // category). It is the one rule a MODEL's published tier list can't expressthe daemon reports what a model
282
+ // accepts without knowing this turn's thinking settingso the consumer that assembles the offered scale
283
+ // (effortsFor, web-side) filters through here, and the clamp over that scale makes the pair unreachable.
120
284
  export const effortAllowed = (effort: string, provider: AgentProvider, thinking: boolean): boolean =>
121
285
  effort !== "max" || (provider === "claude" && thinking);
122
-
123
- // The tier a selection falls back to when effortAllowed rejects it — one rung down from 'max', the top of every
124
- // scale that excludes it.
125
- export const clampEffort = (effort: string, provider: AgentProvider, thinking: boolean): string =>
126
- effortAllowed(effort, provider, thinking) ? effort : "xhigh";
@@ -1,15 +1,6 @@
1
1
  import { eventIterator, oc } from "@orpc/contract";
2
2
  import { AgentCommandsQuerySchema, AgentCommandsSchema, AttachFrameSchema } from "../events.js";
3
- import {
4
- AgentReplySchema,
5
- AgentTurnSchema,
6
- AttachTurnSchema,
7
- OkSchema,
8
- ResumeLimitSchema,
9
- StartedTurnSchema,
10
- SteerSchema,
11
- StopTurnSchema,
12
- } from "../schemas.js";
3
+ import { AgentReplySchema, AgentTurnSchema, AttachTurnSchema, OkSchema, StartedTurnSchema, SteerSchema, StopTurnSchema } from "../schemas.js";
13
4
 
14
5
  // A turn EXECUTES as a detached daemon-side run: `run` starts it and acks with the run id; any number of
15
6
  // clients render it via `attach` (replay from a seq cursor, then live) — the initiating window holds no
@@ -22,10 +13,6 @@ export const agentContract = {
22
13
  reply: oc.route({ method: "POST", path: "/agent/reply" }).input(AgentReplySchema).output(OkSchema),
23
14
  steer: oc.route({ method: "POST", path: "/agent/steer" }).input(SteerSchema).output(OkSchema),
24
15
  stop: oc.route({ method: "POST", path: "/agent/stop" }).input(StopTurnSchema).output(OkSchema),
25
- // Fire the conversation's pending usage-limit resume immediately, optionally on another account — the
26
- // chat's "resume on another account" action. Acks with the run id, exactly like `run`; the resumed turn
27
- // is an ordinary detached run any window attaches to.
28
- resumeLimit: oc.route({ method: "POST", path: "/agent/resume-limit" }).input(ResumeLimitSchema).output(StartedTurnSchema),
29
16
  // The provider's slash commands as last published by one of its turns, so a conversation's `/` popover is
30
17
  // populated before it has run one. The live `commands` frame stays authoritative for a running turn.
31
18
  commands: oc.route({ method: "GET", path: "/agent/commands" }).input(AgentCommandsQuerySchema).output(AgentCommandsSchema),
@@ -20,19 +20,21 @@ import {
20
20
  OkSchema,
21
21
  } from "../schemas.js";
22
22
 
23
- // The fleet: every registered conversation-agent (see AgentSummarySchema). `diff` is the conversation
24
- // worktree's CUMULATIVE multi-repo delta vs its recorded per-repo bases one flat change set per repo
23
+ // The fleet: every registered conversation-agent (see AgentSummarySchema). Registry-level actions (read,
24
+ // rename, seen, archive) apply uniformly. For branch-backed conversations, `diff` is the worktree's CUMULATIVE
25
+ // multi-repo delta vs its recorded per-repo bases — one flat change set per repo
25
26
  // (AgentChanges), each file flagged `landed` or not, deliberately not the working tree's staged/unstaged
26
27
  // shape: a worktree the user never checks out has no index they could stage into. `land` merges the worktree
27
28
  // branches into the main tree (per-repo, conflicts reported, nothing lost on failure); `discard` removes
28
- // worktrees + branches + registry entry. An unknown {id} is NOT_FOUND; land/discard while the turn runs is CONFLICT.
29
+ // worktrees + branches + registry entry. Those branch actions reject workspace conversations explicitly. An
30
+ // unknown {id} is NOT_FOUND; land/discard while the turn runs is CONFLICT.
29
31
  // `rename` sets the user-chosen display title — legal mid-turn (it touches no worktree state).
30
32
  // `seen`/`seenAll` stamp the read marker behind the cards' unread badge (AgentSummarySchema.seenAt) — also
31
33
  // legal mid-turn, and like `rename` they never bump `updatedAt` (reading is not activity).
32
34
  //
33
- // ARCHIVE is the non-destructive counterpart to discard, and the one the board leans on: `archive` commits
34
- // whatever the worktree still holds onto agent/<id>, drops the CHECKOUT (the expensive part — one file tree
35
- // per repo per agent) and keeps the branch, the entry, and the transcript. `list` then stops carrying it and
35
+ // ARCHIVE is the non-destructive counterpart to discard, and the one the board leans on: for an isolated
36
+ // conversation, `archive` commits whatever the worktree still holds onto agent/<id> and drops the CHECKOUT;
37
+ // for a workspace conversation it has no git teardown. Both keep the entry and transcript. `list` stops carrying it and
36
38
  // `archived` does; `unarchive` puts it back, and either way the next turn re-attaches a worktree from the
37
39
  // surviving branch. Archiving a running agent is CONFLICT, same as land/discard.
38
40
  //
@@ -9,8 +9,8 @@ import { GateVerdictSchema, OkSchema } from "../schemas.js";
9
9
  // claiming a green light the moment the tree moves under it. The three writes answer `ok` and nothing else —
10
10
  // each starts work that outlives the request, so there is no result to return and the poll is what reports.
11
11
  // `run` arms nothing and waits for nothing: it starts the check now, the user's own "I'm about to commit, check
12
- // this". `fix` opens the seeded main-tree turn for a red verdict, the same shape /ci/fix opens for a red
13
- // pipeline, minus the worktree it must not have.
12
+ // this". `fix` opens a seeded workspace conversation for a red verdict; it has the same registry lifecycle as
13
+ // /ci/fix, minus the isolated worktree it must not have.
14
14
  export const gateContract = {
15
15
  verdict: oc.route({ method: "GET", path: "/gate/verdict" }).output(GateVerdictSchema),
16
16
  run: oc.route({ method: "POST", path: "/gate/run" }).output(OkSchema),
@@ -1,26 +1,9 @@
1
1
  import { oc } from "@orpc/contract";
2
- import {
3
- AccountIdSchema,
4
- AccountRenameSchema,
5
- KimiConnectSchema,
6
- ModelsSchema,
7
- OauthAccountListSchema,
8
- OauthAccountSchema,
9
- OkSchema,
10
- } from "../schemas.js";
2
+ import { ModelsSchema } from "../schemas.js";
11
3
 
12
- // Kimi Code (Moonshot) the sandbox owns the credential, an API key rather than an OAuth grant. Kimi speaks the
13
- // Anthropic Messages protocol, so a Kimi turn runs on the SAME Claude Code harness as Claude, with the harness
14
- // pointed at Moonshot's Anthropic-compatible endpoint and authenticated with the stored key (see agent.routes).
15
- // `connect` stores a pasted key as a new account and returns it; `models` is the live catalog for the picker;
16
- // `accounts` lists connected keys (tokens never ride back); `rename` renames one (a pasted key carries no
17
- // identity to derive a name from, so this is the ONLY way a Kimi row gets a distinguishing name);
18
- // `disconnect` clears the one named by id.
4
+ // Kimi Code's picker catalog. Authentication and accounts belong to the bundled translator just like the other
5
+ // routed subscriptions (translator.contract.ts); this provider-specific route only projects CLIProxyAPI's Kimi
6
+ // model definitions into the shared picker shape.
19
7
  export const kimiContract = {
20
- connect: oc.route({ method: "POST", path: "/kimi/account/connect" }).input(KimiConnectSchema).output(OauthAccountSchema),
21
- // Kimi/Moonshot's live models for the picker — the source of valid ids (see kimi-models.ts).
22
8
  models: oc.route({ method: "GET", path: "/kimi/models" }).output(ModelsSchema),
23
- accounts: oc.route({ method: "GET", path: "/kimi/accounts" }).output(OauthAccountListSchema),
24
- rename: oc.route({ method: "POST", path: "/kimi/account/rename" }).input(AccountRenameSchema).output(OauthAccountSchema),
25
- disconnect: oc.route({ method: "POST", path: "/kimi/account/disconnect" }).input(AccountIdSchema).output(OkSchema),
26
9
  };
@@ -1,13 +1,17 @@
1
1
  import { eventIterator, oc } from "@orpc/contract";
2
2
  import { z } from "zod";
3
- import { SystemEventSchema } from "../events.js";
3
+ import { SessionTranscriptSchema, SystemEventSchema } from "../events.js";
4
4
  import {
5
+ BrowserNameParamSchema,
6
+ BrowsersListSchema,
5
7
  DaemonSessionSchema,
6
8
  HostTunnelInputSchema,
7
9
  HostTunnelSchema,
8
10
  InfoSchema,
9
11
  OkSchema,
10
12
  PresenceReportSchema,
13
+ SubagentIdParamSchema,
14
+ SubagentsListSchema,
11
15
  TerminalNameParamSchema,
12
16
  TerminalsListSchema,
13
17
  UsageSummarySchema,
@@ -38,4 +42,23 @@ export const systemContract = {
38
42
  // (browser fetch sends the header), unlike the header-less WS route which app.ts exempts.
39
43
  terminals: oc.route({ method: "GET", path: "/system/terminals" }).output(TerminalsListSchema),
40
44
  killTerminal: oc.route({ method: "DELETE", path: "/system/terminals/{name}" }).input(TerminalNameParamSchema).output(OkSchema),
45
+ // The agent's live Chromiums and the pages each has open — the Browsers view's roster, polled while it is on
46
+ // screen and by the rail so its tile can appear the moment a turn starts browsing. The frames are the
47
+ // separate /system/browser-view WebSocket; this is the control plane, exactly as `terminals` is for tmux.
48
+ // `closeBrowser` shuts one Chromium down: the agent's next browser tool call then fails as if it had crashed,
49
+ // which is the honest account of the owner pulling the plug.
50
+ browsers: oc.route({ method: "GET", path: "/system/browsers" }).output(BrowsersListSchema),
51
+ closeBrowser: oc.route({ method: "DELETE", path: "/system/browsers/{name}" }).input(BrowserNameParamSchema).output(OkSchema),
52
+ // The agents this sandbox's agents started — SDK subagents and delegated Codex/Grok runs alike (see
53
+ // SubagentSessionSchema). Same two-route shape as the browsers above, and same division of labour: the list
54
+ // is polled by the Subagents area while it is on screen and loosely by the rail, so its tile can appear the
55
+ // moment a turn delegates. There is no third WebSocket here, because a subagent has no byte stream to watch —
56
+ // what you watch it through is its TRANSCRIPT, which `subagentTranscript` serves in the one shape every
57
+ // other transcript route already answers in: live from the parent turn's frame log while it runs, off the
58
+ // provider's own store once it has finished.
59
+ subagents: oc.route({ method: "GET", path: "/system/subagents" }).output(SubagentsListSchema),
60
+ subagentTranscript: oc
61
+ .route({ method: "GET", path: "/system/subagents/{id}/transcript" })
62
+ .input(SubagentIdParamSchema)
63
+ .output(SessionTranscriptSchema),
41
64
  };
@@ -7,12 +7,10 @@ import { KeyedProviderSchema, OkSchema, TranslatorAccountsSchema, TranslatorComp
7
7
  // than an API key. A provider can hold several accounts side by side (the translator balances across them);
8
8
  // `accounts` lists what's connected per provider and `disconnect` clears ONE account by its auth-file `name`.
9
9
  //
10
- // Two login shapes ride one pair of routes. Codex and Grok mint a one-time device `code`: the user enters it at
11
- // the provider's site and the translator polls to completion in the background, so the UI just polls `accounts`
12
- // and `complete` is never called. Google has no device flow it redirects the browser to a loopback URL this
13
- // sandbox can't receive so its `connect` returns an EMPTY code, the card asks the user to paste the URL they
14
- // landed on, and `complete` hands it to the translator to finish the exchange. The card branches on that empty
15
- // code rather than on the provider id, so a provider that later gains a device flow needs no UI change.
10
+ // Two login shapes ride one pair of routes. Codex, Grok and Kimi use device authorization: the translator polls
11
+ // to completion in the background, so the UI only polls `accounts`. Google redirects the browser to a loopback
12
+ // URL this sandbox can't receive, so `complete` hands the landing URL to the translator. `connect.flow` tells the
13
+ // card which mechanic it received without inferring it from whether an optional device code happened to exist.
16
14
  export const translatorContract = {
17
15
  accounts: oc.route({ method: "GET", path: "/translator/accounts" }).output(TranslatorAccountsSchema),
18
16
  connect: oc
@@ -0,0 +1,32 @@
1
+ import { expect, test } from "vitest";
2
+ import { RESUME_NOTES, withResumeNote, withoutResumeNote } from "./events.js";
3
+
4
+ /* The resume note is a round trip across the wire: the daemon wraps a prompt to tell the model what interrupted
5
+ * it, and the client unwraps the SAME prompt off an attach head to tell whether it already has that bubble. A
6
+ * mismatch between the two halves fails silently and cosmetically — a paragraph of machine prose rendered as
7
+ * something the user typed — which is exactly the kind of drift that stays broken. */
8
+ test("a resume note round-trips back to the user's own words", () => {
9
+ for (const note of Object.values(RESUME_NOTES)) {
10
+ expect(withoutResumeNote(withResumeNote("ship the parser", note))).toBe("ship the parser");
11
+ }
12
+ });
13
+
14
+ // A prompt with blank lines of its own: only the note's own separator comes off, never the user's paragraphs.
15
+ test("stripping takes the note and nothing of the prompt", () => {
16
+ const prompt = "step one\n\nstep two\n\nstep three";
17
+ expect(withoutResumeNote(withResumeNote(prompt, RESUME_NOTES.outage))).toBe(prompt);
18
+ });
19
+
20
+ // An ordinary prompt passes through untouched, so every attach head can be handed through it.
21
+ test("a prompt that is not a resume is left alone", () => {
22
+ expect(withoutResumeNote("just a question")).toBe("just a question");
23
+ expect(withoutResumeNote("")).toBe("");
24
+ });
25
+
26
+ // Wrapping is idempotent: a resume that dies the same way again is re-recorded from its own input, and a second
27
+ // note stacked on the first would grow the prompt on every attempt.
28
+ test("wrapping an already-wrapped prompt adds nothing", () => {
29
+ const once = withResumeNote("retry me", RESUME_NOTES.restart);
30
+ expect(withResumeNote(once, RESUME_NOTES.restart)).toBe(once);
31
+ expect(withResumeNote(once, RESUME_NOTES.auth)).toBe(once);
32
+ });