@intentic/sandbox-contract 1.240.0 → 1.242.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 (310) hide show
  1. package/README.md +52 -16
  2. package/dist/agent-catalog.d.ts +5 -28
  3. package/dist/agent-catalog.d.ts.map +1 -1
  4. package/dist/agent-catalog.js +21 -157
  5. package/dist/agent-catalog.js.map +1 -1
  6. package/dist/agent-run-model.d.ts +3 -2
  7. package/dist/agent-run-model.d.ts.map +1 -1
  8. package/dist/agent-run-model.js +9 -6
  9. package/dist/agent-run-model.js.map +1 -1
  10. package/dist/agent-runtimes.d.ts +27 -0
  11. package/dist/agent-runtimes.d.ts.map +1 -0
  12. package/dist/agent-runtimes.js +113 -0
  13. package/dist/agent-runtimes.js.map +1 -0
  14. package/dist/approvals-execution.d.ts +11 -0
  15. package/dist/approvals-execution.d.ts.map +1 -0
  16. package/dist/approvals-execution.js +44 -0
  17. package/dist/approvals-execution.js.map +1 -0
  18. package/dist/arrival.d.ts +151 -0
  19. package/dist/arrival.d.ts.map +1 -0
  20. package/dist/arrival.js +56 -0
  21. package/dist/arrival.js.map +1 -0
  22. package/dist/card-status.d.ts +5 -0
  23. package/dist/card-status.d.ts.map +1 -0
  24. package/dist/card-status.js +74 -0
  25. package/dist/card-status.js.map +1 -0
  26. package/dist/chores/chores.d.ts.map +1 -1
  27. package/dist/chores/chores.js +1 -1
  28. package/dist/chores/chores.js.map +1 -1
  29. package/dist/command-classes.d.ts +0 -1
  30. package/dist/command-classes.d.ts.map +1 -1
  31. package/dist/command-classes.js +9 -2
  32. package/dist/command-classes.js.map +1 -1
  33. package/dist/command-run.d.ts +10 -0
  34. package/dist/command-run.d.ts.map +1 -0
  35. package/dist/command-run.js +46 -0
  36. package/dist/command-run.js.map +1 -0
  37. package/dist/contracts/agent.contract.d.ts +991 -771
  38. package/dist/contracts/agent.contract.d.ts.map +1 -1
  39. package/dist/contracts/agents.contract.d.ts +345 -337
  40. package/dist/contracts/agents.contract.d.ts.map +1 -1
  41. package/dist/contracts/agents.contract.js +20 -2
  42. package/dist/contracts/agents.contract.js.map +1 -1
  43. package/dist/contracts/approvals.contract.d.ts +99 -0
  44. package/dist/contracts/approvals.contract.d.ts.map +1 -0
  45. package/dist/contracts/approvals.contract.js +32 -0
  46. package/dist/contracts/approvals.contract.js.map +1 -0
  47. package/dist/contracts/ci.contract.d.ts +1 -4
  48. package/dist/contracts/ci.contract.d.ts.map +1 -1
  49. package/dist/contracts/ci.contract.js +1 -9
  50. package/dist/contracts/ci.contract.js.map +1 -1
  51. package/dist/contracts/claude.contract.d.ts +9 -0
  52. package/dist/contracts/claude.contract.d.ts.map +1 -1
  53. package/dist/contracts/cursor.contract.d.ts +6 -0
  54. package/dist/contracts/cursor.contract.d.ts.map +1 -1
  55. package/dist/contracts/git.contract.d.ts +63 -1
  56. package/dist/contracts/git.contract.d.ts.map +1 -1
  57. package/dist/contracts/git.contract.js +22 -4
  58. package/dist/contracts/git.contract.js.map +1 -1
  59. package/dist/contracts/grok.contract.d.ts +3 -0
  60. package/dist/contracts/grok.contract.d.ts.map +1 -1
  61. package/dist/contracts/keys.contract.d.ts +81 -0
  62. package/dist/contracts/keys.contract.d.ts.map +1 -0
  63. package/dist/contracts/keys.contract.js +51 -0
  64. package/dist/contracts/keys.contract.js.map +1 -0
  65. package/dist/contracts/panels.contract.d.ts +7 -0
  66. package/dist/contracts/panels.contract.d.ts.map +1 -1
  67. package/dist/contracts/prepush.contract.js +2 -2
  68. package/dist/contracts/providers.contract.d.ts +2 -0
  69. package/dist/contracts/providers.contract.d.ts.map +1 -1
  70. package/dist/contracts/runner.contract.d.ts +84 -86
  71. package/dist/contracts/runner.contract.d.ts.map +1 -1
  72. package/dist/contracts/safety.contract.d.ts +35 -0
  73. package/dist/contracts/safety.contract.d.ts.map +1 -0
  74. package/dist/contracts/safety.contract.js +32 -0
  75. package/dist/contracts/safety.contract.js.map +1 -0
  76. package/dist/contracts/sessions.contract.d.ts +73 -337
  77. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  78. package/dist/contracts/settings.contract.d.ts +30 -32
  79. package/dist/contracts/settings.contract.d.ts.map +1 -1
  80. package/dist/contracts/system.contract.d.ts +139 -337
  81. package/dist/contracts/system.contract.d.ts.map +1 -1
  82. package/dist/contracts/translator.contract.d.ts +31 -3
  83. package/dist/contracts/translator.contract.d.ts.map +1 -1
  84. package/dist/contracts/usage.contract.d.ts +29 -20
  85. package/dist/contracts/usage.contract.d.ts.map +1 -1
  86. package/dist/contracts/usage.contract.js +13 -0
  87. package/dist/contracts/usage.contract.js.map +1 -1
  88. package/dist/contracts/workspace.contract.d.ts +8 -0
  89. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  90. package/dist/conversation-ids.d.ts +2 -0
  91. package/dist/conversation-ids.d.ts.map +1 -1
  92. package/dist/conversation-ids.js +8 -0
  93. package/dist/conversation-ids.js.map +1 -1
  94. package/dist/credential-material.d.ts +1 -0
  95. package/dist/credential-material.d.ts.map +1 -1
  96. package/dist/credential-material.js +24 -2
  97. package/dist/credential-material.js.map +1 -1
  98. package/dist/definition.d.ts +51 -93
  99. package/dist/definition.d.ts.map +1 -1
  100. package/dist/definition.js +6 -28
  101. package/dist/definition.js.map +1 -1
  102. package/dist/events.d.ts +1831 -2285
  103. package/dist/events.d.ts.map +1 -1
  104. package/dist/events.js +155 -55
  105. package/dist/events.js.map +1 -1
  106. package/dist/history-state.d.ts.map +1 -1
  107. package/dist/history-state.js +1 -0
  108. package/dist/history-state.js.map +1 -1
  109. package/dist/index.d.ts +1959 -1903
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js +20 -5
  112. package/dist/index.js.map +1 -1
  113. package/dist/ingress-protocol.d.ts +13 -1
  114. package/dist/ingress-protocol.d.ts.map +1 -1
  115. package/dist/ingress-protocol.js +19 -0
  116. package/dist/ingress-protocol.js.map +1 -1
  117. package/dist/mentions.d.ts +3 -0
  118. package/dist/mentions.d.ts.map +1 -0
  119. package/dist/mentions.js +13 -0
  120. package/dist/mentions.js.map +1 -0
  121. package/dist/needs-action.d.ts +7 -0
  122. package/dist/needs-action.d.ts.map +1 -0
  123. package/dist/needs-action.js +3 -0
  124. package/dist/needs-action.js.map +1 -0
  125. package/dist/overlay-lint.d.ts +7 -0
  126. package/dist/overlay-lint.d.ts.map +1 -0
  127. package/dist/overlay-lint.js +68 -0
  128. package/dist/overlay-lint.js.map +1 -0
  129. package/dist/owner-ticket.d.ts +18 -0
  130. package/dist/owner-ticket.d.ts.map +1 -0
  131. package/dist/owner-ticket.js +51 -0
  132. package/dist/owner-ticket.js.map +1 -0
  133. package/dist/plan-pools.d.ts +12 -0
  134. package/dist/plan-pools.d.ts.map +1 -0
  135. package/dist/plan-pools.js +30 -0
  136. package/dist/plan-pools.js.map +1 -0
  137. package/dist/provider-specs.d.ts +228 -0
  138. package/dist/provider-specs.d.ts.map +1 -0
  139. package/dist/provider-specs.js +124 -0
  140. package/dist/provider-specs.js.map +1 -0
  141. package/dist/quick-model.d.ts.map +1 -1
  142. package/dist/quick-model.js +2 -1
  143. package/dist/quick-model.js.map +1 -1
  144. package/dist/runner-protocol.js +2 -2
  145. package/dist/runner-protocol.js.map +1 -1
  146. package/dist/runtime-state.d.ts +2 -2
  147. package/dist/runtime-state.d.ts.map +1 -1
  148. package/dist/runtime-state.js +1 -1
  149. package/dist/runtime-state.js.map +1 -1
  150. package/dist/safety-policy.d.ts +49 -0
  151. package/dist/safety-policy.d.ts.map +1 -0
  152. package/dist/safety-policy.js +52 -0
  153. package/dist/safety-policy.js.map +1 -0
  154. package/dist/schemas/agent.d.ts +21 -3
  155. package/dist/schemas/agent.d.ts.map +1 -1
  156. package/dist/schemas/agent.js +18 -8
  157. package/dist/schemas/agent.js.map +1 -1
  158. package/dist/schemas/agents.d.ts +19 -0
  159. package/dist/schemas/agents.d.ts.map +1 -1
  160. package/dist/schemas/agents.js +15 -0
  161. package/dist/schemas/agents.js.map +1 -1
  162. package/dist/schemas/approvals.d.ts +241 -0
  163. package/dist/schemas/approvals.d.ts.map +1 -0
  164. package/dist/schemas/approvals.js +63 -0
  165. package/dist/schemas/approvals.js.map +1 -0
  166. package/dist/schemas/automations.d.ts +5 -0
  167. package/dist/schemas/automations.d.ts.map +1 -1
  168. package/dist/schemas/ci.d.ts +5 -9
  169. package/dist/schemas/ci.d.ts.map +1 -1
  170. package/dist/schemas/ci.js +3 -10
  171. package/dist/schemas/ci.js.map +1 -1
  172. package/dist/schemas/computers.d.ts +31 -5
  173. package/dist/schemas/computers.d.ts.map +1 -1
  174. package/dist/schemas/computers.js +7 -5
  175. package/dist/schemas/computers.js.map +1 -1
  176. package/dist/schemas/environment.d.ts +11 -131
  177. package/dist/schemas/environment.d.ts.map +1 -1
  178. package/dist/schemas/environment.js +5 -42
  179. package/dist/schemas/environment.js.map +1 -1
  180. package/dist/schemas/git-history.d.ts +4 -0
  181. package/dist/schemas/git-history.d.ts.map +1 -1
  182. package/dist/schemas/git.d.ts +179 -0
  183. package/dist/schemas/git.d.ts.map +1 -1
  184. package/dist/schemas/git.js +39 -1
  185. package/dist/schemas/git.js.map +1 -1
  186. package/dist/schemas/panels.d.ts +21 -0
  187. package/dist/schemas/panels.d.ts.map +1 -1
  188. package/dist/schemas/panels.js +5 -0
  189. package/dist/schemas/panels.js.map +1 -1
  190. package/dist/schemas/plan-limits.d.ts +50 -3
  191. package/dist/schemas/plan-limits.d.ts.map +1 -1
  192. package/dist/schemas/plan-limits.js +11 -6
  193. package/dist/schemas/plan-limits.js.map +1 -1
  194. package/dist/schemas/provider-oauth.d.ts +6 -0
  195. package/dist/schemas/provider-oauth.d.ts.map +1 -1
  196. package/dist/schemas/provider-subscriptions.d.ts +2 -1
  197. package/dist/schemas/provider-subscriptions.d.ts.map +1 -1
  198. package/dist/schemas/provider-subscriptions.js +2 -1
  199. package/dist/schemas/provider-subscriptions.js.map +1 -1
  200. package/dist/schemas/settings.d.ts +29 -16
  201. package/dist/schemas/settings.d.ts.map +1 -1
  202. package/dist/schemas/settings.js +13 -20
  203. package/dist/schemas/settings.js.map +1 -1
  204. package/dist/schemas/system.d.ts +3 -0
  205. package/dist/schemas/system.d.ts.map +1 -1
  206. package/dist/schemas/system.js +5 -1
  207. package/dist/schemas/system.js.map +1 -1
  208. package/dist/schemas/workspace-repos.d.ts +14 -0
  209. package/dist/schemas/workspace-repos.d.ts.map +1 -1
  210. package/dist/schemas/workspace-repos.js +5 -0
  211. package/dist/schemas/workspace-repos.js.map +1 -1
  212. package/dist/schemas/workspace-tree.d.ts +1 -0
  213. package/dist/schemas/workspace-tree.d.ts.map +1 -1
  214. package/dist/schemas/workspace-tree.js +3 -0
  215. package/dist/schemas/workspace-tree.js.map +1 -1
  216. package/dist/transcript-fold.d.ts +31 -0
  217. package/dist/transcript-fold.d.ts.map +1 -0
  218. package/dist/transcript-fold.js +457 -0
  219. package/dist/transcript-fold.js.map +1 -0
  220. package/dist/workspace-state.d.ts +20 -3
  221. package/dist/workspace-state.d.ts.map +1 -1
  222. package/dist/workspace-state.js +22 -2
  223. package/dist/workspace-state.js.map +1 -1
  224. package/package.json +27 -5
  225. package/src/agent-catalog.test.ts +1 -1
  226. package/src/agent-catalog.ts +76 -555
  227. package/src/agent-run-model.test.ts +31 -15
  228. package/src/agent-run-model.ts +26 -12
  229. package/src/agent-runtimes.ts +419 -0
  230. package/src/approvals-execution.ts +96 -0
  231. package/src/arrival.ts +160 -0
  232. package/src/capability-ledger.test.ts +6 -4
  233. package/src/card-status.ts +93 -0
  234. package/src/chores/chores.ts +6 -3
  235. package/src/command-classes.test.ts +25 -0
  236. package/src/command-classes.ts +47 -5
  237. package/src/command-run.ts +78 -0
  238. package/src/contract-lock.test.ts +1 -1
  239. package/src/contract-lock.ts +2 -2
  240. package/src/contracts/agents.contract.ts +35 -2
  241. package/src/contracts/approvals.contract.ts +37 -0
  242. package/src/contracts/ci.contract.ts +0 -13
  243. package/src/contracts/git.contract.ts +28 -3
  244. package/src/contracts/keys.contract.ts +79 -0
  245. package/src/contracts/prepush.contract.ts +3 -3
  246. package/src/contracts/safety.contract.ts +48 -0
  247. package/src/contracts/usage.contract.ts +22 -0
  248. package/src/conversation-ids.test.ts +31 -1
  249. package/src/conversation-ids.ts +37 -3
  250. package/src/credential-material.test.ts +69 -1
  251. package/src/credential-material.ts +86 -5
  252. package/src/definition.ts +25 -53
  253. package/src/events.ts +374 -173
  254. package/src/history-state.ts +6 -0
  255. package/src/index.ts +24 -5
  256. package/src/ingress-protocol.ts +52 -1
  257. package/src/mentions.test.ts +22 -0
  258. package/src/mentions.ts +25 -0
  259. package/src/needs-action.ts +14 -0
  260. package/src/overlay-lint.test.ts +89 -0
  261. package/src/overlay-lint.ts +116 -0
  262. package/src/owner-ticket.test.ts +54 -0
  263. package/src/owner-ticket.ts +102 -0
  264. package/src/plan-pools.test.ts +71 -0
  265. package/src/plan-pools.ts +82 -0
  266. package/src/provider-specs.test.ts +130 -0
  267. package/src/provider-specs.ts +352 -0
  268. package/src/quick-model.ts +9 -5
  269. package/src/runner-protocol.ts +2 -2
  270. package/src/runtime-state.ts +6 -6
  271. package/src/safety-policy.ts +165 -0
  272. package/src/schemas/agent.ts +62 -17
  273. package/src/schemas/agents.ts +71 -0
  274. package/src/schemas/approvals.ts +174 -0
  275. package/src/schemas/ci.ts +19 -26
  276. package/src/schemas/computers.ts +52 -11
  277. package/src/schemas/environment.ts +21 -100
  278. package/src/schemas/git.ts +121 -6
  279. package/src/schemas/issues.ts +1 -1
  280. package/src/schemas/panels.ts +20 -0
  281. package/src/schemas/plan-limits.ts +58 -11
  282. package/src/schemas/provider-subscriptions.ts +7 -2
  283. package/src/schemas/settings.ts +67 -93
  284. package/src/schemas/system.ts +12 -1
  285. package/src/schemas/workspace-repos.ts +9 -0
  286. package/src/schemas/workspace-tree.ts +11 -0
  287. package/src/transcript-fold.test.ts +493 -0
  288. package/src/transcript-fold.ts +655 -0
  289. package/src/workspace-state.test.ts +77 -29
  290. package/src/workspace-state.ts +99 -31
  291. package/dist/contracts/drafts.contract.d.ts +0 -57
  292. package/dist/contracts/drafts.contract.d.ts.map +0 -1
  293. package/dist/contracts/drafts.contract.js +0 -32
  294. package/dist/contracts/drafts.contract.js.map +0 -1
  295. package/dist/definition-action.d.ts +0 -7
  296. package/dist/definition-action.d.ts.map +0 -1
  297. package/dist/definition-action.js +0 -3
  298. package/dist/definition-action.js.map +0 -1
  299. package/dist/publish-drafts.d.ts +0 -7
  300. package/dist/publish-drafts.d.ts.map +0 -1
  301. package/dist/publish-drafts.js +0 -20
  302. package/dist/publish-drafts.js.map +0 -1
  303. package/dist/schemas/drafts.d.ts +0 -84
  304. package/dist/schemas/drafts.d.ts.map +0 -1
  305. package/dist/schemas/drafts.js +0 -44
  306. package/dist/schemas/drafts.js.map +0 -1
  307. package/src/contracts/drafts.contract.ts +0 -35
  308. package/src/definition-action.ts +0 -8
  309. package/src/publish-drafts.ts +0 -59
  310. package/src/schemas/drafts.ts +0 -108
@@ -1,10 +1,17 @@
1
- import { type AgentHarness, type AgentProvider, NATIVE_PROVIDERS, type NativeProvider, type PermissionMode } from "./schemas/agent.js";
1
+ import { ACP, type AgentCapabilities, CLAUDE_CODE, PI } from "./agent-runtimes.js";
2
+ import { type NativeProvider, PROVIDER_SPECS, type ProviderAccess, providerSpec } from "./provider-specs.js";
3
+ import type { AgentHarness, AgentProvider, PermissionMode } from "./schemas/agent.js";
2
4
  import type { Model, ModelBadge } from "./schemas/provider-oauth.js";
3
5
 
4
6
  /* The provider / harness / model catalog every picker shares (the chat menu, the automations dialog), pure
5
7
  * data keyed by the wire vocabulary in schemas/agent.ts, so the surfaces can't drift. Live state stays with the
6
8
  * consumer (native Grok's model list is the daemon's own catalog for it, layered on top of modelsFor by the
7
- * web; ACP providers are merged in from the installed `agent` capabilities). */
9
+ * web; ACP providers are merged in from the installed `agent` capabilities).
10
+ *
11
+ * NOTHING HERE IS A LIST OF PROVIDERS ANY MORE. Every table below is DERIVED from PROVIDER_SPECS, which is the
12
+ * one row-per-provider table this package keeps (provider-specs.ts, and its header says why). What remains here
13
+ * is the shape each surface wants that table in, plus the rules that are about something other than a provider
14
+ * (the trial, the endpoint namespace, the effort and fast-mode gates). */
8
15
 
9
16
  export interface CatalogOption {
10
17
  readonly label: string;
@@ -13,86 +20,35 @@ export interface CatalogOption {
13
20
 
14
21
  // The NATIVE agent runtimes; ACP providers are appended by the consumer from the installed capabilities.
15
22
  // New conversations use the selection, open ones stay locked (the pill reflects the locked provider). The
16
- // brand logo per provider is drawn by ProviderLogo (by value).
17
- export const PROVIDERS: readonly { label: string; value: NativeProvider }[] = [
18
- { label: "Claude Code", value: "claude" },
19
- { label: "Codex", value: "codex" },
20
- { label: "Grok", value: "grok" },
21
- { label: "Kimi Code", value: "kimi" },
22
- // Labelled for the ACCOUNT, not the model family: the `gemini` id names one channel. Google's Antigravity,
23
- // and that channel vends Claude and GPT-OSS models alongside Gemini's own (see gemini-models.ts). A section
24
- // headed "Gemini" holding Claude Opus would be a lie; "Google" is what the whole list has in common.
25
- { label: "Google", value: "gemini" },
26
- // Cursor's own agent runtime, driven through the SDK Anysphere publishes, on the user's Cursor subscription.
27
- // Like Google above, the label names the ACCOUNT rather than a model family: the channel vends Anthropic,
28
- // OpenAI and xAI models alongside Cursor's own Composer, and no model name covers that list.
29
- { label: "Cursor", value: "cursor" },
30
- ];
23
+ // brand logo per provider is drawn by ProviderLogo (by value). Order is the spec table's order.
24
+ export const PROVIDERS: readonly { label: string; value: NativeProvider }[] = PROVIDER_SPECS.map((spec) => ({
25
+ label: spec.label,
26
+ value: spec.id,
27
+ }));
31
28
 
32
29
  // What it COSTS to unlock a provider, and what the user connects to do it, the axis the picker groups on, since
33
- // "can this row actually run" is the first thing a model list has to answer. `free` is not a courtesy tier: the
34
- // Google channel serves its models on an ordinary Google sign-in, at no subscription, which is the single most
35
- // useful thing this catalog can tell a user who has connected nothing yet.
36
- export type AccessKind = "free" | "subscription" | "key";
37
-
38
- export interface ProviderAccess {
39
- readonly kind: AccessKind;
40
- // What the user connects, named the way its vendor names it, this is the noun every connect prompt uses.
41
- readonly requirement: string;
42
- // What connecting it lets them run, for the connect gate's one-line pitch.
43
- readonly runs: string;
44
- }
45
-
46
- export const PROVIDER_ACCESS: Record<NativeProvider, ProviderAccess> = {
47
- claude: { kind: "subscription", requirement: "Claude subscription", runs: "Claude Code" },
48
- codex: { kind: "subscription", requirement: "ChatGPT subscription", runs: "Codex" },
49
- grok: { kind: "subscription", requirement: "SuperGrok subscription", runs: "Grok" },
50
- kimi: { kind: "subscription", requirement: "Kimi Code subscription", runs: "Kimi Code" },
51
- gemini: { kind: "free", requirement: "Google sign-in", runs: "Gemini, Claude and GPT-OSS under Claude Code" },
52
- // A `subscription` like the first four, and the requirement names the PLAN rather than the account, because
53
- // a free Cursor account signs in perfectly and still cannot run a turn here: the SDK behind this provider is
54
- // gated to the paid tiers. Saying "Cursor account" would send someone to a sign-in that ends in a refusal
55
- // they had no way to predict.
56
- cursor: { kind: "subscription", requirement: "Cursor Pro subscription", runs: "Cursor Agent" },
57
- };
58
-
59
- /* THE PROVIDERS THAT COST NOTHING, derived from the table above rather than named a second time, and read by
60
- * every surface that LEADS with a free option instead of merely labelling one.
30
+ // "can this row actually run" is the first thing a model list has to answer. See ProviderAccess in
31
+ // provider-specs.ts for what `free` means here, and why it is not a courtesy tier.
32
+ export const PROVIDER_ACCESS: Record<NativeProvider, ProviderAccess> = Object.fromEntries(
33
+ PROVIDER_SPECS.map((spec) => [spec.id, spec.access] as const),
34
+ ) as Record<NativeProvider, ProviderAccess>;
35
+
36
+ /* THE PROVIDERS THAT COST NOTHING, read by every surface that LEADS with a free option instead of merely
37
+ * labelling one.
61
38
  *
62
39
  * The distinction is worth the export. `accessBadge` answers "what does this row cost" for a row the user is
63
40
  * already looking at; this answers "which row should a user who has connected nothing be shown FIRST", which is
64
41
  * the connect gate's whole job. Ranking the one free channel last among equal buttons is how a user with
65
42
  * no subscription concluded the product needed one. Deriving the list keeps that promotion honest: a channel
66
- * that stops being free stops being promoted, from one edit to PROVIDER_ACCESS. */
67
- export const FREE_PROVIDERS: readonly NativeProvider[] = NATIVE_PROVIDERS.filter((provider) => PROVIDER_ACCESS[provider].kind === "free");
43
+ * that stops being free stops being promoted, from one edit to its spec row. */
44
+ export const FREE_PROVIDERS: readonly NativeProvider[] = PROVIDER_SPECS.filter((spec) => spec.access.kind === "free").map((spec) => spec.id);
68
45
  export const isFreeProvider = (provider: AgentProvider): boolean => FREE_PROVIDERS.includes(provider as NativeProvider);
69
46
 
70
- /* WHOSE ALLOWANCE A TURN ON THIS PROVIDER SPENDS, as the subject of a sentence, a third naming of the same
71
- * six ids, and the third is not redundancy. PROVIDERS names the RUNTIME the user picks ("Claude Code", "Kimi
72
- * Code") and PROVIDER_ACCESS.requirement names the thing they CONNECT ("Claude subscription", "Google sign-in");
73
- * neither reads as English in "… usage limit reached", and neither is what a spent quota belongs to.
74
- *
75
- * The routed providers are why this can't be inferred from the harness: a `gemini` turn drives Claude Opus 4.6
76
- * through Google's Antigravity channel on a plain Google sign-in, so the quota that refuses it is Google's and
77
- * Anthropic has no part in it. Saying "Claude usage limit reached" there sends the user to check the wrong
78
- * account, and to a reset that is days out on a pool they never touched. */
79
- export const PROVIDER_VENDOR: Record<NativeProvider, string> = {
80
- claude: "Claude",
81
- codex: "ChatGPT",
82
- grok: "xAI",
83
- kimi: "Kimi Code",
84
- gemini: "Google",
85
- // The plan that gets billed is Cursor's, whichever vendor's model actually answered. A Cursor turn on Claude
86
- // Opus spends Cursor's included usage and Anthropic has no part in it, the same reasoning that makes a
87
- // `gemini` turn say "Google" above.
88
- cursor: "Cursor",
89
- };
90
-
91
- // What a turn on this provider costs at the MARGIN, ordering the same three kinds by the only question a
92
- // helper spending the user's money on their behalf has to answer: free is free; a subscription is already paid
93
- // but has a quota the user watches; a key is metered, so every call is real money. Deliberately not folded into
94
- // AccessKind's declaration order, a union's order is not a runtime fact, and this one is relied on.
95
- export const ACCESS_COST: Record<AccessKind, number> = { free: 0, subscription: 1, key: 2 };
47
+ // WHOSE ALLOWANCE A TURN ON THIS PROVIDER SPENDS, as the subject of a sentence. See ProviderSpec.vendor for why
48
+ // this is a third naming of the same ids rather than a duplicate of the label or the requirement.
49
+ export const PROVIDER_VENDOR: Record<NativeProvider, string> = Object.fromEntries(
50
+ PROVIDER_SPECS.map((spec) => [spec.id, spec.vendor] as const),
51
+ ) as Record<NativeProvider, string>;
96
52
 
97
53
  /* THE PROVIDER ID OF AN `endpoint` CAPABILITY, a model API the user pointed us at, native or not, near or far.
98
54
  *
@@ -155,495 +111,52 @@ export const endpointIdOf = (provider: AgentProvider): string | undefined =>
155
111
  // so there is likewise nothing left to connect. What a turn on it COSTS is deliberately not claimed here, a
156
112
  // self-hosted model on the user's own GPU and a metered gateway key are the same shape to us, and inventing an
157
113
  // AccessKind for them would have the picker assert a price the daemon has no way to know.
158
- export const accessFor = (provider: AgentProvider): ProviderAccess | undefined => PROVIDER_ACCESS[provider as NativeProvider];
114
+ export const accessFor = (provider: AgentProvider): ProviderAccess | undefined => providerSpec(provider)?.access;
159
115
 
160
116
  // An ACP provider's label is its capability's display name, which the web layers on top, the raw id is the
161
117
  // static fallback.
162
- export const providerLabel = (provider: AgentProvider): string => PROVIDERS.find((p) => p.value === provider)?.label ?? provider;
118
+ export const providerLabel = (provider: AgentProvider): string => providerSpec(provider)?.label ?? provider;
163
119
 
164
- /* Whether a plan-limit reading for this provider is OBTAINABLE at all, one fact, on the wire, because both
165
- * halves need it and they need the same answer. The daemon reads it to decide what to even ask upstream for
166
- * (usage/translator-usage.ts); the browser reads it to say WHY an account shows no meter, which is the
167
- * difference between "this plan publishes nothing" and "we haven't measured yet", two states that look
168
- * identical as a blank row and mean opposite things.
169
- *
170
- * Four can be read, by two mechanisms that stop at the daemon's readers: Claude's rides its own turn (the
171
- * OAuth usage endpoint, agent.ts), ChatGPT's, Google's and Kimi's are pulled through the translator's
172
- * credential-scoped api-call. Kimi's endpoint is the platform's own `/coding/v1/usages`, which the Kimi Code
173
- * subscription's OAuth token reads directly, the bundled translator does not route it, but it does not have
174
- * to: the api-call substitutes that token server-side like it does for the other two.
175
- *
176
- * Grok is the one absence, because xAI's usable billing data needs a subject id CLIProxyAPI keeps out of its
177
- * auth-file listing, and the fallback probe spends a token to answer. Adding it is adding a reader and its name
178
- * here, and nothing else. */
179
- export const PLAN_LIMIT_PROVIDERS: readonly NativeProvider[] = ["claude", "codex", "gemini", "kimi"];
120
+ // Whether a plan-limit reading for this provider is OBTAINABLE at all. See ProviderSpec.planLimits for which
121
+ // four can be read, by which two mechanisms, and why the absences are absences rather than gaps.
122
+ export const PLAN_LIMIT_PROVIDERS: readonly NativeProvider[] = PROVIDER_SPECS.filter((spec) => spec.planLimits).map((spec) => spec.id);
180
123
  export const reportsPlanLimits = (provider: AgentProvider): boolean => PLAN_LIMIT_PROVIDERS.includes(provider as NativeProvider);
181
124
 
182
125
  // The harness (agentic loop) a turn runs on, orthogonal to the provider. `native` = the provider's own runtime;
183
126
  // `claude-code` = the Claude Code loop for any provider (codex/grok then route through the translator).
184
- // Surfaced for codex/grok alone. Claude is always its own Claude Code loop; kimi has no native runtime to switch
185
- // to (it only exists under this harness); and GEMINI IS THE MIRROR OF KIMI, it only exists under its native
186
- // one, because Google refuses Claude Code's traffic outright (capabilitiesOf says why). See AgentHarness in
187
- // schemas/agent.ts.
188
- //
189
- // Gemini's `native` is OpenCode rather than a Google CLI: the image ships no Gemini binary, and OpenCode is
190
- // already here driving Grok. It spends the same translator accounts a routed turn would have, what Google
191
- // refuses is the loop, never the credential.
127
+ // WHICH providers actually offer the choice is not a list any more: a spec whose two runtime records are the
128
+ // same one is a provider with nothing to choose, and every surface reads that rather than remembering the
129
+ // exceptions (Claude is always its own loop; kimi, meta and zai have no native runtime to switch to; and GEMINI
130
+ // IS THE MIRROR, it only exists under its native one, because Google refuses Claude Code's traffic outright).
131
+ // See AgentHarness in schemas/agent.ts.
192
132
  export const HARNESSES: readonly { label: string; value: AgentHarness }[] = [
193
133
  { label: "Native", value: "native" },
194
134
  { label: "Claude Code", value: "claude-code" },
195
135
  ];
196
136
 
197
- /* WHAT A PROVIDER/HARNESS PAIR CAN ACTUALLY DO, one declaration, read by both sides of the wire.
198
- *
199
- * Six runtimes serve turns behind one seam (AgentRequest in, AgentEvent frames out): the Claude Code Agent SDK
200
- * 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
201
- * the only thing that said so was a comment inside each adapter, "Ignores the Claude-only request fields",
202
- * which no surface above it could read. So the composer offered "Ask before each file edit" on a runtime whose
203
- * every tool call is pre-approved, and offered a reasoning-effort scale to a runtime that drops the field.
204
- *
205
- * A capability is listed here only if something READS it: the daemon gates a seam on it, the composer hides or
206
- * clamps a control by it, or `limitationsOf` tells the user about it. That is the whole point, an ability the
207
- * matrix claims and nothing consults is how the drift started.
208
- *
209
- * Adding a provider is a row here, not a hunt for literals; agent-catalog.test.ts walks PROVIDERS × HARNESSES
210
- * and demands one, so a pair can never be silently absent. */
211
-
212
- // An execution backend: one way a turn runs work of its own, named for the AgentCapabilities.execution axis
213
- // and for the persona switch that grants it. Adding a language is a member here and a backend in the daemon's
214
- // execution/ module, never a new one-off tool wired where nothing else can see it.
215
- export type ExecutionBackend = "shell" | "js";
216
-
217
- export interface AgentCapabilities {
218
- // Which agentic loop actually serves the turn, the question "is the harness `claude-code`" only looks like.
219
- // Claude is always its own Claude Code loop and Kimi has no native runtime, so both run it whatever harness
220
- // the client sent; codex/grok/gemini each have a native runtime to switch away from. Names the session store
221
- // a finished conversation's transcript is backfilled from, too.
222
- //
223
- // `opencode-gemini` is the OpenCode loop pointed at Gemini rather than at xAI, and it is a SEPARATE runtime
224
- // id from `opencode` on purpose: adapter health is keyed by this field (adapter-health.ts), so sharing one
225
- // would make Grok's xAI credential decide whether the picker greys out Gemini, and the reverse.
226
- readonly runtime: "claude-code" | "codex" | "opencode" | "opencode-gemini" | "acp" | "pi" | "cursor";
227
- // Mid-turn injection (the SteeringQueue behind /agent/steer). Needs the SDK's streaming-input mode.
228
- readonly steering: boolean;
229
- // How much of the permission-mode axis the runtime honours. "modes" = every PermissionMode, with per-tool
230
- // permission cards and `mode` frames when the agent moves itself; "plan" = propose-then-approve or run, and
231
- // nothing in between, the container is the isolation boundary and every tool call is pre-approved.
232
- readonly permissions: "modes" | "plan";
233
- // Can stop mid-turn and ask the user a multiple-choice question (`question` frames).
234
- readonly questions: boolean;
235
- /* Which of the turn's tools reach the agent. "full" = http MCP tools + in-process SDK servers + plugin
236
- * checkouts + the browser servers; "tools" = all of that EXCEPT plugin checkouts; "browser" = the
237
- * process-backed browser servers alone; "http" = the http MCP tools alone, and only if the agent advertises
238
- * http MCP support; "none" = the runtime has no seam for them at all. Keeping the partial answers distinct
239
- * matters: a runtime that can drive a connected account must not be described as tool-less, and one that
240
- * cannot host daemon-side SDK servers must not claim full.
241
- *
242
- * "tools" exists for the Cursor runtime and would have been a lie either way without it. Cursor's SDK takes
243
- * stdio AND http/sse MCP servers, and its `customTools` run host callbacks in this process, which is the
244
- * seam an in-process SDK server needs, so calling it "browser" would understate it by three whole
245
- * categories. What it genuinely cannot host is a Claude Code PLUGIN checkout: that is a directory layout the
246
- * Agent SDK loads, not a protocol, and no other runtime will ever read one. So the gap is real, permanent
247
- * and worth its own word rather than being rounded to "full". */
248
- readonly mcp: "full" | "tools" | "browser" | "http" | "none";
249
- /* WHICH EXECUTION BACKENDS THE RUNTIME HOSTS, the ways a turn RUNS things, as opposed to the tools it is
250
- * handed. "shell" is the runtime's own command tool (Bash on the Claude Code loop, each foreign loop's
251
- * equivalent); "js" is the sandbox's JavaScript backend (execution/ in the daemon): the model writes a
252
- * script instead of a command line, and the daemon runs it in a permission-fenced Node subprocess.
253
- *
254
- * A first-class axis rather than a corollary of `mcp`, because the two answer different questions: `mcp`
255
- * says which TOOLS reach the model's context, this says which ways of EXECUTING the daemon can stand
256
- * behind for this runtime, with the same guard, secret and persona seams the shell gets. A runtime that
257
- * cannot host a backend simply never shows it, and the persona switch for it (PersonaPowersSchema.code)
258
- * then has nothing to grant there. */
259
- readonly execution: readonly ExecutionBackend[];
260
- // Reasoning-effort selection is forwarded to the model.
261
- readonly effort: boolean;
262
- /* The runtime can serve a turn at fast speed when asked (AgentTurn.fast). A statement about the LOOP, not
263
- * about the route: the Claude Code loop knows how to ask for it, which is why every provider this record
264
- * hands the loop to reads true here, including the ones served through the translator, whose turns the
265
- * harness will then refuse fast mode for because a translator endpoint is not first-party. That second
266
- * question is answered where the endpoint is decided (planHarnessTurn), because it is a fact about the
267
- * CREDENTIAL rather than about the runtime, and this record is a pure function of (provider, harness). */
268
- readonly fastMode: boolean;
269
- // How an isolated conversation's worktree is enforced. "namespace" = the worktree IS /work inside the turn's
270
- // mount namespace (with the tool-input rewrite as the fallback when the container can't build one); "cwd" =
271
- // the turn is merely cwd'd into the worktree, so an absolute /work path still reaches the shared checkout,
272
- // which is why those turns are told where their tree is (turn-preamble.ts).
273
- readonly isolation: "namespace" | "cwd";
274
- // Publishes its slash commands (`commands` frames) for the composer's `/` popover.
275
- readonly commands: boolean;
276
- // Runs its shell in a tmux session the terminal panel can attach to (`terminal` frames).
277
- readonly terminals: boolean;
278
- // Fails with the coded frames the daemon's auto-resume keys off (rate_limit, provider-outage), so a turn the
279
- // provider killed is re-run once the breaker says the provider is back (turn-resume.ts).
280
- readonly recovery: boolean;
281
- /* HOW MUCH OF ITS STANDING INSTRUCTIONS THIS RUNTIME WILL TAKE FROM US, the axis behind the sandbox's
282
- * system-prompt setting (SandboxSettings.systemPromptMode) and the persona's own override.
283
- *
284
- * It exists because that setting was silently a Claude Code setting. The composer offers Codex, Grok and
285
- * Gemini on their own runtimes, and a turn on any of them ignored the prompt the owner had written without
286
- * saying so anywhere, the one failure mode a settings page cannot recover from, because nothing on screen
287
- * is wrong. Naming it here means every surface reads the same answer and the daemon composes to it
288
- * (agent/system-prompt.ts), rather than each learning the exception separately.
289
- *
290
- * "replace", the whole base prompt can be swapped for the owner's text, and extra guidance appended on
291
- * top of whichever base is in force. The Claude Code loop (SDK `systemPrompt`) and native
292
- * Codex (`model_instructions_file` replaces its base; `developer_instructions` adds a
293
- * developer message, both verified on the wire against codex-cli 0.147).
294
- * "append" , extra system text only; the runtime's own base prompt stands. OpenCode takes one per
295
- * message (`system` on the prompt body), and there is no seam for replacing its base.
296
- * "none" , no system seam at all. What must still reach the model (the persona note) rides the user
297
- * message instead, which is the door the delegation note already uses.
298
- *
299
- * The BASE CHOICE. Intentic's prompt or Claude Code's, is a "replace" runtime's question and, of those,
300
- * only the Claude Code loop's: Codex's own base describes Codex's own tools, so swapping it for a prompt
301
- * written about another harness is the owner's deliberate act (their custom text), never ours. */
302
- readonly instructions: "replace" | "append" | "none";
303
- /* HOW THIS RUNTIME DISCOVERS THE WORKSPACE'S LOADED SKILLS.
304
- *
305
- * "native", the runtime scans one of the filesystem projections itself: `.agents/skills/` for Codex,
306
- * `.claude/skills/` for the Claude Code loop. Its own loader injects the catalogue and reads
307
- * the matching SKILL.md on demand, so adding our own note would duplicate it.
308
- * "prompt", the runtime has no loader the daemon can rely on. turn-plan.ts puts the same name,
309
- * description and absolute SKILL.md path into the opening user-message preamble. This is a
310
- * separate axis from `instructions`: Pi and ACP take no system prompt at all, while OpenCode
311
- * and Cursor take an append, but all four still need skill discovery. */
312
- readonly skillDiscovery: "native" | "prompt";
313
- /* WHETHER THE OWNER'S COMMAND RULEBOOK REACHES THIS RUNTIME (SandboxSettings.commandRules, decided by
314
- * guard/actions.ts commandRun, delivered by guard/command-gate.ts).
315
- *
316
- * It exists because the rulebook was silently a Claude Code rulebook. The gate is a PreToolUse hook, which
317
- * is an Agent SDK seam, so an owner who set `files.destructive: hold` was asked on a Claude turn and never
318
- * on a Codex, Grok, Gemini, Pi or ACP one, with nothing on screen saying so. Same failure mode as the
319
- * `instructions` axis above, and the same fix: name it once, let every surface read it.
320
- *
321
- * "hooks" , the runtime's own pre-execution hook carries the verdict and a HOLD can park the call.
322
- * The Claude Code loop, whose PreToolUse hook fires even under bypassPermissions.
323
- * "approval" , the vendor publishes a per-call approval channel the daemon answers from the same
324
- * rulebook, and a hold parks on a card because the vendor is blocked on the answer
325
- * (Codex's `item/commandExecution/requestApproval`, ACP's `session/request_permission`).
326
- * Weaker than "hooks" in one stated way: the vendor decides WHICH calls it asks about, so a
327
- * class it never raises is a class the rulebook cannot see. What it does raise is judged by
328
- * the same decide fn.
329
- * "refuse-only", the same channel, but the vendor puts a CLOCK on the wait, so a hold cannot park and
330
- * arrives as a refusal instead. OpenCode's turn has an inactivity watchdog that reads a
331
- * paused approval as a stalled turn; a card there would break the turn rather than gate it.
332
- * `deny` rules work fully; `hold` rules stop the command and say they could not ask.
333
- * "none" , the runtime publishes no seam before it runs a command, so no rule can apply. Pi runs its
334
- * bash in-process with no approval channel at all.
335
- *
336
- * The taint floor rides this axis too: a runtime with no consult has no place to apply it, which is why
337
- * `conversationTainted` must read a "none" runtime as tainted rather than as clean (guard/turn-taint.ts). */
338
- readonly rulebook: "hooks" | "approval" | "refuse-only" | "none";
339
- /* WHETHER A STORED CREDENTIAL IS MASKED IN WHAT THIS RUNTIME'S MODEL READS (secrets/secret-registry.ts and
340
- * the two seams around it).
341
- *
342
- * "masked" is the full round trip: every stored value is replaced by its `{{secret:name}}` reference on the
343
- * way into the model's context, and the same reference resolves back to the value at the two exits that
344
- * spend it (a shell command, a script). The Claude Code loop, via PostToolUse and PreToolUse.
345
- *
346
- * "none" is a STRUCTURAL limit, not an unfinished wire, and it is the reason this axis is honest rather
347
- * than aspirational. On every other runtime the tool runs inside the VENDOR'S own loop: the model has read
348
- * the result before the daemon sees any frame about it, so there is no seam left to rewrite. A PostToolUse
349
- * hook is the only thing that can edit what a model reads, and only the Claude Code loop has one. Nothing
350
- * about wiring more transports changes that, which is why the answer here is a disclosure and the real fix
351
- * is to stop putting credentials where a vendor's tool can read them at all.
352
- *
353
- * Read by limitationsOf, and by agent/system-prompt.ts, which must not teach the reference language to a
354
- * runtime that has no exit for it. */
355
- readonly secrets: "masked" | "none";
356
- }
357
-
358
- // The Claude Code Agent SDK loop, the ceiling every other runtime is measured against, and the only one that
359
- // owns the whole request: permission callbacks, the ask tool, plugins, hooks, and the spawn seam a mount
360
- // namespace needs.
361
- const CLAUDE_CODE: AgentCapabilities = {
362
- runtime: "claude-code",
363
- steering: true,
364
- permissions: "modes",
365
- questions: true,
366
- mcp: "full",
367
- // The one loop with a seam the daemon can put its own backend through, so it hosts the JS backend beside
368
- // its Bash. Every other runtime below hosts only its own shell.
369
- execution: ["shell", "js"],
370
- effort: true,
371
- fastMode: true,
372
- isolation: "namespace",
373
- commands: true,
374
- terminals: true,
375
- recovery: true,
376
- instructions: "replace",
377
- skillDiscovery: "native",
378
- // The only runtime with a pre-execution hook of its own, which is why it is the only one where a HOLD can
379
- // park the call and wait for a card rather than having to refuse it.
380
- rulebook: "hooks",
381
- secrets: "masked",
137
+ // Whether the harness axis is a real choice for this provider, or a switch whose two positions run the same
138
+ // loop. Derived, so a provider that gains a native runtime tomorrow gains the switch with it, and one that
139
+ // never had one never shows a control that does nothing.
140
+ export const harnessChoosable = (provider: AgentProvider): boolean => {
141
+ const spec = providerSpec(provider);
142
+ return spec !== undefined && spec.runtimes.native.runtime !== spec.runtimes.claudeCode.runtime;
382
143
  };
383
144
 
384
- /* Codex app-server: item-level events, process-backed MCP servers, and the four interactive seams its protocol
385
- * actually publishes, `turn/steer` for mid-turn injection, the experimental `item/tool/requestUserInput` server
386
- * request behind a question card, `skills/list` for the `/` popover (a picked command rides back as a structured
387
- * skill input), and the same mount namespace the Claude Code loop gets, because app-server is a child process
388
- * the adapter spawns and nsenter can put it in the turn's namespace like any other.
145
+ /* THE PAIR ITS RECORD. A native provider answers its spec row's two runtimes. An `endpoint/<id>` provider is
146
+ * a model API the user configured, driven BY the Claude Code loop on either harness, so it gets that loop's
147
+ * full ceiling, which is the entire point of routing a model through it rather than adopting a second runtime.
148
+ * The reserved `pi` id is the Pi coding agent on its own RPC runtime (harness doesn't apply. Pi is its own
149
+ * loop, like ACP). Any other id that names no native provider is an installed `agent`-kind capability, served
150
+ * over ACP.
389
151
  *
390
- * Browser servers ride the per-thread config; daemon-side SDK servers, plugins and server-initiated APPROVALS
391
- * stay unwired, the container is the isolation boundary, so approvals are declined by design rather than
392
- * missing (codex-app-server.ts refuses every server request but the question one). */
393
- const CODEX: AgentCapabilities = {
394
- runtime: "codex",
395
- steering: true,
396
- permissions: "plan",
397
- questions: true,
398
- mcp: "browser",
399
- execution: ["shell"],
400
- effort: true,
401
- fastMode: false,
402
- isolation: "namespace",
403
- commands: true,
404
- terminals: false,
405
- recovery: false,
406
- /* Both halves, through the per-thread `config` block the adapter already sends: `model_instructions_file`
407
- * takes the place of Codex's own base prompt, `developer_instructions` arrives as an extra developer
408
- * message ahead of its skills and team blocks. Verified against codex-cli 0.147 by reading what actually
409
- * reached the wire, the keys are undocumented, and a strings dump proves only that they parse. */
410
- instructions: "replace",
411
- skillDiscovery: "native",
412
- /* App-server publishes `item/commandExecution/requestApproval`, whose params carry the command text, and
413
- * takes `accept`/`decline` back (codex-cli 0.147's own generated JSON Schema, read with
414
- * `codex app-server generate-json-schema`). The daemon only asks Codex to raise those requests when the
415
- * owner has written command rules, so an unconfigured workspace keeps `approvalPolicy: "never"` and pays
416
- * nothing (codex/codex-agent.ts threadOptions). */
417
- rulebook: "approval",
418
- secrets: "none",
419
- };
420
-
421
- // OpenCode (the Grok runtime): its own agentic loop, its own tools, allow-all permissions. It takes a model id,
422
- // a prompt and one system message of ours, no effort scale, no tools of ours, no command list.
423
- const OPENCODE: AgentCapabilities = {
424
- runtime: "opencode",
425
- steering: false,
426
- permissions: "plan",
427
- questions: false,
428
- mcp: "none",
429
- execution: ["shell"],
430
- effort: false,
431
- fastMode: false,
432
- isolation: "cwd",
433
- commands: false,
434
- terminals: false,
435
- recovery: false,
436
- // `system` on the prompt body, per message. It ADDS to OpenCode's own prompt, there is no seam for
437
- // replacing that, so a custom prompt lands here as extra instructions, and the settings page says so
438
- // rather than letting "replaces everything" quietly mean something else on two providers.
439
- instructions: "append",
440
- skillDiscovery: "prompt",
441
- /* OpenCode asks over its own permission channel (`permission.updated`, replied on
442
- * `/session/{id}/permissions/{permissionID}`, vocabulary once/always/reject), and the daemon judges what it
443
- * raises with the same decide fn every other runtime uses.
444
- *
445
- * REFUSE-ONLY because of its watchdog, not because of its protocol. A Grok/Gemini turn is aborted after two
446
- * minutes without an event for its session (grok/grok-agent.ts GROK_INACTIVITY_MS), and a permission paused
447
- * on a person is exactly that silence, so a parked card would turn "ask me" into a broken turn. A `deny`
448
- * rule is enforced in full; a `hold` stops the command and tells the agent it could not be asked about. */
449
- rulebook: "refuse-only",
450
- secrets: "none",
451
- };
452
-
453
- /* The same OpenCode loop, serving Gemini instead of xAI, identical abilities, which is the point of giving it
454
- * its own row rather than its own record shape.
455
- *
456
- * It exists because the alternative was Gemini's ONLY route being the Claude Code loop, and that loop announces
457
- * itself to whatever it is pointed at: the CLI prepends its own "You are a Claude agent, built on Anthropic's
458
- * Claude Agent SDK." to every request, baked into the binary with no option to suppress it. Google's Antigravity
459
- * channel matches that exact sentence and refuses the request, reported as a quota error, which sent the
460
- * translator walking all 31 connected accounts looking for one with room, ~60s per attempt, none of which could
461
- * ever have answered. Under this runtime the request carries OpenCode's own prompt, so the turn is simply not
462
- * Claude Code traffic and the block has nothing to match.
463
- *
464
- * The credential is unchanged: both harnesses reach Google through the translator and the same auth files. Only
465
- * the loop around the model differs. */
466
- const OPENCODE_GEMINI: AgentCapabilities = {
467
- ...OPENCODE,
468
- runtime: "opencode-gemini",
469
- };
470
-
471
- // Any agent speaking the Agent Client Protocol: a documented floor rather than the native ceiling. It publishes
472
- // commands, runs its terminals in the conversation's tmux session, and takes our http MCP tools when it says it
473
- // can, but it owns its own model, effort and permission posture.
474
- const ACP: AgentCapabilities = {
475
- runtime: "acp",
476
- steering: false,
477
- permissions: "plan",
478
- questions: false,
479
- mcp: "http",
480
- execution: ["shell"],
481
- effort: false,
482
- fastMode: false,
483
- isolation: "cwd",
484
- commands: true,
485
- terminals: true,
486
- recovery: false,
487
- // ACP's `session/new` and `session/prompt` carry no system field: the agent owns its own instructions the
488
- // same way it owns its model and its permission posture. The persona note takes the user message instead.
489
- instructions: "none",
490
- skillDiscovery: "prompt",
491
- /* `session/request_permission` is in the protocol floor, so every conforming agent has the channel and the
492
- * daemon answers it from the rulebook (acp/acp-permissions.ts). The caveat the "approval" value already
493
- * carries is at its widest here: WHICH calls an agent asks about is entirely the agent's choice, and one
494
- * that never asks is one no rule can reach. */
495
- rulebook: "approval",
496
- secrets: "none",
497
- };
498
-
499
- /* THE PI CAPABILITY ID IS RESERVED, the same way the six native ids are: an `agent`-kind capability installed
500
- * under it is served over Pi's own RPC protocol rather than ACP. Pi closed ACP support deliberately (its RPC
501
- * mode is the embedding surface), and the two want different records. A bare id rather than a namespace like
502
- * `endpoint/`, because there is exactly one Pi runtime to name; capabilitiesOf still answers from the id alone,
503
- * which is what keeps it a pure function of (provider, harness). */
504
- export const PI_PROVIDER = "pi";
505
-
506
- // Pi driven over its RPC mode (`pi --mode rpc`, strict-LF JSONL over stdio): above the ACP floor and below the
507
- // Claude Code ceiling. Its `steer` command is real mid-turn injection; `set_thinking_level` takes the effort
508
- // tiers; `get_commands` publishes its extension/skill commands. It has no MCP seam (Pi's own extensions are its
509
- // tool surface), no approval channel (plan is the shared two-phase emulation), and runs bash in-process, no
510
- // tmux session for the terminal panel to attach to.
511
- const PI: AgentCapabilities = {
512
- runtime: "pi",
513
- steering: true,
514
- permissions: "plan",
515
- questions: false,
516
- mcp: "none",
517
- execution: ["shell"],
518
- effort: true,
519
- fastMode: false,
520
- isolation: "cwd",
521
- commands: true,
522
- terminals: false,
523
- recovery: false,
524
- // Pi's RPC opens a session with a prompt and steers it; nothing in that protocol sets standing
525
- // instructions, so like ACP it hears the persona note through the user message.
526
- instructions: "none",
527
- skillDiscovery: "prompt",
528
- /* THE ONE RUNTIME WITH NO SEAM AT ALL. Pi runs bash in-process and its RPC publishes no approval request,
529
- * so there is nothing to consult before a command runs and no rule the owner writes can apply here. Said
530
- * out loud rather than left to be discovered: limitationsOf renders it, and the taint floor treats a "none"
531
- * runtime as permanently tainted, because a bit nobody can act on is worse than no bit. */
532
- rulebook: "none",
533
- secrets: "none",
534
- };
535
-
536
- /* CURSOR'S OWN AGENT RUNTIME, driven through `@cursor/sdk`, the SDK Anysphere publishes, in this daemon's own
537
- * process. The second-richest row in this file after the Claude Code loop, and the reason is the SDK rather
538
- * than the vendor: it is an EMBEDDING surface, not a CLI wrapped in a pipe, so most of the seams the other
539
- * foreign runtimes lack are simply function arguments here.
540
- *
541
- * WHY NOT THROUGH OPENCODE, which is already in this image and already serves two providers. Every Cursor
542
- * bridge for OpenCode is a community reverse-engineering of Cursor's private agent RPC or a localhost shim
543
- * around its CLI, and the OPENCODE record above is the weakest in this file. Routing Cursor through it would
544
- * have capped a first-party SDK at Grok's ceiling and made the row depend on a third party's spare time.
545
- *
546
- * WHY THE HARNESS AXIS DOESN'T APPLY, the same way it doesn't for Gemini, and for the mirror-image reason.
547
- * Gemini has no Claude Code route because Google refuses that traffic; Cursor has none because there is no
548
- * translator route at all, CLIProxyAPI does not serve Cursor as a provider (asked for repeatedly upstream and
549
- * closed as not planned), and Cursor publishes no OpenAI-compatible endpoint on a subscription. The SDK IS the
550
- * only door, so `capabilitiesOf` answers this record whatever harness the client sent.
551
- *
552
- * The three axes below that read weaker than they could are deliberate, not unfinished: see the notes on each. */
553
- const CURSOR: AgentCapabilities = {
554
- runtime: "cursor",
555
- // The SDK's Run can be cancelled but not written to mid-flight: a second `send` on a busy agent is an
556
- // AgentBusyError, not an injection. So the steering queue has nowhere to go and the composer hides it.
557
- steering: false,
558
- /* Cursor's OWN plan mode (`mode: "agent" | "plan"`), not this repo's two-phase emulation, which is the
559
- * better version of the same bargain: the model is put in a read-only posture by the vendor rather than
560
- * being asked to behave.
561
- *
562
- * Not "modes", and that is the honest half. The hook seam below can gate shell, MCP, file reads and file
563
- * edits, which is most of the tool surface but not all of it, and a per-tool posture with a silent gap in
564
- * it is worse than one that says where it stops. */
565
- permissions: "plan",
566
- /* TRUE BECAUSE WE SUPPLY THE TOOL, not because Cursor's own askQuestion is wired. That one is put in
567
- * `disallowedTools`: in a headless run it has been reported to answer itself with a fabricated "Questions
568
- * skipped by the user", which is the single worst failure shape available here, an agent acting on consent
569
- * nobody gave. The ask tool the daemon registers through `customTools` runs in this process, parks on a
570
- * real card, and cannot invent an answer because it is the thing that receives one. */
571
- questions: true,
572
- // stdio + http/sse MCP servers, plus host callbacks through `customTools` (which is where the browser stack
573
- // and the in-process SDK servers land). Everything but a Claude Code plugin checkout, see the axis note.
574
- mcp: "tools",
575
- execution: ["shell"],
576
- /* Cursor publishes effort as MODEL PARAMETERS rather than as one scale (`ModelListItem.parameters` /
577
- * `variants` → `ModelSelection.params`), so the shared tiers are mapped onto whatever the selected model
578
- * declares, and a model that declares none simply offers no control. True here because the axis is
579
- * forwardable at all; which tiers exist is the live catalog's answer, not this record's. */
580
- effort: true,
581
- fastMode: false,
582
- /* "cwd", and this is the one place the SDK's in-process design costs something. A namespace is built around
583
- * a CHILD the daemon spawns (that is how the Claude Code loop and Codex app-server get theirs); Cursor's
584
- * loop runs inside the daemon, whose own /work must stay the shared checkout, so an isolated conversation
585
- * gets its worktree by working directory and the turn is told where its tree is (turn-preamble.ts). */
586
- isolation: "cwd",
587
- // Cursor's commands are files on disk (`.cursor/commands`), which the SDK loads but does not publish back,
588
- // so there is no list to hand the `/` popover.
589
- commands: false,
590
- // The SDK runs its shell in-process; there is no tmux session for the terminal panel to attach to.
591
- terminals: false,
592
- // The SDK throws typed errors (RateLimitError and friends) rather than dissolving a refusal into prose, so
593
- // the adapter can file the coded frames auto-resume keys off.
594
- recovery: true,
595
- /* "append", the OpenCode answer, reached by a completely different road. There is no system-prompt argument
596
- * on `Agent.create`; what there is, is the `beforeSubmitPrompt` hook, whose reply carries
597
- * `additional_context` that is folded into the request. So the owner's prompt and the persona note DO reach
598
- * the model, on top of Cursor's own base prompt, and nothing can replace that base. */
599
- instructions: "append",
600
- skillDiscovery: "prompt",
601
- /* THE FULL HOOK TIER, the only foreign runtime that reaches it. Cursor reads `.cursor/hooks.json` in its
602
- * local runtime, and `beforeShellExecution` answers with `allow` / `deny` / `ask` plus the messages that
603
- * explain it, with `failClosed` available so a crashed gate blocks instead of waving the command through.
604
- *
605
- * What earns "hooks" rather than "approval" is that a HOLD can genuinely park: the hook is a process the
606
- * daemon wrote, so it blocks on the card and the vendor is simply waiting on a script, exactly the shape
607
- * that makes the Claude Code loop's PreToolUse hook able to stop and ask. The vendor never decides which
608
- * calls to raise, either, which is the caveat the "approval" tier carries and this one does not. */
609
- rulebook: "hooks",
610
- /* "none", and structurally so, like every other foreign runtime. Masking needs a seam that rewrites what
611
- * the model READS after a tool ran; Cursor's `afterShellExecution` fires with the output but its reply is
612
- * discarded upstream, and `beforeReadFile` sees the content only to allow or deny it. Both are gates, not
613
- * filters, so there is nothing here to substitute a reference back into. */
614
- secrets: "none",
615
- };
616
-
617
- // The pair → its record. An `endpoint/<id>` provider is a model API the user configured, driven BY the Claude
618
- // Code loop on either harness, so it gets that loop's full ceiling, which is the entire point of routing a
619
- // model through it rather than adopting a second runtime. The reserved `pi` id is the Pi coding agent on its
620
- // own RPC runtime (harness doesn't apply. Pi is its own loop, like ACP). Any other id that names no native
621
- // provider is an installed `agent`-kind capability, served over ACP.
152
+ * The three providers whose spec names ONE record on both harnesses are answering the harness with a fact
153
+ * rather than a preference, and each has its own reason, stated on its row: Kimi, Meta and Z.ai have no native
154
+ * runtime at all; Google refuses Claude Code's traffic; Cursor has no route but its own SDK. Reading it off the
155
+ * table is what makes those structural instead of a rule each surface has to remember. */
622
156
  export const capabilitiesOf = (provider: AgentProvider, harness: AgentHarness): AgentCapabilities => {
623
- if (provider === "codex") {
624
- return harness === "claude-code" ? CLAUDE_CODE : CODEX;
625
- }
626
- if (provider === "grok") {
627
- return harness === "claude-code" ? CLAUDE_CODE : OPENCODE;
628
- }
629
- /* GEMINI IGNORES THE HARNESS, and it is the only routed provider that does. The Claude Code loop announces
630
- * itself in every request it sends and Google refuses on that announcement (see OPENCODE_GEMINI), so
631
- * "Gemini under Claude Code" was never a slower or poorer option, it was one that could not complete a
632
- * single turn, on any of the connected accounts, ever.
633
- *
634
- * Answering OPENCODE_GEMINI whatever the caller asked for is what makes that structural rather than a rule
635
- * each surface has to remember. Everything downstream reads the runtime off this record, the adapter that
636
- * serves a turn, the transcript store, the quick helper's choice of loop, so there is exactly one place
637
- * where Gemini's loop is decided, and no way left to route Claude Code traffic at Google by asking for it. */
638
- if (provider === "gemini") {
639
- return OPENCODE_GEMINI;
640
- }
641
- // Cursor ignores the harness for the mirror of Gemini's reason: there is no route to it but its own SDK. No
642
- // translator serves Cursor and Cursor publishes no model endpoint on a subscription, so "Cursor under Claude
643
- // Code" names a road that does not exist. Answering this record whatever was asked for is what keeps that a
644
- // fact of the catalog rather than a rule each surface has to remember.
645
- if (provider === "cursor") {
646
- return CURSOR;
157
+ const spec = providerSpec(provider);
158
+ if (spec !== undefined) {
159
+ return harness === "claude-code" ? spec.runtimes.claudeCode : spec.runtimes.native;
647
160
  }
648
161
  if (isEndpointProvider(provider)) {
649
162
  return CLAUDE_CODE;
@@ -651,9 +164,16 @@ export const capabilitiesOf = (provider: AgentProvider, harness: AgentHarness):
651
164
  if (provider === PI_PROVIDER) {
652
165
  return PI;
653
166
  }
654
- return (NATIVE_PROVIDERS as readonly string[]).includes(provider) ? CLAUDE_CODE : ACP;
167
+ return ACP;
655
168
  };
656
169
 
170
+ /* THE PI CAPABILITY ID IS RESERVED, the same way the native ids are: an `agent`-kind capability installed
171
+ * under it is served over Pi's own RPC protocol rather than ACP. Pi closed ACP support deliberately (its RPC
172
+ * mode is the embedding surface), and the two want different records. A bare id rather than a namespace like
173
+ * `endpoint/`, because there is exactly one Pi runtime to name; capabilitiesOf still answers from the id alone,
174
+ * which is what keeps it a pure function of (provider, harness). */
175
+ export const PI_PROVIDER = "pi";
176
+
657
177
  // Which permission modes a runtime can actually be put in. Under "plan" every other mode collapses onto the
658
178
  // autonomous posture the runtime already runs, so offering them would be offering four names for two behaviours.
659
179
  export const modesFor = (capabilities: AgentCapabilities): readonly PermissionMode[] =>
@@ -694,9 +214,7 @@ export const limitationsOf = (capabilities: AgentCapabilities): string[] => [
694
214
  ...(capabilities.instructions === "append" ? ["system prompt appended, not replaced"] : []),
695
215
  ...(capabilities.instructions === "none" ? ["system prompt not applied"] : []),
696
216
  ...(capabilities.rulebook === "approval" ? ["command rules apply only to calls this agent raises"] : []),
697
- ...(capabilities.rulebook === "refuse-only"
698
- ? ["command rules can refuse but not hold"]
699
- : []),
217
+ ...(capabilities.rulebook === "refuse-only" ? ["command rules can refuse but not hold"] : []),
700
218
  ...(capabilities.rulebook === "none" ? ["command rules not applied"] : []),
701
219
  ...(capabilities.secrets === "none" ? ["secrets reach the model unmasked"] : []),
702
220
  ];
@@ -716,14 +234,16 @@ export const CLAUDE_SEED_MODELS: readonly Model[] = [
716
234
 
717
235
  // The STATIC floor of the model catalog, harness-independent: every provider's real list is the daemon's live
718
236
  // catalog (/providers/{provider}/models, discovery with a persisted/seed floor, never empty),
719
- // which consumers layer on top. Codex/grok are empty here (nothing sensible to offer before the live load, and
720
- // under the Claude Code harness they route through the translator, which serves the SAME subscription model ids
721
- // as the native catalog, so the harness no longer changes the list).
237
+ // which consumers layer on top. Every provider but Claude is empty here (nothing sensible to offer before the
238
+ // live load, and under the Claude Code harness the routed ones serve the SAME subscription model ids as their
239
+ // native catalog, so the harness no longer changes the list).
722
240
  export const modelsFor = (provider: AgentProvider): CatalogOption[] => {
723
241
  if (provider === "claude") {
724
242
  return CLAUDE_SEED_MODELS.map((model) => ({ label: model.label, value: model.id }));
725
243
  }
726
- // Codex/Grok/Kimi/Gemini (live catalog only) and ACP providers (the agent owns its model): nothing static.
244
+ // Every other native provider (live catalog only) and ACP providers (the agent owns its model): nothing
245
+ // static. A keyed provider's floor is the daemon's, not the browser's: it is discovered from the vendor's
246
+ // own /models with a seed behind it, and duplicating that seed here would be a second list to keep right.
727
247
  return [];
728
248
  };
729
249
 
@@ -759,8 +279,9 @@ export const sendableEffort = (effort: string | undefined, thinking: boolean | u
759
279
  *
760
280
  * - the RUNTIME has to know how to ask (capabilities.fastMode). Only the Claude Code loop does.
761
281
  * - the ROUTE has to be first-party. Every non-Claude provider the Claude Code loop serves is served through
762
- * the sandbox's translator, and the harness refuses fast mode on a non-Anthropic endpoint ("not_first_party")
763
- * , so a `grok` turn on the claude-code harness reads true on the capability and still cannot go fast.
282
+ * the sandbox's translator or pointed at the vendor's own endpoint, and the harness refuses fast mode on a
283
+ * non-Anthropic endpoint ("not_first_party"), so a `grok` turn on the claude-code harness reads true on the
284
+ * capability and still cannot go fast.
764
285
  * - the MODEL has to publish it, which is the `fast` badge Anthropic's own catalog reports per model
765
286
  * (claude-models.ts maps supportsFastMode onto it). Curating a list of ids here instead is what this repo
766
287
  * deliberately does not do, a model that gains or loses fast mode moves the badge, and this follows.