@intentic/sandbox-contract 1.176.3 → 1.209.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (334) hide show
  1. package/README.md +65 -0
  2. package/dist/agent-catalog.d.ts +15 -2
  3. package/dist/agent-catalog.d.ts.map +1 -1
  4. package/dist/agent-catalog.js +50 -1
  5. package/dist/agent-catalog.js.map +1 -1
  6. package/dist/agent-run-model.d.ts +3 -0
  7. package/dist/agent-run-model.d.ts.map +1 -0
  8. package/dist/agent-run-model.js +10 -0
  9. package/dist/agent-run-model.js.map +1 -0
  10. package/dist/capability-env.d.ts +2 -0
  11. package/dist/capability-env.d.ts.map +1 -0
  12. package/dist/capability-env.js +2 -0
  13. package/dist/capability-env.js.map +1 -0
  14. package/dist/chores/chores.d.ts +9 -0
  15. package/dist/chores/chores.d.ts.map +1 -1
  16. package/dist/chores/chores.js +257 -18
  17. package/dist/chores/chores.js.map +1 -1
  18. package/dist/chores/digest.js +0 -0
  19. package/dist/chores/digest.js.map +1 -1
  20. package/dist/chores/extension-update.d.ts +9 -0
  21. package/dist/chores/extension-update.d.ts.map +1 -0
  22. package/dist/chores/extension-update.js +15 -0
  23. package/dist/chores/extension-update.js.map +1 -0
  24. package/dist/chores/fix-deps.d.ts +10 -0
  25. package/dist/chores/fix-deps.d.ts.map +1 -0
  26. package/dist/chores/fix-deps.js +18 -0
  27. package/dist/chores/fix-deps.js.map +1 -0
  28. package/dist/chores/index.d.ts +7 -2
  29. package/dist/chores/index.d.ts.map +1 -1
  30. package/dist/chores/index.js +4 -1
  31. package/dist/chores/index.js.map +1 -1
  32. package/dist/chores/probes.d.ts.map +1 -1
  33. package/dist/chores/probes.js +127 -3
  34. package/dist/chores/probes.js.map +1 -1
  35. package/dist/chores/stack.d.ts +27 -0
  36. package/dist/chores/stack.d.ts.map +1 -0
  37. package/dist/chores/stack.js +121 -0
  38. package/dist/chores/stack.js.map +1 -0
  39. package/dist/chores/verdict.d.ts +2 -1
  40. package/dist/chores/verdict.d.ts.map +1 -1
  41. package/dist/chores/verdict.js +53 -6
  42. package/dist/chores/verdict.js.map +1 -1
  43. package/dist/contract-lock.d.ts +3 -0
  44. package/dist/contract-lock.d.ts.map +1 -0
  45. package/dist/contract-lock.js +35 -0
  46. package/dist/contract-lock.js.map +1 -0
  47. package/dist/contracts/activity.contract.d.ts +10 -0
  48. package/dist/contracts/activity.contract.d.ts.map +1 -1
  49. package/dist/contracts/agent.contract.d.ts +230 -20
  50. package/dist/contracts/agent.contract.d.ts.map +1 -1
  51. package/dist/contracts/agent.contract.js +2 -1
  52. package/dist/contracts/agent.contract.js.map +1 -1
  53. package/dist/contracts/agents.contract.d.ts +847 -1
  54. package/dist/contracts/agents.contract.d.ts.map +1 -1
  55. package/dist/contracts/agents.contract.js +3 -1
  56. package/dist/contracts/agents.contract.js.map +1 -1
  57. package/dist/contracts/automations.contract.d.ts +89 -0
  58. package/dist/contracts/automations.contract.d.ts.map +1 -1
  59. package/dist/contracts/automations.contract.js +3 -1
  60. package/dist/contracts/automations.contract.js.map +1 -1
  61. package/dist/contracts/capabilities.contract.d.ts +108 -32
  62. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  63. package/dist/contracts/capabilities.contract.js +5 -1
  64. package/dist/contracts/capabilities.contract.js.map +1 -1
  65. package/dist/contracts/chores.contract.d.ts +43 -0
  66. package/dist/contracts/chores.contract.d.ts.map +1 -1
  67. package/dist/contracts/ci.contract.d.ts +5 -0
  68. package/dist/contracts/ci.contract.d.ts.map +1 -1
  69. package/dist/contracts/ci.contract.js +2 -2
  70. package/dist/contracts/ci.contract.js.map +1 -1
  71. package/dist/contracts/claude.contract.d.ts +3 -14
  72. package/dist/contracts/claude.contract.d.ts.map +1 -1
  73. package/dist/contracts/claude.contract.js +2 -3
  74. package/dist/contracts/claude.contract.js.map +1 -1
  75. package/dist/contracts/drafts.contract.d.ts +4 -0
  76. package/dist/contracts/drafts.contract.d.ts.map +1 -1
  77. package/dist/contracts/endpoints.contract.d.ts +28 -12
  78. package/dist/contracts/endpoints.contract.d.ts.map +1 -1
  79. package/dist/contracts/endpoints.contract.js +9 -0
  80. package/dist/contracts/endpoints.contract.js.map +1 -1
  81. package/dist/contracts/extensions.contract.d.ts +399 -69
  82. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  83. package/dist/contracts/extensions.contract.js +15 -1
  84. package/dist/contracts/extensions.contract.js.map +1 -1
  85. package/dist/contracts/git.contract.d.ts +209 -11
  86. package/dist/contracts/git.contract.d.ts.map +1 -1
  87. package/dist/contracts/git.contract.js +12 -2
  88. package/dist/contracts/git.contract.js.map +1 -1
  89. package/dist/contracts/grok.contract.d.ts +0 -13
  90. package/dist/contracts/grok.contract.d.ts.map +1 -1
  91. package/dist/contracts/grok.contract.js +1 -2
  92. package/dist/contracts/grok.contract.js.map +1 -1
  93. package/dist/contracts/host.contract.d.ts +40 -0
  94. package/dist/contracts/host.contract.d.ts.map +1 -1
  95. package/dist/contracts/host.contract.js +3 -2
  96. package/dist/contracts/host.contract.js.map +1 -1
  97. package/dist/contracts/loops.contract.d.ts +343 -0
  98. package/dist/contracts/loops.contract.d.ts.map +1 -0
  99. package/dist/contracts/loops.contract.js +11 -0
  100. package/dist/contracts/loops.contract.js.map +1 -0
  101. package/dist/contracts/panels.contract.d.ts +6 -0
  102. package/dist/contracts/panels.contract.d.ts.map +1 -1
  103. package/dist/contracts/personas.contract.d.ts +112 -0
  104. package/dist/contracts/personas.contract.d.ts.map +1 -0
  105. package/dist/contracts/personas.contract.js +13 -0
  106. package/dist/contracts/personas.contract.js.map +1 -0
  107. package/dist/contracts/ports.contract.d.ts +1 -0
  108. package/dist/contracts/ports.contract.d.ts.map +1 -1
  109. package/dist/contracts/prepush.contract.d.ts +1 -0
  110. package/dist/contracts/prepush.contract.d.ts.map +1 -1
  111. package/dist/contracts/prepush.contract.js.map +1 -1
  112. package/dist/contracts/{codex.contract.d.ts → providers.contract.d.ts} +11 -3
  113. package/dist/contracts/providers.contract.d.ts.map +1 -0
  114. package/dist/contracts/providers.contract.js +6 -0
  115. package/dist/contracts/providers.contract.js.map +1 -0
  116. package/dist/contracts/public.contract.d.ts +24 -0
  117. package/dist/contracts/public.contract.d.ts.map +1 -0
  118. package/dist/contracts/public.contract.js +8 -0
  119. package/dist/contracts/public.contract.js.map +1 -0
  120. package/dist/contracts/push.contract.d.ts +3 -3
  121. package/dist/contracts/secrets.contract.d.ts +9 -0
  122. package/dist/contracts/secrets.contract.d.ts.map +1 -1
  123. package/dist/contracts/sessions.contract.d.ts +16 -1
  124. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  125. package/dist/contracts/sessions.contract.js +1 -1
  126. package/dist/contracts/sessions.contract.js.map +1 -1
  127. package/dist/contracts/settings.contract.d.ts +352 -58
  128. package/dist/contracts/settings.contract.d.ts.map +1 -1
  129. package/dist/contracts/settings.contract.js +2 -1
  130. package/dist/contracts/settings.contract.js.map +1 -1
  131. package/dist/contracts/share.contract.d.ts +55 -0
  132. package/dist/contracts/share.contract.d.ts.map +1 -0
  133. package/dist/contracts/share.contract.js +9 -0
  134. package/dist/contracts/share.contract.js.map +1 -0
  135. package/dist/contracts/skills.contract.d.ts +41 -0
  136. package/dist/contracts/skills.contract.d.ts.map +1 -0
  137. package/dist/contracts/skills.contract.js +9 -0
  138. package/dist/contracts/skills.contract.js.map +1 -0
  139. package/dist/contracts/system.contract.d.ts +205 -8
  140. package/dist/contracts/system.contract.d.ts.map +1 -1
  141. package/dist/contracts/system.contract.js +10 -2
  142. package/dist/contracts/system.contract.js.map +1 -1
  143. package/dist/contracts/workflows.contract.d.ts +575 -0
  144. package/dist/contracts/workflows.contract.d.ts.map +1 -0
  145. package/dist/contracts/workflows.contract.js +13 -0
  146. package/dist/contracts/workflows.contract.js.map +1 -0
  147. package/dist/contracts/workspace.contract.d.ts +18 -4
  148. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  149. package/dist/contracts/workspace.contract.js +6 -3
  150. package/dist/contracts/workspace.contract.js.map +1 -1
  151. package/dist/conversation-ids.d.ts +2 -0
  152. package/dist/conversation-ids.d.ts.map +1 -0
  153. package/dist/conversation-ids.js +123 -0
  154. package/dist/conversation-ids.js.map +1 -0
  155. package/dist/events.d.ts +523 -17
  156. package/dist/events.d.ts.map +1 -1
  157. package/dist/events.js +106 -13
  158. package/dist/events.js.map +1 -1
  159. package/dist/history-state.d.ts.map +1 -1
  160. package/dist/history-state.js +10 -0
  161. package/dist/history-state.js.map +1 -1
  162. package/dist/hostnames.d.ts +4 -0
  163. package/dist/hostnames.d.ts.map +1 -1
  164. package/dist/hostnames.js +7 -2
  165. package/dist/hostnames.js.map +1 -1
  166. package/dist/index.d.ts +3792 -478
  167. package/dist/index.d.ts.map +1 -1
  168. package/dist/index.js +39 -17
  169. package/dist/index.js.map +1 -1
  170. package/dist/listener-protocol.d.ts +77 -0
  171. package/dist/listener-protocol.d.ts.map +1 -0
  172. package/dist/listener-protocol.js +33 -0
  173. package/dist/listener-protocol.js.map +1 -0
  174. package/dist/model-order.d.ts +1 -0
  175. package/dist/model-order.d.ts.map +1 -1
  176. package/dist/model-order.js +16 -0
  177. package/dist/model-order.js.map +1 -1
  178. package/dist/output-fields.d.ts +30 -0
  179. package/dist/output-fields.d.ts.map +1 -0
  180. package/dist/output-fields.js +57 -0
  181. package/dist/output-fields.js.map +1 -0
  182. package/dist/path-refs.d.ts.map +1 -1
  183. package/dist/path-refs.js.map +1 -1
  184. package/dist/publish-drafts.d.ts +7 -0
  185. package/dist/publish-drafts.d.ts.map +1 -0
  186. package/dist/publish-drafts.js +20 -0
  187. package/dist/publish-drafts.js.map +1 -0
  188. package/dist/quick-model.d.ts +2 -1
  189. package/dist/quick-model.d.ts.map +1 -1
  190. package/dist/quick-model.js +18 -14
  191. package/dist/quick-model.js.map +1 -1
  192. package/dist/routes.d.ts +3 -0
  193. package/dist/routes.d.ts.map +1 -1
  194. package/dist/routes.js +63 -0
  195. package/dist/routes.js.map +1 -1
  196. package/dist/runtime-state.d.ts +33 -0
  197. package/dist/runtime-state.d.ts.map +1 -0
  198. package/dist/runtime-state.js +14 -0
  199. package/dist/runtime-state.js.map +1 -0
  200. package/dist/schemas.d.ts +4889 -851
  201. package/dist/schemas.d.ts.map +1 -1
  202. package/dist/schemas.js +993 -192
  203. package/dist/schemas.js.map +1 -1
  204. package/dist/search-globs.d.ts +6 -0
  205. package/dist/search-globs.d.ts.map +1 -0
  206. package/dist/search-globs.js +39 -0
  207. package/dist/search-globs.js.map +1 -0
  208. package/dist/share-paths.d.ts +8 -0
  209. package/dist/share-paths.d.ts.map +1 -0
  210. package/dist/share-paths.js +21 -0
  211. package/dist/share-paths.js.map +1 -0
  212. package/dist/title.d.ts.map +1 -1
  213. package/dist/title.js +1 -1
  214. package/dist/title.js.map +1 -1
  215. package/dist/tunnel-ids.d.ts +1 -0
  216. package/dist/tunnel-ids.d.ts.map +1 -1
  217. package/dist/tunnel-ids.js +1 -0
  218. package/dist/tunnel-ids.js.map +1 -1
  219. package/dist/versions.d.ts +4 -0
  220. package/dist/versions.d.ts.map +1 -0
  221. package/dist/versions.js +15 -0
  222. package/dist/versions.js.map +1 -0
  223. package/dist/workflow-faults.d.ts +4 -0
  224. package/dist/workflow-faults.d.ts.map +1 -0
  225. package/dist/workflow-faults.js +115 -0
  226. package/dist/workflow-faults.js.map +1 -0
  227. package/dist/workspace-state.d.ts +280 -1
  228. package/dist/workspace-state.d.ts.map +1 -1
  229. package/dist/workspace-state.js +163 -21
  230. package/dist/workspace-state.js.map +1 -1
  231. package/package.json +30 -6
  232. package/src/agent-catalog.test.ts +147 -7
  233. package/src/agent-catalog.ts +233 -21
  234. package/src/agent-run-model.test.ts +60 -0
  235. package/src/agent-run-model.ts +51 -0
  236. package/src/capability-env.ts +10 -0
  237. package/src/capability-ledger.test.ts +110 -0
  238. package/src/chores/chores.test.ts +32 -0
  239. package/src/chores/chores.ts +456 -32
  240. package/src/chores/digest.ts +0 -0
  241. package/src/chores/extension-update.ts +37 -0
  242. package/src/chores/fix-deps.ts +40 -0
  243. package/src/chores/index.ts +7 -2
  244. package/src/chores/probes.test.ts +155 -3
  245. package/src/chores/probes.ts +200 -4
  246. package/src/chores/stack.test.ts +126 -0
  247. package/src/chores/stack.ts +244 -0
  248. package/src/chores/verdict.test.ts +357 -19
  249. package/src/chores/verdict.ts +95 -21
  250. package/src/contract-lock.test.ts +17 -0
  251. package/src/contract-lock.ts +66 -0
  252. package/src/contracts/agent.contract.ts +6 -0
  253. package/src/contracts/agents.contract.ts +12 -0
  254. package/src/contracts/automations.contract.ts +17 -1
  255. package/src/contracts/capabilities.contract.ts +22 -0
  256. package/src/contracts/ci.contract.ts +10 -2
  257. package/src/contracts/claude.contract.ts +8 -4
  258. package/src/contracts/endpoints.contract.ts +21 -0
  259. package/src/contracts/extensions.contract.ts +42 -0
  260. package/src/contracts/git.contract.ts +33 -6
  261. package/src/contracts/grok.contract.ts +4 -3
  262. package/src/contracts/host.contract.ts +13 -2
  263. package/src/contracts/loops.contract.ts +62 -0
  264. package/src/contracts/personas.contract.ts +56 -0
  265. package/src/contracts/prepush.contract.ts +4 -3
  266. package/src/contracts/providers.contract.ts +23 -0
  267. package/src/contracts/public.contract.ts +14 -0
  268. package/src/contracts/sessions.contract.ts +5 -1
  269. package/src/contracts/settings.contract.ts +5 -0
  270. package/src/contracts/share.contract.ts +20 -0
  271. package/src/contracts/skills.contract.ts +24 -0
  272. package/src/contracts/system.contract.ts +27 -3
  273. package/src/contracts/workflows.contract.ts +78 -0
  274. package/src/contracts/workspace.contract.ts +10 -2
  275. package/src/conversation-ids.test.ts +61 -0
  276. package/src/conversation-ids.ts +173 -0
  277. package/src/events.test.ts +20 -1
  278. package/src/events.ts +404 -43
  279. package/src/history-state.ts +30 -1
  280. package/src/hostnames.ts +20 -6
  281. package/src/index.ts +58 -17
  282. package/src/listener-protocol.ts +96 -0
  283. package/src/model-order.test.ts +32 -1
  284. package/src/model-order.ts +56 -3
  285. package/src/output-fields.ts +111 -0
  286. package/src/path-refs.test.ts +4 -4
  287. package/src/path-refs.ts +5 -5
  288. package/src/publish-drafts.ts +59 -0
  289. package/src/quick-model.test.ts +70 -27
  290. package/src/quick-model.ts +76 -40
  291. package/src/routes.test.ts +91 -3
  292. package/src/routes.ts +135 -0
  293. package/src/runtime-state.test.ts +45 -0
  294. package/src/runtime-state.ts +124 -0
  295. package/src/schemas.test.ts +16 -29
  296. package/src/schemas.ts +3480 -552
  297. package/src/search-globs.test.ts +46 -0
  298. package/src/search-globs.ts +76 -0
  299. package/src/share-paths.test.ts +48 -0
  300. package/src/share-paths.ts +68 -0
  301. package/src/state-portability.ts +8 -8
  302. package/src/title.test.ts +20 -1
  303. package/src/title.ts +12 -3
  304. package/src/tunnel-ids.ts +15 -1
  305. package/src/versions.test.ts +39 -0
  306. package/src/versions.ts +48 -0
  307. package/src/workflow-faults.test.ts +127 -0
  308. package/src/workflow-faults.ts +188 -0
  309. package/src/workspace-state.test.ts +169 -15
  310. package/src/workspace-state.ts +427 -35
  311. package/dist/contracts/codex.contract.d.ts.map +0 -1
  312. package/dist/contracts/codex.contract.js +0 -6
  313. package/dist/contracts/codex.contract.js.map +0 -1
  314. package/dist/contracts/gemini.contract.d.ts +0 -16
  315. package/dist/contracts/gemini.contract.d.ts.map +0 -1
  316. package/dist/contracts/gemini.contract.js +0 -6
  317. package/dist/contracts/gemini.contract.js.map +0 -1
  318. package/dist/contracts/kimi.contract.d.ts +0 -16
  319. package/dist/contracts/kimi.contract.d.ts.map +0 -1
  320. package/dist/contracts/kimi.contract.js +0 -6
  321. package/dist/contracts/kimi.contract.js.map +0 -1
  322. package/dist/contracts/komodo.contract.d.ts +0 -125
  323. package/dist/contracts/komodo.contract.d.ts.map +0 -1
  324. package/dist/contracts/komodo.contract.js +0 -14
  325. package/dist/contracts/komodo.contract.js.map +0 -1
  326. package/dist/contracts/memory.contract.d.ts +0 -34
  327. package/dist/contracts/memory.contract.d.ts.map +0 -1
  328. package/dist/contracts/memory.contract.js +0 -9
  329. package/dist/contracts/memory.contract.js.map +0 -1
  330. package/src/contracts/codex.contract.ts +0 -10
  331. package/src/contracts/gemini.contract.ts +0 -11
  332. package/src/contracts/kimi.contract.ts +0 -9
  333. package/src/contracts/komodo.contract.ts +0 -36
  334. package/src/contracts/memory.contract.ts +0 -13
@@ -1,8 +1,16 @@
1
- import { type AgentHarness, type AgentProvider, type Model, NATIVE_PROVIDERS, type NativeProvider, type PermissionMode } from "./schemas.js";
1
+ import {
2
+ type AgentHarness,
3
+ type AgentProvider,
4
+ type Model,
5
+ type ModelBadge,
6
+ NATIVE_PROVIDERS,
7
+ type NativeProvider,
8
+ type PermissionMode,
9
+ } from "./schemas.js";
2
10
 
3
11
  /* The provider / harness / model catalog every picker shares (the chat menu, the automations dialog) — pure
4
12
  * data keyed by the wire vocabulary in schemas.ts, so the surfaces can't drift. Live state stays with the
5
- * consumer (native Grok's model list is the daemon's /grok/models catalog, layered on top of modelsFor by the
13
+ * consumer (native Grok's model list is the daemon's own catalog for it, layered on top of modelsFor by the
6
14
  * web; ACP providers are merged in from the installed `agent` capabilities). */
7
15
 
8
16
  export interface CatalogOption {
@@ -46,6 +54,17 @@ export const PROVIDER_ACCESS: Record<NativeProvider, ProviderAccess> = {
46
54
  gemini: { kind: "free", requirement: "Google sign-in", runs: "Gemini, Claude and GPT-OSS under Claude Code" },
47
55
  };
48
56
 
57
+ /* THE PROVIDERS THAT COST NOTHING — derived from the table above rather than named a second time, and read by
58
+ * every surface that LEADS with a free option instead of merely labelling one.
59
+ *
60
+ * The distinction is worth the export. `accessBadge` answers "what does this row cost" for a row the user is
61
+ * already looking at; this answers "which row should a user who has connected nothing be shown FIRST", which is
62
+ * the connect gate's whole job. Ranking the one free channel fifth among five equal buttons is how a user with
63
+ * no subscription concluded the product needed one. Deriving the list keeps that promotion honest: a channel
64
+ * that stops being free stops being promoted, from one edit to PROVIDER_ACCESS. */
65
+ export const FREE_PROVIDERS: readonly NativeProvider[] = NATIVE_PROVIDERS.filter((provider) => PROVIDER_ACCESS[provider].kind === "free");
66
+ export const isFreeProvider = (provider: AgentProvider): boolean => FREE_PROVIDERS.includes(provider as NativeProvider);
67
+
49
68
  /* WHOSE ALLOWANCE A TURN ON THIS PROVIDER SPENDS, as the subject of a sentence — a third naming of the same
50
69
  * five ids, and the third is not redundancy. PROVIDERS names the RUNTIME the user picks ("Claude Code", "Kimi
51
70
  * Code") and PROVIDER_ACCESS.requirement names the thing they CONNECT ("Claude subscription", "Google sign-in");
@@ -81,6 +100,26 @@ export const ACCESS_COST: Record<AccessKind, number> = { free: 0, subscription:
81
100
  * splits a pinned selection on the FIRST colon. `endpoint:ollama:qwen3` would parse as provider "endpoint" with
82
101
  * model "ollama:qwen3" — a pin that silently resolves to nothing. The capability id (entryId) excludes both
83
102
  * characters, so `endpoint/<id>` stays unambiguous in either direction. */
103
+ /* THE FREE TRIAL'S ENDPOINT ID IS RESERVED, the way `pi` is — an `endpoint`-kind capability like any model API
104
+ * the user configured, except that this one is provisioned by the DAEMON rather than added by a person, and it
105
+ * points at intentic's own pool (see the sandbox's trial/ and the platform's /trial routes).
106
+ *
107
+ * Riding the endpoint kind is the entire reason the trial needed no new turn path, no new provider and no new
108
+ * adapter: the translator already re-serves an OpenAI-compatible upstream to the Claude Code loop, so a trial
109
+ * turn is an endpoint turn and everything downstream — catalog, picker, routing — works unchanged.
110
+ *
111
+ * What the reserved id buys is the part that must NOT look the same. A trial turn passes through intentic's
112
+ * servers, which no other provider in this product does, and a user cannot consent to something they were not
113
+ * told. So every surface that names a provider asks `isTrialProvider` and says so, and the id is here — beside
114
+ * the vocabulary those surfaces already read — rather than spelled out in each of them. */
115
+ export const TRIAL_ENDPOINT_ID = "free-trial";
116
+ export const TRIAL_PROVIDER = "endpoint/free-trial";
117
+ export const isTrialProvider = (provider: AgentProvider): boolean => provider === TRIAL_PROVIDER;
118
+ // What the picker calls it, and the sentence the surfaces put underneath. One wording, so the composer's notice
119
+ // and the picker's row cannot end up describing different bargains.
120
+ export const TRIAL_LABEL = "Free trial";
121
+ export const TRIAL_NOTICE = "Trial messages pass through intentic's servers. Connect an account to chat directly.";
122
+
84
123
  export const ENDPOINT_PROVIDER_PREFIX = "endpoint/";
85
124
  export const endpointProvider = (id: string): AgentProvider => `${ENDPOINT_PROVIDER_PREFIX}${id}`;
86
125
  export const isEndpointProvider = (provider: AgentProvider): boolean => provider.startsWith(ENDPOINT_PROVIDER_PREFIX);
@@ -119,9 +158,15 @@ export const PLAN_LIMIT_PROVIDERS: readonly NativeProvider[] = ["claude", "codex
119
158
  export const reportsPlanLimits = (provider: AgentProvider): boolean => PLAN_LIMIT_PROVIDERS.includes(provider as NativeProvider);
120
159
 
121
160
  // The harness (agentic loop) a turn runs on, orthogonal to the provider. `native` = the provider's own runtime;
122
- // `claude-code` = the Claude Code loop for any provider (codex/grok then route through the translator). Only
123
- // surfaced for codex/grok claude is always its own Claude Code loop, and kimi/gemini have no native runtime
124
- // to switch to (both only exist under this harness). See AgentHarness in schemas.ts.
161
+ // `claude-code` = the Claude Code loop for any provider (codex/grok then route through the translator).
162
+ // Surfaced for codex/grok alone. Claude is always its own Claude Code loop; kimi has no native runtime to switch
163
+ // to (it only exists under this harness); and GEMINI IS THE MIRROR OF KIMI — it only exists under its native
164
+ // one, because Google refuses Claude Code's traffic outright (capabilitiesOf says why). See AgentHarness in
165
+ // schemas.ts.
166
+ //
167
+ // Gemini's `native` is OpenCode rather than a Google CLI: the image ships no Gemini binary, and OpenCode is
168
+ // already here driving Grok. It spends the same translator accounts a routed turn would have — what Google
169
+ // refuses is the loop, never the credential.
125
170
  export const HARNESSES: readonly { label: string; value: AgentHarness }[] = [
126
171
  { label: "Native", value: "native" },
127
172
  { label: "Claude Code", value: "claude-code" },
@@ -129,8 +174,8 @@ export const HARNESSES: readonly { label: string; value: AgentHarness }[] = [
129
174
 
130
175
  /* WHAT A PROVIDER/HARNESS PAIR CAN ACTUALLY DO — one declaration, read by both sides of the wire.
131
176
  *
132
- * Four runtimes serve turns behind one seam (AgentRequest in, AgentEvent frames out): the Claude Code Agent SDK
133
- * loop, Codex's exec surface, OpenCode, and any ACP agent. They do NOT do the same things, and for a long time
177
+ * Five runtimes serve turns behind one seam (AgentRequest in, AgentEvent frames out): the Claude Code Agent SDK
178
+ * loop, Codex app-server, OpenCode, any ACP agent, and Pi's RPC surface. They do NOT do the same things, and for a long time
134
179
  * the only thing that said so was a comment inside each adapter — "Ignores the Claude-only request fields" —
135
180
  * which no surface above it could read. So the composer offered "Ask before each file edit" on a runtime whose
136
181
  * every tool call is pre-approved, and offered a reasoning-effort scale to a runtime that drops the field.
@@ -141,13 +186,22 @@ export const HARNESSES: readonly { label: string; value: AgentHarness }[] = [
141
186
  *
142
187
  * Adding a provider is a row here, not a hunt for literals; agent-catalog.test.ts walks PROVIDERS × HARNESSES
143
188
  * and demands one, so a pair can never be silently absent. */
189
+
190
+ // An execution backend: one way a turn runs work of its own, named for the AgentCapabilities.execution axis
191
+ // and for the persona switch that grants it. Adding a language is a member here and a backend in the daemon's
192
+ // execution/ module — never a new one-off tool wired where nothing else can see it.
193
+ export type ExecutionBackend = "shell" | "js";
194
+
144
195
  export interface AgentCapabilities {
145
196
  // Which agentic loop actually serves the turn — the question "is the harness `claude-code`" only looks like.
146
- // Claude is always its own Claude Code loop, and kimi/gemini have no native runtime at all (both are
147
- // re-served through the translator), so all three run it whatever harness the client sent; only codex/grok
148
- // have a native runtime to switch away from. Names the session store a finished conversation's transcript is
149
- // backfilled from, too.
150
- readonly runtime: "claude-code" | "codex" | "opencode" | "acp";
197
+ // Claude is always its own Claude Code loop and Kimi has no native runtime, so both run it whatever harness
198
+ // the client sent; codex/grok/gemini each have a native runtime to switch away from. Names the session store
199
+ // a finished conversation's transcript is backfilled from, too.
200
+ //
201
+ // `opencode-gemini` is the OpenCode loop pointed at Gemini rather than at xAI, and it is a SEPARATE runtime
202
+ // id from `opencode` on purpose: adapter health is keyed by this field (adapter-health.ts), so sharing one
203
+ // would make Grok's xAI credential decide whether the picker greys out Gemini, and the reverse.
204
+ readonly runtime: "claude-code" | "codex" | "opencode" | "opencode-gemini" | "acp" | "pi";
151
205
  // Mid-turn injection (the SteeringQueue behind /agent/steer). Needs the SDK's streaming-input mode.
152
206
  readonly steering: boolean;
153
207
  // How much of the permission-mode axis the runtime honours. "modes" = every PermissionMode, with per-tool
@@ -160,8 +214,26 @@ export interface AgentCapabilities {
160
214
  // checkouts + the browser servers; "http" = the http MCP tools alone, and only if the agent advertises http
161
215
  // MCP support; "none" = the runtime has no seam for them at all.
162
216
  readonly mcp: "full" | "http" | "none";
217
+ /* WHICH EXECUTION BACKENDS THE RUNTIME HOSTS — the ways a turn RUNS things, as opposed to the tools it is
218
+ * handed. "shell" is the runtime's own command tool (Bash on the Claude Code loop, each foreign loop's
219
+ * equivalent); "js" is the sandbox's JavaScript backend (execution/ in the daemon): the model writes a
220
+ * script instead of a command line, and the daemon runs it in a permission-fenced Node subprocess.
221
+ *
222
+ * A first-class axis rather than a corollary of `mcp`, because the two answer different questions: `mcp`
223
+ * says which TOOLS reach the model's context, this says which ways of EXECUTING the daemon can stand
224
+ * behind for this runtime — with the same guard, secret and persona seams the shell gets. A runtime that
225
+ * cannot host a backend simply never shows it, and the persona switch for it (PersonaPowersSchema.code)
226
+ * then has nothing to grant there. */
227
+ readonly execution: readonly ExecutionBackend[];
163
228
  // Reasoning-effort selection is forwarded to the model.
164
229
  readonly effort: boolean;
230
+ /* The runtime can serve a turn at fast speed when asked (AgentTurn.fast). A statement about the LOOP, not
231
+ * about the route: the Claude Code loop knows how to ask for it, which is why every provider this record
232
+ * hands the loop to reads true here — including the ones served through the translator, whose turns the
233
+ * harness will then refuse fast mode for because a translator endpoint is not first-party. That second
234
+ * question is answered where the endpoint is decided (planHarnessTurn), because it is a fact about the
235
+ * CREDENTIAL rather than about the runtime, and this record is a pure function of (provider, harness). */
236
+ readonly fastMode: boolean;
165
237
  // How an isolated conversation's worktree is enforced. "namespace" = the worktree IS /work inside the turn's
166
238
  // mount namespace (with the tool-input rewrite as the fallback when the container can't build one); "cwd" =
167
239
  // the turn is merely cwd'd into the worktree, so an absolute /work path still reaches the shared checkout —
@@ -174,6 +246,28 @@ export interface AgentCapabilities {
174
246
  // Fails with the coded frames the daemon's auto-resume keys off (rate_limit, provider-outage), so a turn the
175
247
  // provider killed is re-run once the breaker says the provider is back (turn-resume.ts).
176
248
  readonly recovery: boolean;
249
+ /* HOW MUCH OF ITS STANDING INSTRUCTIONS THIS RUNTIME WILL TAKE FROM US — the axis behind the sandbox's
250
+ * system-prompt setting (SandboxSettings.systemPromptMode) and the persona's own override.
251
+ *
252
+ * It exists because that setting was silently a Claude Code setting. The composer offers Codex, Grok and
253
+ * Gemini on their own runtimes, and a turn on any of them ignored the prompt the owner had written without
254
+ * saying so anywhere — the one failure mode a settings page cannot recover from, because nothing on screen
255
+ * is wrong. Naming it here means every surface reads the same answer and the daemon composes to it
256
+ * (agent/system-prompt.ts), rather than each learning the exception separately.
257
+ *
258
+ * "replace" — the whole base prompt can be swapped for the owner's text, and extra guidance appended on
259
+ * top of whichever base is in force. The Claude Code loop (SDK `systemPrompt`) and native
260
+ * Codex (`model_instructions_file` replaces its base; `developer_instructions` adds a
261
+ * developer message — both verified on the wire against codex-cli 0.147).
262
+ * "append" — extra system text only; the runtime's own base prompt stands. OpenCode takes one per
263
+ * message (`system` on the prompt body), and there is no seam for replacing its base.
264
+ * "none" — no system seam at all. What must still reach the model (the persona note) rides the user
265
+ * message instead, which is the door the delegation note already uses.
266
+ *
267
+ * The BASE CHOICE — Intentic's prompt or Claude Code's — is a "replace" runtime's question and, of those,
268
+ * only the Claude Code loop's: Codex's own base describes Codex's own tools, so swapping it for a prompt
269
+ * written about another harness is the owner's deliberate act (their custom text), never ours. */
270
+ readonly instructions: "replace" | "append" | "none";
177
271
  }
178
272
 
179
273
  // The Claude Code Agent SDK loop — the ceiling every other runtime is measured against, and the only one that
@@ -185,41 +279,78 @@ const CLAUDE_CODE: AgentCapabilities = {
185
279
  permissions: "modes",
186
280
  questions: true,
187
281
  mcp: "full",
282
+ // The one loop with a seam the daemon can put its own backend through — so it hosts the JS backend beside
283
+ // its Bash. Every other runtime below hosts only its own shell.
284
+ execution: ["shell", "js"],
188
285
  effort: true,
286
+ fastMode: true,
189
287
  isolation: "namespace",
190
288
  commands: true,
191
289
  terminals: true,
192
290
  recovery: true,
291
+ instructions: "replace",
193
292
  };
194
293
 
195
- // Codex's exec surface: item-level events, no approval channel, no MCP seam through the SDK constructor we use.
196
- // Reasoning effort IS forwarded (modelReasoningEffort). `codex app-server` is the upgrade path for the first two.
294
+ // Codex app-server: item-level events plus richer request/MCP channels. This client deliberately declines
295
+ // server-initiated approvals and has not connected questions or MCP to Intentic's policy seams, so only the
296
+ // item stream, reasoning effort and the instruction seam are claimed here.
197
297
  const CODEX: AgentCapabilities = {
198
298
  runtime: "codex",
199
299
  steering: false,
200
300
  permissions: "plan",
201
301
  questions: false,
202
302
  mcp: "none",
303
+ execution: ["shell"],
203
304
  effort: true,
305
+ fastMode: false,
204
306
  isolation: "cwd",
205
307
  commands: false,
206
308
  terminals: false,
207
309
  recovery: false,
310
+ /* Both halves, through the per-thread `config` block the adapter already sends: `model_instructions_file`
311
+ * takes the place of Codex's own base prompt, `developer_instructions` arrives as an extra developer
312
+ * message ahead of its skills and team blocks. Verified against codex-cli 0.147 by reading what actually
313
+ * reached the wire — the keys are undocumented, and a strings dump proves only that they parse. */
314
+ instructions: "replace",
208
315
  };
209
316
 
210
- // OpenCode (the Grok runtime): its own agentic loop, its own tools, allow-all permissions. It takes a model id
211
- // and a prompt — no effort scale, no tools of ours, no command list.
317
+ // OpenCode (the Grok runtime): its own agentic loop, its own tools, allow-all permissions. It takes a model id,
318
+ // a prompt and one system message of ours — no effort scale, no tools of ours, no command list.
212
319
  const OPENCODE: AgentCapabilities = {
213
320
  runtime: "opencode",
214
321
  steering: false,
215
322
  permissions: "plan",
216
323
  questions: false,
217
324
  mcp: "none",
325
+ execution: ["shell"],
218
326
  effort: false,
327
+ fastMode: false,
219
328
  isolation: "cwd",
220
329
  commands: false,
221
330
  terminals: false,
222
331
  recovery: false,
332
+ // `system` on the prompt body, per message. It ADDS to OpenCode's own prompt — there is no seam for
333
+ // replacing that — so a custom prompt lands here as extra instructions, and the settings page says so
334
+ // rather than letting "replaces everything" quietly mean something else on two providers.
335
+ instructions: "append",
336
+ };
337
+
338
+ /* The same OpenCode loop, serving Gemini instead of xAI — identical abilities, which is the point of giving it
339
+ * its own row rather than its own record shape.
340
+ *
341
+ * It exists because the alternative was Gemini's ONLY route being the Claude Code loop, and that loop announces
342
+ * itself to whatever it is pointed at: the CLI prepends its own "You are a Claude agent, built on Anthropic's
343
+ * Claude Agent SDK." to every request, baked into the binary with no option to suppress it. Google's Antigravity
344
+ * channel matches that exact sentence and refuses the request — reported as a quota error, which sent the
345
+ * translator walking all 31 connected accounts looking for one with room, ~60s per attempt, none of which could
346
+ * ever have answered. Under this runtime the request carries OpenCode's own prompt, so the turn is simply not
347
+ * Claude Code traffic and the block has nothing to match.
348
+ *
349
+ * The credential is unchanged: both harnesses reach Google through the translator and the same auth files. Only
350
+ * the loop around the model differs. */
351
+ const OPENCODE_GEMINI: AgentCapabilities = {
352
+ ...OPENCODE,
353
+ runtime: "opencode-gemini",
223
354
  };
224
355
 
225
356
  // Any agent speaking the Agent Client Protocol: a documented floor rather than the native ceiling. It publishes
@@ -231,17 +362,53 @@ const ACP: AgentCapabilities = {
231
362
  permissions: "plan",
232
363
  questions: false,
233
364
  mcp: "http",
365
+ execution: ["shell"],
234
366
  effort: false,
367
+ fastMode: false,
235
368
  isolation: "cwd",
236
369
  commands: true,
237
370
  terminals: true,
238
371
  recovery: false,
372
+ // ACP's `session/new` and `session/prompt` carry no system field: the agent owns its own instructions the
373
+ // same way it owns its model and its permission posture. The persona note takes the user message instead.
374
+ instructions: "none",
375
+ };
376
+
377
+ /* THE PI CAPABILITY ID IS RESERVED, the same way the five native ids are: an `agent`-kind capability installed
378
+ * under it is served over Pi's own RPC protocol rather than ACP — Pi closed ACP support deliberately (its RPC
379
+ * mode is the embedding surface), and the two want different records. A bare id rather than a namespace like
380
+ * `endpoint/`, because there is exactly one Pi runtime to name; capabilitiesOf still answers from the id alone,
381
+ * which is what keeps it a pure function of (provider, harness). */
382
+ export const PI_PROVIDER = "pi";
383
+
384
+ // Pi driven over its RPC mode (`pi --mode rpc`, strict-LF JSONL over stdio): above the ACP floor and below the
385
+ // Claude Code ceiling. Its `steer` command is real mid-turn injection; `set_thinking_level` takes the effort
386
+ // tiers; `get_commands` publishes its extension/skill commands. It has no MCP seam (Pi's own extensions are its
387
+ // tool surface), no approval channel (plan is the shared two-phase emulation), and runs bash in-process — no
388
+ // tmux session for the terminal panel to attach to.
389
+ const PI: AgentCapabilities = {
390
+ runtime: "pi",
391
+ steering: true,
392
+ permissions: "plan",
393
+ questions: false,
394
+ mcp: "none",
395
+ execution: ["shell"],
396
+ effort: true,
397
+ fastMode: false,
398
+ isolation: "cwd",
399
+ commands: true,
400
+ terminals: false,
401
+ recovery: false,
402
+ // Pi's RPC opens a session with a prompt and steers it; nothing in that protocol sets standing
403
+ // instructions, so like ACP it hears the persona note through the user message.
404
+ instructions: "none",
239
405
  };
240
406
 
241
407
  // The pair → its record. An `endpoint/<id>` provider is a model API the user configured, driven BY the Claude
242
408
  // Code loop on either harness — so it gets that loop's full ceiling, which is the entire point of routing a
243
- // model through it rather than adopting a second runtime. Any other id that names no native provider is an
244
- // installed `agent`-kind capability, served over ACP.
409
+ // model through it rather than adopting a second runtime. The reserved `pi` id is the Pi coding agent on its
410
+ // own RPC runtime (harness doesn't apply — Pi is its own loop, like ACP). Any other id that names no native
411
+ // provider is an installed `agent`-kind capability, served over ACP.
245
412
  export const capabilitiesOf = (provider: AgentProvider, harness: AgentHarness): AgentCapabilities => {
246
413
  if (provider === "codex") {
247
414
  return harness === "claude-code" ? CLAUDE_CODE : CODEX;
@@ -249,9 +416,24 @@ export const capabilitiesOf = (provider: AgentProvider, harness: AgentHarness):
249
416
  if (provider === "grok") {
250
417
  return harness === "claude-code" ? CLAUDE_CODE : OPENCODE;
251
418
  }
419
+ /* GEMINI IGNORES THE HARNESS, and it is the only routed provider that does. The Claude Code loop announces
420
+ * itself in every request it sends and Google refuses on that announcement (see OPENCODE_GEMINI), so
421
+ * "Gemini under Claude Code" was never a slower or poorer option — it was one that could not complete a
422
+ * single turn, on any of the connected accounts, ever.
423
+ *
424
+ * Answering OPENCODE_GEMINI whatever the caller asked for is what makes that structural rather than a rule
425
+ * each surface has to remember. Everything downstream reads the runtime off this record — the adapter that
426
+ * serves a turn, the transcript store, the quick helper's choice of loop — so there is exactly one place
427
+ * where Gemini's loop is decided, and no way left to route Claude Code traffic at Google by asking for it. */
428
+ if (provider === "gemini") {
429
+ return OPENCODE_GEMINI;
430
+ }
252
431
  if (isEndpointProvider(provider)) {
253
432
  return CLAUDE_CODE;
254
433
  }
434
+ if (provider === PI_PROVIDER) {
435
+ return PI;
436
+ }
255
437
  return (NATIVE_PROVIDERS as readonly string[]).includes(provider) ? CLAUDE_CODE : ACP;
256
438
  };
257
439
 
@@ -265,18 +447,30 @@ export const modesFor = (capabilities: AgentCapabilities): readonly PermissionMo
265
447
  export const clampMode = (mode: PermissionMode, capabilities: AgentCapabilities): PermissionMode =>
266
448
  modesFor(capabilities).includes(mode) ? mode : "bypassPermissions";
267
449
 
268
- // What this pair does NOT do, phrased for the person about to send a message to it — the honest half of the
269
- // picker, and the reason the record carries axes the daemon itself never branches on. Empty ⇒ the full ceiling.
450
+ /* What this pair does NOT do, phrased for the person about to send a message to it — the honest half of the
451
+ * picker, and the reason the record carries axes the daemon itself never branches on. Empty ⇒ the full ceiling.
452
+ *
453
+ * `fastMode` is deliberately NOT disclosed here, and it is the one axis that can't be: every other axis is fully
454
+ * determined by the record, while fast mode also depends on the route and the model. The record says true for
455
+ * every provider the Claude Code loop serves — including the ones routed through the translator, which can never
456
+ * go fast — so a sentence derived from it would stay silent for exactly the turns that most need to hear it.
457
+ * fastAllowed answers the real question, and the `fast_mode` frame reports what the turn actually got. */
270
458
  export const limitationsOf = (capabilities: AgentCapabilities): string[] => [
271
459
  ...(capabilities.permissions === "plan" ? ["no per-tool approvals"] : []),
272
460
  ...(capabilities.questions ? [] : ["no clarifying questions"]),
273
461
  ...(capabilities.steering ? [] : ["no mid-turn steering"]),
274
462
  ...(capabilities.mcp === "none" ? ["no MCP tools or plugins"] : capabilities.mcp === "http" ? ["MCP tools only — no plugins or browser"] : []),
463
+ ...(capabilities.execution.includes("js") ? [] : ["no code runs — its shell is the one way to execute"]),
275
464
  ...(capabilities.effort ? [] : ["no effort control"]),
276
465
  ...(capabilities.commands ? [] : ["no slash commands"]),
277
466
  ...(capabilities.terminals ? [] : ["no terminal panel"]),
278
467
  ...(capabilities.isolation === "namespace" ? [] : ["worktree by working directory only"]),
279
468
  ...(capabilities.recovery ? [] : ["no auto-resume after an outage"]),
469
+ /* The two weaker answers on the instruction axis, and only those: "replace" is the ceiling this list
470
+ * measures against, so it has nothing to disclose. Both phrasings name the OWNER'S prompt rather than the
471
+ * mechanism, because that is the thing they wrote and the thing that will or will not be in force. */
472
+ ...(capabilities.instructions === "append" ? ["your system prompt is added to theirs, not replacing it"] : []),
473
+ ...(capabilities.instructions === "none" ? ["your system prompt isn't applied"] : []),
280
474
  ];
281
475
 
282
476
  // Claude's compile-time model floor, shared by the daemon's catalog (claude-models.ts — its last rung, reached
@@ -293,7 +487,7 @@ export const CLAUDE_SEED_MODELS: readonly Model[] = [
293
487
  ];
294
488
 
295
489
  // The STATIC floor of the model catalog, harness-independent: every provider's real list is the daemon's live
296
- // catalog (/claude/models · /codex/models · /grok/models — discovery with a persisted/seed floor, never empty),
490
+ // catalog (/providers/{provider}/models — discovery with a persisted/seed floor, never empty),
297
491
  // which consumers layer on top. Codex/grok are empty here (nothing sensible to offer before the live load — and
298
492
  // under the Claude Code harness they route through the translator, which serves the SAME subscription model ids
299
493
  // as the native catalog, so the harness no longer changes the list).
@@ -329,3 +523,21 @@ export const effortAllowed = (effort: string, provider: AgentProvider, thinking:
329
523
  * 400 by spending the user's money. */
330
524
  export const sendableEffort = (effort: string | undefined, thinking: boolean | undefined): string | undefined =>
331
525
  effort === "max" && thinking !== true ? "high" : effort;
526
+
527
+ /* WHETHER FAST SPEED CAN BE OFFERED for a provider/harness/model triple — the picker-side filter, the same
528
+ * shape and the same reason as effortAllowed: the composer must not show a control that does nothing.
529
+ *
530
+ * Three conditions, each answering a different question, and all three are load-bearing:
531
+ *
532
+ * - the RUNTIME has to know how to ask (capabilities.fastMode). Only the Claude Code loop does.
533
+ * - the ROUTE has to be first-party. Every non-Claude provider the Claude Code loop serves is served through
534
+ * the sandbox's translator, and the harness refuses fast mode on a non-Anthropic endpoint ("not_first_party")
535
+ * — so a `grok` turn on the claude-code harness reads true on the capability and still cannot go fast.
536
+ * - the MODEL has to publish it, which is the `fast` badge Anthropic's own catalog reports per model
537
+ * (claude-models.ts maps supportsFastMode onto it). Curating a list of ids here instead is what this repo
538
+ * deliberately does not do — a model that gains or loses fast mode moves the badge, and this follows.
539
+ *
540
+ * `badges` absent ⇒ false. That is the honest reading: a catalog row that published no capabilities said
541
+ * nothing about fast mode, and the seed floor a picker shows before its first live load is exactly that row. */
542
+ export const fastAllowed = (capabilities: AgentCapabilities, provider: AgentProvider, badges: readonly ModelBadge[] | undefined): boolean =>
543
+ capabilities.fastMode && provider === "claude" && (badges ?? []).includes("fast");
@@ -0,0 +1,60 @@
1
+ import { expect, test } from "vitest";
2
+ import { resolveAgentRunModels } from "./agent-run-model.js";
3
+ import type { QuickModelSource } from "./quick-model.js";
4
+
5
+ /* Which model a run somebody's BUTTON started opens on. The rule answers the same two surfaces its quick-model
6
+ * sibling does — the daemon walks it, the settings row names it — so what these pin is the pair of properties
7
+ * that separate the two: an account this sandbox cannot reach never sits at the head of the chain, and an empty
8
+ * answer stays empty rather than being filled in with a tier nobody chose. */
9
+
10
+ const CLAUDE: QuickModelSource = { provider: `claude`, ready: true, models: [`claude-opus-5`, `claude-sonnet-5`] };
11
+ const CODEX: QuickModelSource = { provider: `codex`, ready: true, models: [`gpt-5.6`] };
12
+ const GOOGLE: QuickModelSource = { provider: `gemini`, ready: true, models: [`gemini-3-pro`] };
13
+
14
+ const offline = (source: QuickModelSource): QuickModelSource => ({ ...source, ready: false });
15
+
16
+ test("keeps the user's own order — this list is read, never ranked", () => {
17
+ // The opposite of the quick chain, which sorts by tier and cost. Here the order IS the setting: someone who
18
+ // put Opus above GPT wants Opus first, and a resolver that knew better would spend the wrong account.
19
+ expect(resolveAgentRunModels([CLAUDE, CODEX], [`codex:gpt-5.6`, `claude:claude-opus-5`])).toEqual([
20
+ { provider: `codex`, model: `gpt-5.6` },
21
+ { provider: `claude`, model: `claude-opus-5` },
22
+ ]);
23
+ });
24
+
25
+ test("steps over a provider this sandbox has no credential for", () => {
26
+ // The whole reason the setting is a list. With Claude disconnected the head would otherwise be an account
27
+ // that fails every Fix with agent, while a perfectly good Codex sits underneath it.
28
+ expect(resolveAgentRunModels([offline(CLAUDE), CODEX], [`claude:claude-opus-5`, `codex:gpt-5.6`])).toEqual([
29
+ { provider: `codex`, model: `gpt-5.6` },
30
+ ]);
31
+ });
32
+
33
+ test("resolves to nothing when no pin is reachable — it does NOT fall back to whatever is connected", () => {
34
+ // The deliberate difference from resolveQuickModels, which lands on its Auto ladder here. An agent run is
35
+ // billed in whole sessions, so an unreachable list hands the choice back to the caller's floor (the user's
36
+ // own composer pick) rather than spending an account they never pointed at.
37
+ expect(resolveAgentRunModels([offline(CLAUDE), GOOGLE], [`claude:claude-opus-5`])).toEqual([]);
38
+ });
39
+
40
+ test("an empty list resolves to nothing even with accounts connected", () => {
41
+ expect(resolveAgentRunModels([CLAUDE, CODEX, GOOGLE], [])).toEqual([]);
42
+ });
43
+
44
+ test("drops a duplicate rather than spending two attempts proving one account is out", () => {
45
+ expect(resolveAgentRunModels([CLAUDE], [`claude:claude-opus-5`, `claude:claude-opus-5`])).toEqual([
46
+ { provider: `claude`, model: `claude-opus-5` },
47
+ ]);
48
+ });
49
+
50
+ test("drops a malformed key instead of sending it to a provider", () => {
51
+ expect(resolveAgentRunModels([CLAUDE], [`claude-opus-5`, `claude:`, `:claude-opus-5`, `claude:claude-sonnet-5`])).toEqual([
52
+ { provider: `claude`, model: `claude-sonnet-5` },
53
+ ]);
54
+ });
55
+
56
+ test("carries a model id the static catalog has never heard of", () => {
57
+ // The picker offers a custom-id escape hatch, so a pin can name a model released after this build. Second-
58
+ // guessing it here would quietly run something other than what the settings row says.
59
+ expect(resolveAgentRunModels([CLAUDE], [`claude:claude-opus-9-preview`])).toEqual([{ provider: `claude`, model: `claude-opus-9-preview` }]);
60
+ });
@@ -0,0 +1,51 @@
1
+ import { parsePinned, type QuickModelChoice, quickModelKey, type QuickModelSource } from "./quick-model.js";
2
+
3
+ /* WHAT A SURFACE-STARTED AGENT RUN OPENS ON — the resolver for `agentRunModels`, sibling to resolveQuickModels
4
+ * and deliberately not the same function.
5
+ *
6
+ * BOTH ARE ORDERED LISTS, FOR THE SAME REASON. One connected account whose allowance went on the chat this
7
+ * morning is enough to take every one of these down: the user presses Fix with agent on a red pipeline, an
8
+ * isolated session opens, and it dies on a credential error they cannot see from the row. Written in order, the
9
+ * next entry catches it.
10
+ *
11
+ * THEY DIFFER ON WHAT AN EMPTY LIST MEANS, and that difference is the whole reason this is its own file rather
12
+ * than a flag on the other one. A quick helper exists to stay OFF the frontier tier, so "work it out from what
13
+ * is connected" is a good answer and quickModel's empty list resolves to a derived Auto ladder. An agent run is
14
+ * a full session with a worktree, billed whole: nothing here can judge whether a job is worth the frontier tier,
15
+ * so an empty list resolves to NOTHING and the caller falls back to the model the user picked for their own
16
+ * chat — a choice they made, rather than one this file guessed for them. For the same reason there is no Auto
17
+ * ladder underneath a list that has been emptied by disconnection: it would spend an account the user never
18
+ * pointed at, on the most expensive kind of run this app starts.
19
+ *
20
+ * WHAT "STEPPED OVER" MEANS HERE IS NARROWER than the quick chain's, and worth being exact about. The quick
21
+ * chain re-asks the next rung when a call comes back refused, because a one-shot that failed has cost nothing
22
+ * and can simply be run again. An agent session cannot be replayed that way — by the time a provider refuses
23
+ * mid-turn the agent may have already edited files — so this list is read ONCE, at the moment the turn is
24
+ * composed, and steps over exactly one thing: an account that is not connected. A model that accepts the turn
25
+ * and fails later is a failed run the user reads on the card, like any other. */
26
+
27
+ /* The pins that could actually be started right now, in the user's own order.
28
+ *
29
+ * `sources` is the same readiness view resolveQuickModels takes, so both settings rows agree about which
30
+ * accounts this sandbox can send to — a pin greyed as "Not connected" in one row and silently spent by the
31
+ * other would be the worst of both.
32
+ *
33
+ * A pin whose provider is gone is DROPPED rather than held: it would otherwise sit at the head of the chain
34
+ * failing every run, which is exactly what the list exists to prevent. It stays on SCREEN, greyed — the settings
35
+ * row renders the stored list, not this one — because a setting that vanished from view would look like the app
36
+ * had eaten it.
37
+ *
38
+ * Empty out means nobody has pinned anything this sandbox can reach, and the caller's floor takes over. */
39
+ export const resolveAgentRunModels = (sources: readonly QuickModelSource[], pinned: readonly string[]): readonly QuickModelChoice[] => {
40
+ const ready = new Set(sources.filter((source) => source.ready).map((source) => source.provider));
41
+ const requested = pinned.flatMap((key) => {
42
+ // Verbatim, unvalidated against the catalog — the same call resolveQuickModels makes and for the same
43
+ // reason: the picker offers a custom-id escape hatch for a model the static catalog has not caught up
44
+ // with, and second-guessing the id here would run a different model than the settings row names.
45
+ const choice = parsePinned(key);
46
+ return choice === undefined || !ready.has(choice.provider) ? [] : [choice];
47
+ });
48
+ // The same model twice would spend two attempts proving the same account is out. Hand-edited list, so this
49
+ // is a real state rather than a defensive branch.
50
+ return [...new Map(requested.map((choice) => [quickModelKey(choice), choice])).values()];
51
+ };
@@ -0,0 +1,10 @@
1
+ /* HOW A CLI CAPABILITY'S ENV VARS ARE NAMED — the one rule both ends of that wire have to apply.
2
+ *
3
+ * The daemon writes the agent's shell env by suffixing every var a cli connector declares with the instance id
4
+ * (cli-env.ts), so two connections of the same provider coexist on one flat environment. A connector's own
5
+ * TOOL then has to read them back, and an extension may not import daemon internals — so without this the
6
+ * rule would be spelled twice, and a change to it would silently split the writer from the reader. */
7
+
8
+ // `analytics` → `POSTGRES_URL_ANALYTICS`; the default-named `github` → `GITHUB_TOKEN_GITHUB`.
9
+ // ponytail: ids differing only by case or `-`/`_` (my-db vs my_db) map to the same suffix — last wins.
10
+ export const envSuffix = (id: string): string => id.toUpperCase().replaceAll("-", "_");