@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,11 +1,16 @@
1
1
  import { expect, test } from "vitest";
2
2
  import { resolveAgentRunModels } from "./agent-run-model.js";
3
3
  import type { QuickModelSource } from "./quick-model.js";
4
+ import type { AgentRunPin } from "./schemas/agent.js";
4
5
 
5
6
  /* Which model a run somebody's BUTTON started opens on. The rule answers the same two surfaces its quick-model
6
7
  * sibling does: the daemon walks it, the settings row names it, so what these pin is the pair of properties
7
8
  * 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
+ * answer stays empty rather than being filled in with a tier nobody chose.
10
+ *
11
+ * And one property neither of those covers, new with the pins being objects: an entry's own knobs are the
12
+ * entry's, so what survives the walk is the WHOLE pin. A resolver that handed back the pair inside it would run
13
+ * the fallback at the head's effort, which is a tier that appears nowhere on the user's screen. */
9
14
 
10
15
  const CLAUDE: QuickModelSource = { provider: `claude`, ready: true, models: [`claude-opus-5`, `claude-sonnet-5`] };
11
16
  const CODEX: QuickModelSource = { provider: `codex`, ready: true, models: [`gpt-5.6`] };
@@ -13,10 +18,12 @@ const GOOGLE: QuickModelSource = { provider: `gemini`, ready: true, models: [`ge
13
18
 
14
19
  const offline = (source: QuickModelSource): QuickModelSource => ({ ...source, ready: false });
15
20
 
21
+ const pin = (provider: string, model: string, knobs: Partial<AgentRunPin> = {}): AgentRunPin => ({ provider, model, ...knobs });
22
+
16
23
  test("keeps the user's own order, this list is read, never ranked", () => {
17
24
  // The opposite of the quick chain, which sorts by tier and cost. Here the order IS the setting: someone who
18
25
  // 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([
26
+ expect(resolveAgentRunModels([CLAUDE, CODEX], [pin(`codex`, `gpt-5.6`), pin(`claude`, `claude-opus-5`)])).toEqual([
20
27
  { provider: `codex`, model: `gpt-5.6` },
21
28
  { provider: `claude`, model: `claude-opus-5` },
22
29
  ]);
@@ -25,36 +32,45 @@ test("keeps the user's own order, this list is read, never ranked", () => {
25
32
  test("steps over a provider this sandbox has no credential for", () => {
26
33
  // The whole reason the setting is a list. With Claude disconnected the head would otherwise be an account
27
34
  // 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([
35
+ expect(resolveAgentRunModels([offline(CLAUDE), CODEX], [pin(`claude`, `claude-opus-5`), pin(`codex`, `gpt-5.6`)])).toEqual([
29
36
  { provider: `codex`, model: `gpt-5.6` },
30
37
  ]);
31
38
  });
32
39
 
40
+ test("each surviving entry keeps its own knobs, not the head's", () => {
41
+ // The effort used to be one setting beside the list, so a fallback ran at whatever the head was set to. It
42
+ // is now a property of the entry that actually answers, which is the only place it was ever true.
43
+ expect(
44
+ resolveAgentRunModels(
45
+ [offline(CODEX), CLAUDE],
46
+ [pin(`codex`, `gpt-5.6`, { effort: `low` }), pin(`claude`, `claude-opus-5`, { effort: `max`, thinking: true })],
47
+ ),
48
+ ).toEqual([{ provider: `claude`, model: `claude-opus-5`, effort: `max`, thinking: true }]);
49
+ });
50
+
33
51
  test("resolves to nothing when no pin is reachable: it does NOT fall back to whatever is connected", () => {
34
52
  // The deliberate difference from resolveQuickModels, which lands on its Auto ladder here. An agent run is
35
53
  // billed in whole sessions, so an unreachable list hands the choice back to the caller's floor (the user's
36
54
  // own composer pick) rather than spending an account they never pointed at.
37
- expect(resolveAgentRunModels([offline(CLAUDE), GOOGLE], [`claude:claude-opus-5`])).toEqual([]);
55
+ expect(resolveAgentRunModels([offline(CLAUDE), GOOGLE], [pin(`claude`, `claude-opus-5`)])).toEqual([]);
38
56
  });
39
57
 
40
58
  test("an empty list resolves to nothing even with accounts connected", () => {
41
59
  expect(resolveAgentRunModels([CLAUDE, CODEX, GOOGLE], [])).toEqual([]);
42
60
  });
43
61
 
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
- ]);
62
+ test("drops a duplicate rather than spending two attempts proving one account is out, and the first one's knobs are the ones kept", () => {
63
+ // Two entries can now name one model and disagree about how hard it thinks, which is what reordering a list
64
+ // by hand produces. The one the user reads first is the one they meant.
65
+ expect(
66
+ resolveAgentRunModels([CLAUDE], [pin(`claude`, `claude-opus-5`, { effort: `max` }), pin(`claude`, `claude-opus-5`, { effort: `low` })]),
67
+ ).toEqual([{ provider: `claude`, model: `claude-opus-5`, effort: `max` }]);
54
68
  });
55
69
 
56
70
  test("carries a model id the static catalog has never heard of", () => {
57
71
  // The picker offers a custom-id escape hatch, so a pin can name a model released after this build. Second-
58
72
  // 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` }]);
73
+ expect(resolveAgentRunModels([CLAUDE], [pin(`claude`, `claude-opus-9-preview`)])).toEqual([
74
+ { provider: `claude`, model: `claude-opus-9-preview` },
75
+ ]);
60
76
  });
@@ -1,4 +1,5 @@
1
- import { parsePinned, type QuickModelChoice, quickModelKey, type QuickModelSource } from "./quick-model.js";
1
+ import { quickModelKey, type QuickModelSource } from "./quick-model.js";
2
+ import type { AgentRunPin } from "./schemas/agent.js";
2
3
 
3
4
  /* WHAT A SURFACE-STARTED AGENT RUN OPENS ON, the resolver for `agentRunModels`, sibling to resolveQuickModels
4
5
  * and deliberately not the same function.
@@ -35,17 +36,30 @@ import { parsePinned, type QuickModelChoice, quickModelKey, type QuickModelSourc
35
36
  * row renders the stored list, not this one, because a setting that vanished from view would look like the app
36
37
  * had eaten it.
37
38
  *
39
+ * THE WHOLE PIN SURVIVES, not the pair inside it: the entry's own effort, harness and cost knobs are what the
40
+ * turn is composed from (turn-resume.ts), so a resolver that handed back a bare (provider, model) would silently
41
+ * run the head of the list at the provider's defaults. Nothing here reads or judges those fields, which is the
42
+ * point of carrying them whole.
43
+ *
38
44
  * 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[] => {
45
+ export const resolveAgentRunModels = (sources: readonly QuickModelSource[], pinned: readonly AgentRunPin[]): readonly AgentRunPin[] => {
40
46
  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()];
47
+ // Taken verbatim, unvalidated against the catalog, the same reading resolveQuickModels gives its keys and
48
+ // for the same reason: the picker offers a custom-id escape hatch for a model the static catalog has not
49
+ // caught up with, and second-guessing the id here would run a different model than the settings row names.
50
+ const requested = pinned.filter((pin) => ready.has(pin.provider));
51
+ /* The same model twice would spend two attempts proving the same account is out. Hand-edited list, so this
52
+ * is a real state rather than a defensive branch.
53
+ *
54
+ * THE FIRST OF A PAIR WINS, WHOLE. Two entries can now name one model and differ in their knobs (the same
55
+ * Sonnet at Max and again at Low, written while reordering the list), and the one the user reads first is
56
+ * the one they meant; keeping the earlier position with the later entry's effort would run a tier that
57
+ * appears nowhere the pin does. */
58
+ const chain: AgentRunPin[] = [];
59
+ for (const pin of requested) {
60
+ if (!chain.some((held) => quickModelKey(held) === quickModelKey(pin))) {
61
+ chain.push(pin);
62
+ }
63
+ }
64
+ return chain;
51
65
  };
@@ -0,0 +1,419 @@
1
+ /* WHAT EACH AGENTIC LOOP CAN DO, one record per runtime, and nothing about WHICH PROVIDER runs on it.
2
+ *
3
+ * Seven runtimes serve turns behind one seam (AgentRequest in, AgentEvent frames out): the Claude Code Agent SDK
4
+ * loop, Codex app-server, OpenCode (twice, once per channel), Cursor's own loop run in-process, any ACP agent,
5
+ * and Pi's RPC surface. They do NOT do the same things, and for a long time the only thing that said so was a
6
+ * comment inside each adapter, "Ignores the Claude-only request fields", which no surface above it could read.
7
+ * So the composer offered "Ask before each file edit" on a runtime whose every tool call is pre-approved, and
8
+ * offered a reasoning-effort scale to a runtime that drops the field.
9
+ *
10
+ * A capability is listed here only if something READS it: the daemon gates a seam on it, the composer hides or
11
+ * clamps a control by it, or `limitationsOf` tells the user about it. That is the whole point, an ability the
12
+ * matrix claims and nothing consults is how the drift started.
13
+ *
14
+ * THIS FILE IS THE BOTTOM OF THE PROVIDER GRAPH and imports nothing from the rest of the contract, which is
15
+ * what lets provider-specs.ts name these records while schemas/agent.ts reads its provider vocabulary back out
16
+ * of that table. A runtime knows nothing about providers; a provider names two runtimes. Keeping the arrow
17
+ * pointing one way is the whole reason the records live apart from the catalog that reads them.
18
+ *
19
+ * Adding a provider is a row in provider-specs.ts pointing at two of these, not a hunt for literals;
20
+ * agent-catalog.test.ts walks PROVIDERS × HARNESSES and demands one, so a pair can never be silently absent. */
21
+
22
+ // An execution backend: one way a turn runs work of its own, named for the AgentCapabilities.execution axis
23
+ // and for the persona switch that grants it. Adding a language is a member here and a backend in the daemon's
24
+ // execution/ module, never a new one-off tool wired where nothing else can see it.
25
+ export type ExecutionBackend = "shell" | "js";
26
+
27
+ export interface AgentCapabilities {
28
+ // Which agentic loop actually serves the turn, the question "is the harness `claude-code`" only looks like.
29
+ // Claude is always its own Claude Code loop and Kimi has no native runtime, so both run it whatever harness
30
+ // the client sent; codex/grok/gemini each have a native runtime to switch away from. Names the session store
31
+ // a finished conversation's transcript is backfilled from, too.
32
+ //
33
+ // `opencode-gemini` is the OpenCode loop pointed at Gemini rather than at xAI, and it is a SEPARATE runtime
34
+ // id from `opencode` on purpose: adapter health is keyed by this field (adapter-health.ts), so sharing one
35
+ // would make Grok's xAI credential decide whether the picker greys out Gemini, and the reverse.
36
+ readonly runtime: "claude-code" | "codex" | "opencode" | "opencode-gemini" | "acp" | "pi" | "cursor";
37
+ // Mid-turn injection (the SteeringQueue behind /agent/steer). Needs the SDK's streaming-input mode.
38
+ readonly steering: boolean;
39
+ // How much of the permission-mode axis the runtime honours. "modes" = every PermissionMode, with per-tool
40
+ // permission cards and `mode` frames when the agent moves itself; "plan" = propose-then-approve or run, and
41
+ // nothing in between, the container is the isolation boundary and every tool call is pre-approved.
42
+ readonly permissions: "modes" | "plan";
43
+ // Can stop mid-turn and ask the user a multiple-choice question (`question` frames).
44
+ readonly questions: boolean;
45
+ /* Which of the turn's tools reach the agent. "full" = http MCP tools + in-process SDK servers + plugin
46
+ * checkouts + the browser servers; "tools" = all of that EXCEPT plugin checkouts; "browser" = the
47
+ * process-backed browser servers alone; "http" = the http MCP tools alone, and only if the agent advertises
48
+ * http MCP support; "none" = the runtime has no seam for them at all. Keeping the partial answers distinct
49
+ * matters: a runtime that can drive a connected account must not be described as tool-less, and one that
50
+ * cannot host daemon-side SDK servers must not claim full.
51
+ *
52
+ * "tools" exists for the Cursor runtime and would have been a lie either way without it. Cursor's SDK takes
53
+ * stdio AND http/sse MCP servers, and its `customTools` run host callbacks in this process, which is the
54
+ * seam an in-process SDK server needs, so calling it "browser" would understate it by three whole
55
+ * categories. What it genuinely cannot host is a Claude Code PLUGIN checkout: that is a directory layout the
56
+ * Agent SDK loads, not a protocol, and no other runtime will ever read one. So the gap is real, permanent
57
+ * and worth its own word rather than being rounded to "full". */
58
+ readonly mcp: "full" | "tools" | "browser" | "http" | "none";
59
+ /* WHICH EXECUTION BACKENDS THE RUNTIME HOSTS, the ways a turn RUNS things, as opposed to the tools it is
60
+ * handed. "shell" is the runtime's own command tool (Bash on the Claude Code loop, each foreign loop's
61
+ * equivalent); "js" is the sandbox's JavaScript backend (execution/ in the daemon): the model writes a
62
+ * script instead of a command line, and the daemon runs it in a permission-fenced Node subprocess.
63
+ *
64
+ * A first-class axis rather than a corollary of `mcp`, because the two answer different questions: `mcp`
65
+ * says which TOOLS reach the model's context, this says which ways of EXECUTING the daemon can stand
66
+ * behind for this runtime, with the same guard, secret and persona seams the shell gets. A runtime that
67
+ * cannot host a backend simply never shows it, and the persona switch for it (PersonaPowersSchema.code)
68
+ * then has nothing to grant there. */
69
+ readonly execution: readonly ExecutionBackend[];
70
+ // Reasoning-effort selection is forwarded to the model.
71
+ readonly effort: boolean;
72
+ /* The runtime can serve a turn at fast speed when asked (AgentTurn.fast). A statement about the LOOP, not
73
+ * about the route: the Claude Code loop knows how to ask for it, which is why every provider this record
74
+ * hands the loop to reads true here, including the ones served through the translator, whose turns the
75
+ * harness will then refuse fast mode for because a translator endpoint is not first-party. That second
76
+ * question is answered where the endpoint is decided (planHarnessTurn), because it is a fact about the
77
+ * CREDENTIAL rather than about the runtime, and this record is a pure function of (provider, harness). */
78
+ readonly fastMode: boolean;
79
+ // How an isolated conversation's worktree is enforced. "namespace" = the worktree IS /work inside the turn's
80
+ // mount namespace (with the tool-input rewrite as the fallback when the container can't build one); "cwd" =
81
+ // the turn is merely cwd'd into the worktree, so an absolute /work path still reaches the shared checkout,
82
+ // which is why those turns are told where their tree is (turn-preamble.ts).
83
+ readonly isolation: "namespace" | "cwd";
84
+ // Publishes its slash commands (`commands` frames) for the composer's `/` popover.
85
+ readonly commands: boolean;
86
+ // Runs its shell in a tmux session the terminal panel can attach to (`terminal` frames).
87
+ readonly terminals: boolean;
88
+ // Fails with the coded frames the daemon's auto-resume keys off (rate_limit, provider-outage), so a turn the
89
+ // provider killed is re-run once the breaker says the provider is back (turn-resume.ts).
90
+ readonly recovery: boolean;
91
+ /* HOW MUCH OF ITS STANDING INSTRUCTIONS THIS RUNTIME WILL TAKE FROM US, the axis behind the sandbox's
92
+ * system-prompt setting (SandboxSettings.systemPromptMode) and the persona's own override.
93
+ *
94
+ * It exists because that setting was silently a Claude Code setting. The composer offers Codex, Grok and
95
+ * Gemini on their own runtimes, and a turn on any of them ignored the prompt the owner had written without
96
+ * saying so anywhere, the one failure mode a settings page cannot recover from, because nothing on screen
97
+ * is wrong. Naming it here means every surface reads the same answer and the daemon composes to it
98
+ * (agent/system-prompt.ts), rather than each learning the exception separately.
99
+ *
100
+ * "replace", the whole base prompt can be swapped for the owner's text, and extra guidance appended on
101
+ * top of whichever base is in force. The Claude Code loop (SDK `systemPrompt`) and native
102
+ * Codex (`model_instructions_file` replaces its base; `developer_instructions` adds a
103
+ * developer message, both verified on the wire against codex-cli 0.147).
104
+ * "append" , extra system text only; the runtime's own base prompt stands. OpenCode takes one per
105
+ * message (`system` on the prompt body), and there is no seam for replacing its base.
106
+ * "none" , no system seam at all. What must still reach the model (the persona note) rides the user
107
+ * message instead, which is the door the delegation note already uses.
108
+ *
109
+ * The BASE CHOICE. Intentic's prompt or Claude Code's, is a "replace" runtime's question and, of those,
110
+ * only the Claude Code loop's: Codex's own base describes Codex's own tools, so swapping it for a prompt
111
+ * written about another harness is the owner's deliberate act (their custom text), never ours. */
112
+ readonly instructions: "replace" | "append" | "none";
113
+ /* HOW THIS RUNTIME DISCOVERS THE WORKSPACE'S LOADED SKILLS.
114
+ *
115
+ * "native", the runtime scans one of the filesystem projections itself: `.agents/skills/` for Codex,
116
+ * `.claude/skills/` for the Claude Code loop. Its own loader injects the catalogue and reads
117
+ * the matching SKILL.md on demand, so adding our own note would duplicate it.
118
+ * "prompt", the runtime has no loader the daemon can rely on. turn-plan.ts puts the same name,
119
+ * description and absolute SKILL.md path into the opening user-message preamble. This is a
120
+ * separate axis from `instructions`: Pi and ACP take no system prompt at all, while OpenCode
121
+ * and Cursor take an append, but all four still need skill discovery. */
122
+ readonly skillDiscovery: "native" | "prompt";
123
+ /* WHETHER THE OWNER'S SAFETY POLICY REACHES THIS RUNTIME (the document at .intentic/config/safety.md, read
124
+ * by the judge in agent/command-judge.ts, delivered by guard/command-gate.ts).
125
+ *
126
+ * It exists because the policy is silently a Claude Code policy without it. The gate is a PreToolUse hook,
127
+ * which is an Agent SDK seam, so an owner whose policy says "ask before force-pushing" was asked on a Claude
128
+ * turn and never on a Codex, Grok, Gemini, Pi or ACP one, with nothing on screen saying so. Same failure
129
+ * mode as the `instructions` axis above, and the same fix: name it once, let every surface read it.
130
+ *
131
+ * "hooks" , the runtime's own pre-execution hook carries the verdict and an ASK can park the call.
132
+ * The Claude Code loop, whose PreToolUse hook fires even under bypassPermissions.
133
+ * "approval" , the vendor publishes a per-call approval channel the daemon answers from the same
134
+ * policy, and an ask parks on a card because the vendor is blocked on the answer
135
+ * (Codex's `item/commandExecution/requestApproval`, ACP's `session/request_permission`).
136
+ * Weaker than "hooks" in one stated way: the vendor decides WHICH calls it asks about, so a
137
+ * command it never raises is one the policy cannot see. What it does raise is judged by
138
+ * the same judge.
139
+ * "refuse-only", the same channel, but the vendor puts a CLOCK on the wait, so a hold cannot park and
140
+ * arrives as a refusal instead. OpenCode's turn has an inactivity watchdog that reads a
141
+ * paused approval as a stalled turn; a card there would break the turn rather than gate it.
142
+ * `refuse` verdicts work fully; an `ask` stops the command and says it could not ask.
143
+ * "none" , the runtime publishes no seam before it runs a command, so no policy can apply. Pi runs
144
+ * its bash in-process with no approval channel at all.
145
+ *
146
+ * The taint bit rides this axis too: a runtime with no consult has no place to hand the judge its facts,
147
+ * which is why `conversationTainted` must read a "none" runtime as tainted rather than as clean
148
+ * (guard/turn-taint.ts). */
149
+ readonly rulebook: "hooks" | "approval" | "refuse-only" | "none";
150
+ /* WHETHER A STORED CREDENTIAL IS MASKED IN WHAT THIS RUNTIME'S MODEL READS (secrets/secret-registry.ts and
151
+ * the two seams around it).
152
+ *
153
+ * "masked" is the full round trip: every stored value is replaced by its `{{secret:name}}` reference on the
154
+ * way into the model's context, and the same reference resolves back to the value at the two exits that
155
+ * spend it (a shell command, a script). The Claude Code loop, via PostToolUse and PreToolUse.
156
+ *
157
+ * "none" is a STRUCTURAL limit, not an unfinished wire, and it is the reason this axis is honest rather
158
+ * than aspirational. On every other runtime the tool runs inside the VENDOR'S own loop: the model has read
159
+ * the result before the daemon sees any frame about it, so there is no seam left to rewrite. A PostToolUse
160
+ * hook is the only thing that can edit what a model reads, and only the Claude Code loop has one. Nothing
161
+ * about wiring more transports changes that, which is why the answer here is a disclosure and the real fix
162
+ * is to stop putting credentials where a vendor's tool can read them at all.
163
+ *
164
+ * Read by limitationsOf, and by agent/system-prompt.ts, which must not teach the reference language to a
165
+ * runtime that has no exit for it. */
166
+ readonly secrets: "masked" | "none";
167
+ }
168
+
169
+ // The Claude Code Agent SDK loop, the ceiling every other runtime is measured against, and the only one that
170
+ // owns the whole request: permission callbacks, the ask tool, plugins, hooks, and the spawn seam a mount
171
+ // namespace needs.
172
+ export const CLAUDE_CODE: AgentCapabilities = {
173
+ runtime: "claude-code",
174
+ steering: true,
175
+ permissions: "modes",
176
+ questions: true,
177
+ mcp: "full",
178
+ // The one loop with a seam the daemon can put its own backend through, so it hosts the JS backend beside
179
+ // its Bash. Every other runtime below hosts only its own shell.
180
+ execution: ["shell", "js"],
181
+ effort: true,
182
+ fastMode: true,
183
+ isolation: "namespace",
184
+ commands: true,
185
+ terminals: true,
186
+ recovery: true,
187
+ instructions: "replace",
188
+ skillDiscovery: "native",
189
+ // The only runtime with a pre-execution hook of its own, which is why it is the only one where a HOLD can
190
+ // park the call and wait for a card rather than having to refuse it.
191
+ rulebook: "hooks",
192
+ secrets: "masked",
193
+ };
194
+
195
+ /* Codex app-server: item-level events, process-backed MCP servers, and the four interactive seams its protocol
196
+ * actually publishes, `turn/steer` for mid-turn injection, the experimental `item/tool/requestUserInput` server
197
+ * request behind a question card, `skills/list` for the `/` popover (a picked command rides back as a structured
198
+ * skill input), and the same mount namespace the Claude Code loop gets, because app-server is a child process
199
+ * the adapter spawns and nsenter can put it in the turn's namespace like any other.
200
+ *
201
+ * Browser servers ride the per-thread config; daemon-side SDK servers, plugins and server-initiated APPROVALS
202
+ * stay unwired, the container is the isolation boundary, so approvals are declined by design rather than
203
+ * missing (codex-app-server.ts refuses every server request but the question one). */
204
+ export const CODEX: AgentCapabilities = {
205
+ runtime: "codex",
206
+ steering: true,
207
+ permissions: "plan",
208
+ questions: true,
209
+ mcp: "browser",
210
+ execution: ["shell"],
211
+ effort: true,
212
+ fastMode: false,
213
+ isolation: "namespace",
214
+ commands: true,
215
+ terminals: false,
216
+ recovery: false,
217
+ /* Both halves, through the per-thread `config` block the adapter already sends: `model_instructions_file`
218
+ * takes the place of Codex's own base prompt, `developer_instructions` arrives as an extra developer
219
+ * message ahead of its skills and team blocks. Verified against codex-cli 0.147 by reading what actually
220
+ * reached the wire, the keys are undocumented, and a strings dump proves only that they parse. */
221
+ instructions: "replace",
222
+ skillDiscovery: "native",
223
+ /* App-server publishes `item/commandExecution/requestApproval`, whose params carry the command text, and
224
+ * takes `accept`/`decline` back (codex-cli 0.147's own generated JSON Schema, read with
225
+ * `codex app-server generate-json-schema`). The daemon only asks Codex to raise those requests when the
226
+ * owner has written command rules, so an unconfigured workspace keeps `approvalPolicy: "never"` and pays
227
+ * nothing (codex/codex-agent.ts threadOptions). */
228
+ rulebook: "approval",
229
+ secrets: "none",
230
+ };
231
+
232
+ // OpenCode (the Grok runtime): its own agentic loop, its own tools, allow-all permissions. It takes a model id,
233
+ // a prompt and one system message of ours, no effort scale, no tools of ours, no command list.
234
+ export const OPENCODE: AgentCapabilities = {
235
+ runtime: "opencode",
236
+ steering: false,
237
+ permissions: "plan",
238
+ questions: false,
239
+ mcp: "none",
240
+ execution: ["shell"],
241
+ effort: false,
242
+ fastMode: false,
243
+ isolation: "cwd",
244
+ commands: false,
245
+ terminals: false,
246
+ recovery: false,
247
+ // `system` on the prompt body, per message. It ADDS to OpenCode's own prompt, there is no seam for
248
+ // replacing that, so a custom prompt lands here as extra instructions, and the settings page says so
249
+ // rather than letting "replaces everything" quietly mean something else on two providers.
250
+ instructions: "append",
251
+ skillDiscovery: "prompt",
252
+ /* OpenCode asks over its own permission channel (`permission.updated`, replied on
253
+ * `/session/{id}/permissions/{permissionID}`, vocabulary once/always/reject), and the daemon judges what it
254
+ * raises with the same decide fn every other runtime uses.
255
+ *
256
+ * REFUSE-ONLY because of its watchdog, not because of its protocol. A Grok/Gemini turn is aborted after two
257
+ * minutes without an event for its session (grok/grok-agent.ts GROK_INACTIVITY_MS), and a permission paused
258
+ * on a person is exactly that silence, so a parked card would turn "ask me" into a broken turn. A `deny`
259
+ * rule is enforced in full; a `hold` stops the command and tells the agent it could not be asked about. */
260
+ rulebook: "refuse-only",
261
+ secrets: "none",
262
+ };
263
+
264
+ /* The same OpenCode loop, serving Gemini instead of xAI, identical abilities, which is the point of giving it
265
+ * its own row rather than its own record shape.
266
+ *
267
+ * It exists because the alternative was Gemini's ONLY route being the Claude Code loop, and that loop announces
268
+ * itself to whatever it is pointed at: the CLI prepends its own "You are a Claude agent, built on Anthropic's
269
+ * Claude Agent SDK." to every request, baked into the binary with no option to suppress it. Google's Antigravity
270
+ * channel matches that exact sentence and refuses the request, reported as a quota error, which sent the
271
+ * translator walking all 31 connected accounts looking for one with room, ~60s per attempt, none of which could
272
+ * ever have answered. Under this runtime the request carries OpenCode's own prompt, so the turn is simply not
273
+ * Claude Code traffic and the block has nothing to match.
274
+ *
275
+ * The credential is unchanged: both harnesses reach Google through the translator and the same auth files. Only
276
+ * the loop around the model differs. */
277
+ export const OPENCODE_GEMINI: AgentCapabilities = {
278
+ ...OPENCODE,
279
+ runtime: "opencode-gemini",
280
+ };
281
+
282
+ // Any agent speaking the Agent Client Protocol: a documented floor rather than the native ceiling. It publishes
283
+ // commands, runs its terminals in the conversation's tmux session, and takes our http MCP tools when it says it
284
+ // can, but it owns its own model, effort and permission posture.
285
+ export const ACP: AgentCapabilities = {
286
+ runtime: "acp",
287
+ steering: false,
288
+ permissions: "plan",
289
+ questions: false,
290
+ mcp: "http",
291
+ execution: ["shell"],
292
+ effort: false,
293
+ fastMode: false,
294
+ isolation: "cwd",
295
+ commands: true,
296
+ terminals: true,
297
+ recovery: false,
298
+ // ACP's `session/new` and `session/prompt` carry no system field: the agent owns its own instructions the
299
+ // same way it owns its model and its permission posture. The persona note takes the user message instead.
300
+ instructions: "none",
301
+ skillDiscovery: "prompt",
302
+ /* `session/request_permission` is in the protocol floor, so every conforming agent has the channel and the
303
+ * daemon answers it from the rulebook (acp/acp-permissions.ts). The caveat the "approval" value already
304
+ * carries is at its widest here: WHICH calls an agent asks about is entirely the agent's choice, and one
305
+ * that never asks is one no rule can reach. */
306
+ rulebook: "approval",
307
+ secrets: "none",
308
+ };
309
+
310
+ // Pi driven over its RPC mode (`pi --mode rpc`, strict-LF JSONL over stdio): above the ACP floor and below the
311
+ // Claude Code ceiling. Its `steer` command is real mid-turn injection; `set_thinking_level` takes the effort
312
+ // tiers; `get_commands` publishes its extension/skill commands. It has no MCP seam (Pi's own extensions are its
313
+ // tool surface), no approval channel (plan is the shared two-phase emulation), and runs bash in-process, no
314
+ // tmux session for the terminal panel to attach to.
315
+ export const PI: AgentCapabilities = {
316
+ runtime: "pi",
317
+ steering: true,
318
+ permissions: "plan",
319
+ questions: false,
320
+ mcp: "none",
321
+ execution: ["shell"],
322
+ effort: true,
323
+ fastMode: false,
324
+ isolation: "cwd",
325
+ commands: true,
326
+ terminals: false,
327
+ recovery: false,
328
+ // Pi's RPC opens a session with a prompt and steers it; nothing in that protocol sets standing
329
+ // instructions, so like ACP it hears the persona note through the user message.
330
+ instructions: "none",
331
+ skillDiscovery: "prompt",
332
+ /* THE ONE RUNTIME WITH NO SEAM AT ALL. Pi runs bash in-process and its RPC publishes no approval request,
333
+ * so there is nothing to consult before a command runs and no rule the owner writes can apply here. Said
334
+ * out loud rather than left to be discovered: limitationsOf renders it, and the taint floor treats a "none"
335
+ * runtime as permanently tainted, because a bit nobody can act on is worse than no bit. */
336
+ rulebook: "none",
337
+ secrets: "none",
338
+ };
339
+
340
+ /* CURSOR'S OWN AGENT RUNTIME, driven through `@cursor/sdk`, the SDK Anysphere publishes, in this daemon's own
341
+ * process. The second-richest row in this file after the Claude Code loop, and the reason is the SDK rather
342
+ * than the vendor: it is an EMBEDDING surface, not a CLI wrapped in a pipe, so most of the seams the other
343
+ * foreign runtimes lack are simply function arguments here.
344
+ *
345
+ * WHY NOT THROUGH OPENCODE, which is already in this image and already serves two providers. Every Cursor
346
+ * bridge for OpenCode is a community reverse-engineering of Cursor's private agent RPC or a localhost shim
347
+ * around its CLI, and the OPENCODE record above is the weakest in this file. Routing Cursor through it would
348
+ * have capped a first-party SDK at Grok's ceiling and made the row depend on a third party's spare time.
349
+ *
350
+ * WHY THE HARNESS AXIS DOESN'T APPLY, the same way it doesn't for Gemini, and for the mirror-image reason.
351
+ * Gemini has no Claude Code route because Google refuses that traffic; Cursor has none because there is no
352
+ * translator route at all, CLIProxyAPI does not serve Cursor as a provider (asked for repeatedly upstream and
353
+ * closed as not planned), and Cursor publishes no OpenAI-compatible endpoint on a subscription. The SDK IS the
354
+ * only door, so `capabilitiesOf` answers this record whatever harness the client sent.
355
+ *
356
+ * The three axes below that read weaker than they could are deliberate, not unfinished: see the notes on each. */
357
+ export const CURSOR: AgentCapabilities = {
358
+ runtime: "cursor",
359
+ // The SDK's Run can be cancelled but not written to mid-flight: a second `send` on a busy agent is an
360
+ // AgentBusyError, not an injection. So the steering queue has nowhere to go and the composer hides it.
361
+ steering: false,
362
+ /* Cursor's OWN plan mode (`mode: "agent" | "plan"`), not this repo's two-phase emulation, which is the
363
+ * better version of the same bargain: the model is put in a read-only posture by the vendor rather than
364
+ * being asked to behave.
365
+ *
366
+ * Not "modes", and that is the honest half. The hook seam below can gate shell, MCP, file reads and file
367
+ * edits, which is most of the tool surface but not all of it, and a per-tool posture with a silent gap in
368
+ * it is worse than one that says where it stops. */
369
+ permissions: "plan",
370
+ /* TRUE BECAUSE WE SUPPLY THE TOOL, not because Cursor's own askQuestion is wired. That one is put in
371
+ * `disallowedTools`: in a headless run it has been reported to answer itself with a fabricated "Questions
372
+ * skipped by the user", which is the single worst failure shape available here, an agent acting on consent
373
+ * nobody gave. The ask tool the daemon registers through `customTools` runs in this process, parks on a
374
+ * real card, and cannot invent an answer because it is the thing that receives one. */
375
+ questions: true,
376
+ // stdio + http/sse MCP servers, plus host callbacks through `customTools` (which is where the browser stack
377
+ // and the in-process SDK servers land). Everything but a Claude Code plugin checkout, see the axis note.
378
+ mcp: "tools",
379
+ execution: ["shell"],
380
+ /* Cursor publishes effort as MODEL PARAMETERS rather than as one scale (`ModelListItem.parameters` /
381
+ * `variants` → `ModelSelection.params`), so the shared tiers are mapped onto whatever the selected model
382
+ * declares, and a model that declares none simply offers no control. True here because the axis is
383
+ * forwardable at all; which tiers exist is the live catalog's answer, not this record's. */
384
+ effort: true,
385
+ fastMode: false,
386
+ /* "cwd", and this is the one place the SDK's in-process design costs something. A namespace is built around
387
+ * a CHILD the daemon spawns (that is how the Claude Code loop and Codex app-server get theirs); Cursor's
388
+ * loop runs inside the daemon, whose own /work must stay the shared checkout, so an isolated conversation
389
+ * gets its worktree by working directory and the turn is told where its tree is (turn-preamble.ts). */
390
+ isolation: "cwd",
391
+ // Cursor's commands are files on disk (`.cursor/commands`), which the SDK loads but does not publish back,
392
+ // so there is no list to hand the `/` popover.
393
+ commands: false,
394
+ // The SDK runs its shell in-process; there is no tmux session for the terminal panel to attach to.
395
+ terminals: false,
396
+ // The SDK throws typed errors (RateLimitError and friends) rather than dissolving a refusal into prose, so
397
+ // the adapter can file the coded frames auto-resume keys off.
398
+ recovery: true,
399
+ /* "append", the OpenCode answer, reached by a completely different road. There is no system-prompt argument
400
+ * on `Agent.create`; what there is, is the `beforeSubmitPrompt` hook, whose reply carries
401
+ * `additional_context` that is folded into the request. So the owner's prompt and the persona note DO reach
402
+ * the model, on top of Cursor's own base prompt, and nothing can replace that base. */
403
+ instructions: "append",
404
+ skillDiscovery: "prompt",
405
+ /* THE FULL HOOK TIER, the only foreign runtime that reaches it. Cursor reads `.cursor/hooks.json` in its
406
+ * local runtime, and `beforeShellExecution` answers with `allow` / `deny` / `ask` plus the messages that
407
+ * explain it, with `failClosed` available so a crashed gate blocks instead of waving the command through.
408
+ *
409
+ * What earns "hooks" rather than "approval" is that a HOLD can genuinely park: the hook is a process the
410
+ * daemon wrote, so it blocks on the card and the vendor is simply waiting on a script, exactly the shape
411
+ * that makes the Claude Code loop's PreToolUse hook able to stop and ask. The vendor never decides which
412
+ * calls to raise, either, which is the caveat the "approval" tier carries and this one does not. */
413
+ rulebook: "hooks",
414
+ /* "none", and structurally so, like every other foreign runtime. Masking needs a seam that rewrites what
415
+ * the model READS after a tool ran; Cursor's `afterShellExecution` fires with the output but its reply is
416
+ * discarded upstream, and `beforeReadFile` sees the content only to allow or deny it. Both are gates, not
417
+ * filters, so there is nothing here to substitute a reference back into. */
418
+ secrets: "none",
419
+ };