@intentic/sandbox-contract 1.333.1 → 1.335.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 (238) hide show
  1. package/README.md +8 -3
  2. package/dist/contracts/accounts.contract.d.ts +11 -11
  3. package/dist/contracts/activity.contract.d.ts +1 -0
  4. package/dist/contracts/activity.contract.d.ts.map +1 -1
  5. package/dist/contracts/activity.contract.js +1 -1
  6. package/dist/contracts/activity.contract.js.map +1 -1
  7. package/dist/contracts/agent.contract.d.ts +102 -18
  8. package/dist/contracts/agent.contract.d.ts.map +1 -1
  9. package/dist/contracts/agents.contract.d.ts +163 -0
  10. package/dist/contracts/agents.contract.d.ts.map +1 -1
  11. package/dist/contracts/agents.contract.js +10 -1
  12. package/dist/contracts/agents.contract.js.map +1 -1
  13. package/dist/contracts/audit.contract.d.ts +84 -0
  14. package/dist/contracts/audit.contract.d.ts.map +1 -0
  15. package/dist/contracts/audit.contract.js +43 -0
  16. package/dist/contracts/audit.contract.js.map +1 -0
  17. package/dist/contracts/automations.contract.d.ts +15 -0
  18. package/dist/contracts/automations.contract.d.ts.map +1 -1
  19. package/dist/contracts/ci.contract.d.ts +10 -0
  20. package/dist/contracts/ci.contract.d.ts.map +1 -1
  21. package/dist/contracts/compliance.contract.d.ts +163 -0
  22. package/dist/contracts/compliance.contract.d.ts.map +1 -0
  23. package/dist/contracts/compliance.contract.js +41 -0
  24. package/dist/contracts/compliance.contract.js.map +1 -0
  25. package/dist/contracts/device.contract.d.ts +6 -0
  26. package/dist/contracts/device.contract.d.ts.map +1 -1
  27. package/dist/contracts/extensions.contract.d.ts +2 -0
  28. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  29. package/dist/contracts/loops.contract.d.ts +15 -0
  30. package/dist/contracts/loops.contract.d.ts.map +1 -1
  31. package/dist/contracts/needs.contract.d.ts +49 -0
  32. package/dist/contracts/needs.contract.d.ts.map +1 -1
  33. package/dist/contracts/offload.contract.d.ts +5 -0
  34. package/dist/contracts/offload.contract.d.ts.map +1 -1
  35. package/dist/contracts/personas.contract.d.ts +10 -0
  36. package/dist/contracts/personas.contract.d.ts.map +1 -1
  37. package/dist/contracts/providers.contract.d.ts +2 -0
  38. package/dist/contracts/providers.contract.d.ts.map +1 -1
  39. package/dist/contracts/providers.contract.js +4 -0
  40. package/dist/contracts/providers.contract.js.map +1 -1
  41. package/dist/contracts/runner.contract.d.ts +42 -10
  42. package/dist/contracts/runner.contract.d.ts.map +1 -1
  43. package/dist/contracts/sessions.contract.d.ts +17 -0
  44. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  45. package/dist/contracts/settings.contract.d.ts +84 -0
  46. package/dist/contracts/settings.contract.d.ts.map +1 -1
  47. package/dist/contracts/settings.contract.js +7 -0
  48. package/dist/contracts/settings.contract.js.map +1 -1
  49. package/dist/contracts/system.contract.d.ts +16 -2
  50. package/dist/contracts/system.contract.d.ts.map +1 -1
  51. package/dist/contracts/workflows.contract.d.ts +30 -0
  52. package/dist/contracts/workflows.contract.d.ts.map +1 -1
  53. package/dist/contracts/workspace.contract.d.ts +3 -0
  54. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  55. package/dist/events/agent-events.d.ts +74 -0
  56. package/dist/events/agent-events.d.ts.map +1 -1
  57. package/dist/events/agent-events.js +1 -0
  58. package/dist/events/agent-events.js.map +1 -1
  59. package/dist/events/requests.d.ts +45 -0
  60. package/dist/events/requests.d.ts.map +1 -1
  61. package/dist/events/system-events.d.ts +12 -0
  62. package/dist/events/system-events.d.ts.map +1 -1
  63. package/dist/events/transcript.d.ts +117 -0
  64. package/dist/events/transcript.d.ts.map +1 -1
  65. package/dist/index.d.ts +828 -73
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +11 -0
  68. package/dist/index.js.map +1 -1
  69. package/dist/models/agent-catalog.d.ts +18 -3
  70. package/dist/models/agent-catalog.d.ts.map +1 -1
  71. package/dist/models/agent-catalog.js +61 -12
  72. package/dist/models/agent-catalog.js.map +1 -1
  73. package/dist/models/agent-runtimes.d.ts +5 -2
  74. package/dist/models/agent-runtimes.d.ts.map +1 -1
  75. package/dist/models/agent-runtimes.js +73 -40
  76. package/dist/models/agent-runtimes.js.map +1 -1
  77. package/dist/models/model-setups.d.ts +5 -0
  78. package/dist/models/model-setups.d.ts.map +1 -1
  79. package/dist/models/provider-specs.d.ts +153 -32
  80. package/dist/models/provider-specs.d.ts.map +1 -1
  81. package/dist/models/provider-specs.js +97 -11
  82. package/dist/models/provider-specs.js.map +1 -1
  83. package/dist/policy/reserved-servers.d.ts.map +1 -1
  84. package/dist/policy/reserved-servers.js +1 -0
  85. package/dist/policy/reserved-servers.js.map +1 -1
  86. package/dist/policy/sandbox-power.d.ts +7 -2
  87. package/dist/policy/sandbox-power.d.ts.map +1 -1
  88. package/dist/policy/sandbox-power.js +34 -1
  89. package/dist/policy/sandbox-power.js.map +1 -1
  90. package/dist/policy/turned-away.d.ts.map +1 -1
  91. package/dist/policy/turned-away.js +1 -0
  92. package/dist/policy/turned-away.js.map +1 -1
  93. package/dist/protocol/doors/listener-protocol.d.ts +43 -1
  94. package/dist/protocol/doors/listener-protocol.d.ts.map +1 -1
  95. package/dist/protocol/doors/listener-protocol.js +10 -0
  96. package/dist/protocol/doors/listener-protocol.js.map +1 -1
  97. package/dist/protocol/raw/raw-json-routes.d.ts +92 -0
  98. package/dist/protocol/raw/raw-json-routes.d.ts.map +1 -1
  99. package/dist/protocol/raw/raw-json-routes.js +2 -1
  100. package/dist/protocol/raw/raw-json-routes.js.map +1 -1
  101. package/dist/protocol/raw/raw-routes.d.ts +13 -0
  102. package/dist/protocol/raw/raw-routes.d.ts.map +1 -1
  103. package/dist/protocol/raw/raw-routes.js +3 -0
  104. package/dist/protocol/raw/raw-routes.js.map +1 -1
  105. package/dist/schemas/activity.d.ts +2 -0
  106. package/dist/schemas/activity.d.ts.map +1 -1
  107. package/dist/schemas/agent.d.ts +34 -0
  108. package/dist/schemas/agent.d.ts.map +1 -1
  109. package/dist/schemas/agent.js +7 -6
  110. package/dist/schemas/agent.js.map +1 -1
  111. package/dist/schemas/agents-list.d.ts +7 -0
  112. package/dist/schemas/agents-list.d.ts.map +1 -1
  113. package/dist/schemas/agents.d.ts +18 -0
  114. package/dist/schemas/agents.d.ts.map +1 -1
  115. package/dist/schemas/ai-literacy.d.ts +54 -0
  116. package/dist/schemas/ai-literacy.d.ts.map +1 -0
  117. package/dist/schemas/ai-literacy.js +15 -0
  118. package/dist/schemas/ai-literacy.js.map +1 -0
  119. package/dist/schemas/audit.d.ts +166 -0
  120. package/dist/schemas/audit.d.ts.map +1 -0
  121. package/dist/schemas/audit.js +100 -0
  122. package/dist/schemas/audit.js.map +1 -0
  123. package/dist/schemas/automations.d.ts +27 -0
  124. package/dist/schemas/automations.d.ts.map +1 -1
  125. package/dist/schemas/automations.js +11 -0
  126. package/dist/schemas/automations.js.map +1 -1
  127. package/dist/schemas/ci.d.ts +10 -0
  128. package/dist/schemas/ci.d.ts.map +1 -1
  129. package/dist/schemas/compliance.d.ts +423 -0
  130. package/dist/schemas/compliance.d.ts.map +1 -0
  131. package/dist/schemas/compliance.js +135 -0
  132. package/dist/schemas/compliance.js.map +1 -0
  133. package/dist/schemas/data-protection.d.ts +28 -0
  134. package/dist/schemas/data-protection.d.ts.map +1 -0
  135. package/dist/schemas/data-protection.js +49 -0
  136. package/dist/schemas/data-protection.js.map +1 -0
  137. package/dist/schemas/devices.d.ts +12 -0
  138. package/dist/schemas/devices.d.ts.map +1 -1
  139. package/dist/schemas/engines.d.ts +22 -1
  140. package/dist/schemas/engines.d.ts.map +1 -1
  141. package/dist/schemas/engines.js +1 -1
  142. package/dist/schemas/engines.js.map +1 -1
  143. package/dist/schemas/extension-updates.d.ts +4 -0
  144. package/dist/schemas/extension-updates.d.ts.map +1 -1
  145. package/dist/schemas/git/git.d.ts +33 -0
  146. package/dist/schemas/git/git.d.ts.map +1 -1
  147. package/dist/schemas/git/git.js +29 -0
  148. package/dist/schemas/git/git.js.map +1 -1
  149. package/dist/schemas/hosts.d.ts +36 -0
  150. package/dist/schemas/hosts.d.ts.map +1 -1
  151. package/dist/schemas/hosts.js +2 -0
  152. package/dist/schemas/hosts.js.map +1 -1
  153. package/dist/schemas/intentic-os.d.ts +117 -0
  154. package/dist/schemas/intentic-os.d.ts.map +1 -0
  155. package/dist/schemas/intentic-os.js +66 -0
  156. package/dist/schemas/intentic-os.js.map +1 -0
  157. package/dist/schemas/loops.d.ts +15 -0
  158. package/dist/schemas/loops.d.ts.map +1 -1
  159. package/dist/schemas/members.d.ts +90 -0
  160. package/dist/schemas/members.d.ts.map +1 -1
  161. package/dist/schemas/members.js +73 -3
  162. package/dist/schemas/members.js.map +1 -1
  163. package/dist/schemas/needs.d.ts +56 -0
  164. package/dist/schemas/needs.d.ts.map +1 -1
  165. package/dist/schemas/offload.d.ts +5 -0
  166. package/dist/schemas/offload.d.ts.map +1 -1
  167. package/dist/schemas/personas.d.ts +10 -0
  168. package/dist/schemas/personas.d.ts.map +1 -1
  169. package/dist/schemas/providers/plan-limits.d.ts +20 -0
  170. package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
  171. package/dist/schemas/providers/provider-oauth.js +1 -1
  172. package/dist/schemas/providers/provider-oauth.js.map +1 -1
  173. package/dist/schemas/providers/usage.d.ts +1 -0
  174. package/dist/schemas/providers/usage.d.ts.map +1 -1
  175. package/dist/schemas/providers/usage.js +4 -0
  176. package/dist/schemas/providers/usage.js.map +1 -1
  177. package/dist/schemas/settings.d.ts +48 -0
  178. package/dist/schemas/settings.d.ts.map +1 -1
  179. package/dist/schemas/settings.js +14 -0
  180. package/dist/schemas/settings.js.map +1 -1
  181. package/dist/schemas/system.d.ts +8 -0
  182. package/dist/schemas/system.d.ts.map +1 -1
  183. package/dist/schemas/system.js +2 -0
  184. package/dist/schemas/system.js.map +1 -1
  185. package/dist/schemas/workflows.d.ts +50 -0
  186. package/dist/schemas/workflows.d.ts.map +1 -1
  187. package/dist/schemas/workspace/workspace-tree.d.ts +4 -0
  188. package/dist/schemas/workspace/workspace-tree.d.ts.map +1 -1
  189. package/dist/schemas/workspace/workspace-tree.js +16 -1
  190. package/dist/schemas/workspace/workspace-tree.js.map +1 -1
  191. package/dist/state/definition.d.ts +80 -4
  192. package/dist/state/definition.d.ts.map +1 -1
  193. package/dist/state/history-state.d.ts.map +1 -1
  194. package/dist/state/history-state.js +2 -0
  195. package/dist/state/history-state.js.map +1 -1
  196. package/package.json +5 -5
  197. package/src/contracts/activity.contract.ts +1 -1
  198. package/src/contracts/agents.contract.ts +13 -1
  199. package/src/contracts/audit.contract.ts +48 -0
  200. package/src/contracts/compliance.contract.ts +57 -0
  201. package/src/contracts/providers.contract.ts +9 -0
  202. package/src/contracts/settings.contract.ts +7 -0
  203. package/src/events/agent-events.ts +4 -0
  204. package/src/index.ts +11 -0
  205. package/src/models/agent-catalog.test.ts +317 -53
  206. package/src/models/agent-catalog.ts +115 -22
  207. package/src/models/agent-runtimes.ts +273 -75
  208. package/src/models/capability-ledger.test.ts +12 -1
  209. package/src/models/provider-specs.test.ts +42 -7
  210. package/src/models/provider-specs.ts +160 -20
  211. package/src/policy/reserved-servers.ts +2 -1
  212. package/src/policy/sandbox-power.ts +67 -8
  213. package/src/policy/tests/sandbox-power.test.ts +59 -1
  214. package/src/policy/turned-away.ts +2 -0
  215. package/src/protocol/doors/listener-protocol.ts +74 -6
  216. package/src/protocol/raw/raw-json-routes.ts +2 -1
  217. package/src/protocol/raw/raw-routes.ts +7 -0
  218. package/src/schemas/activity.ts +2 -2
  219. package/src/schemas/agent.ts +22 -9
  220. package/src/schemas/ai-literacy.ts +17 -0
  221. package/src/schemas/audit.ts +131 -0
  222. package/src/schemas/automations.ts +13 -0
  223. package/src/schemas/compliance.ts +195 -0
  224. package/src/schemas/daemon-health.test.ts +2 -0
  225. package/src/schemas/data-protection.ts +99 -0
  226. package/src/schemas/engines.ts +1 -1
  227. package/src/schemas/git/git.ts +46 -0
  228. package/src/schemas/hosts.ts +5 -0
  229. package/src/schemas/intentic-os.test.ts +86 -0
  230. package/src/schemas/intentic-os.ts +130 -0
  231. package/src/schemas/members.test.ts +47 -0
  232. package/src/schemas/members.ts +141 -6
  233. package/src/schemas/providers/provider-oauth.ts +1 -1
  234. package/src/schemas/providers/usage.ts +6 -0
  235. package/src/schemas/settings.ts +32 -0
  236. package/src/schemas/system.ts +2 -0
  237. package/src/schemas/workspace/workspace-tree.ts +26 -1
  238. package/src/state/history-state.ts +7 -0
@@ -8,7 +8,7 @@ export type ExecutionBackend = "shell" | "js";
8
8
 
9
9
  export interface AgentCapabilities {
10
10
  // Which loop serves the turn; `opencode-gemini` stays separate since adapter health keys off this field.
11
- readonly runtime: "claude-code" | "codex" | "opencode" | "opencode-gemini" | "acp" | "devin" | "pi" | "cursor";
11
+ readonly runtime: "claude-code" | "codex" | "opencode" | "opencode-gemini" | "acp" | "devin" | "kimi-cli" | "grok-build" | "pi" | "cursor" | "muse";
12
12
  // Mid-turn injection (the SteeringQueue behind /agent/steer); needs the SDK's streaming-input mode.
13
13
  readonly steering: boolean;
14
14
  // How much of the permission-mode axis the runtime honours: every PermissionMode, or just propose-then-approve.
@@ -49,8 +49,14 @@ export interface AgentCapabilities {
49
49
  // refuse-only - the same channel, but a clock forces a refusal instead of a park.
50
50
  // none - no seam before a command runs; treated as tainted, never as clean.
51
51
  readonly rulebook: "hooks" | "approval" | "refuse-only" | "none";
52
- // Whether a credential is masked; "none" is structural, only the Claude Code loop can rewrite a model's read.
53
- readonly secrets: "masked" | "none";
52
+ // How far the sandbox's stored secrets reach this runtime and are kept from its model:
53
+ // masked - a `{{secret:…}}` reference resolves in the shell, and a stored value is masked out of every tool result,
54
+ // file reads included (the Claude Code loop's PostToolUse hook; Pi's extension, whose tool_result handler sees all).
55
+ // shell - a reference resolves in shell commands and the daemon's own tools, and a value is masked out of what a
56
+ // command prints and those tools' results, since the daemon runs or wraps the shell; a file the agent reads, a
57
+ // fetched page and an MCP result reach the model as they are (Cursor refuses a read of a file holding one instead).
58
+ // none - neither: a reference stays text, and a value reaches the model as the tool read it.
59
+ readonly secrets: "masked" | "shell" | "none";
54
60
  // Whether the privacy shield can stand between this loop and its provider: `gateway` when the daemon names the base
55
61
  // URL the loop sends its model requests to (so the gateway reads and masks every byte of them); `hooks` when the wire
56
62
  // is the vendor's own but every channel the model reads through passes the daemon first (the prompt and instructions
@@ -94,66 +100,119 @@ export const CLAUDE_CODE: AgentCapabilities = {
94
100
  toolAllowlist: true,
95
101
  };
96
102
 
97
- // Codex app-server: item-level events, the turn's remote MCP servers over http, and four seams (steer, a question
98
- // request, the skills list, the shared mount namespace). The daemon's in-process servers, plugins and server approvals
99
- // stay unwired.
103
+ // Codex app-server: item-level events, the turn's remote MCP servers and the daemon's own over http, per-thread hooks
104
+ // on the door's policy gate, and its seams (steer, a question request, approval requests, the skills list, the shared
105
+ // mount namespace). Plugins stay unwired.
100
106
  export const CODEX: AgentCapabilities = {
101
107
  runtime: "codex",
102
108
  steering: true,
103
- permissions: "plan",
109
+ // Every mode: `default` raises the per-tool card on each command, patch and sandbox widening Codex asks about
110
+ // (known-safe commands never ask); plan runs Codex's own plan mode under the shared plan card; the rest run.
111
+ permissions: "modes",
104
112
  questions: true,
105
- mcp: "http",
106
- execution: ["shell"],
113
+ // The daemon's own servers (ui, accounts, secrets, subagents, watch, deps, diagnostics, code, gate) ride the turn's
114
+ // door lease beside its remote mounts (runtimes/daemon-tools/daemon-servers.ts), each with its own tool timeout.
115
+ mcp: "tools",
116
+ // The JS backend is the door's `code` server, its scripts consulted against the turn's gate before they run.
117
+ execution: ["shell", "js"],
107
118
  effort: true,
108
- fastMode: false,
119
+ // Asks app-server for the tier a model publishes as Fast (serviceTier "priority"), which model/list badges.
120
+ fastMode: true,
109
121
  isolation: "namespace",
110
122
  commands: true,
111
- terminals: false,
112
- recovery: false,
123
+ // The gate's PreToolUse answer rewrites each shell command through bin/tmux-run into the conversation's agent tmux
124
+ // session, as the Claude loop's Bash hook does (codex-hooks.ts). Codex's own `write_stdin` then reaches tmux-run,
125
+ // not the program in the pane, and a call tmux-run returns from early ends Codex's process for it.
126
+ terminals: true,
127
+ // Failures arrive typed (codexErrorInfo): a transport or capacity failure is coded provider-outage, a spent
128
+ // allowance rate_limit, a full window context-overflow, so the breaker counts them and auto-resume re-runs them.
129
+ recovery: true,
113
130
  warm: false,
114
- // Both ride the per-thread config: one replaces Codex's base prompt, the other adds a developer message.
131
+ // thread/start's own fields: baseInstructions replaces Codex's base prompt, developerInstructions adds a developer
132
+ // message.
115
133
  instructions: "replace",
116
134
  skillDiscovery: "native",
117
- // Asked only when the owner has written command rules; an unconfigured workspace pays nothing for it.
118
- rulebook: "approval",
119
- secrets: "none",
135
+ // A per-thread PreToolUse hook puts every function tool (the shell, apply_patch, every MCP tool, spawn_agent) to the
136
+ // door's gate before it runs, which may park it on a card (codex-hooks.ts). Codex fails a hook open, so the gate is
137
+ // confirmed connected before the turn starts, a command or patch Codex asks approval for runs only if the gate
138
+ // allowed that very call, and a call that starts unanswered stops the turn. Codex's own subagents' threads never
139
+ // connect the gate, so there the rules reach only the commands and patches Codex asks about.
140
+ rulebook: "hooks",
141
+ // Shell commands resolve `{{secret:…}}` in the gate's rewrite and run under a wrapper that masks stored values in
142
+ // their output (codex-mask-script.ts); apply_patch, view_image and hosted tools pass no wrapper.
143
+ secrets: "shell",
120
144
  // Its model provider's base_url is the daemon's, written into the per-thread config.
121
145
  privacy: "gateway",
122
- // Codex runs the commands it judges read-only without asking (its known-safe list), and every tool it has is the
123
- // shell, so no answer the daemon gives can keep a call off a list.
124
- toolAllowlist: false,
146
+ // The gate's PreToolUse refuses every call off the list, fail closed on the turn's own thread as above; with a list
147
+ // Codex's hosted search and pictures, its image viewer, question tool and own subagents are switched off, the shell
148
+ // too unless listed, and each MCP server is cut to its listed tools (`enabled_tools`). The residual: a known-safe
149
+ // read-only command off the list runs if the in-process gate fails after it connected, and the turn is then stopped.
150
+ toolAllowlist: true,
125
151
  };
126
152
 
127
153
  // OpenCode (the Grok runtime): its own agentic loop and tools, with the turn's remote MCP servers mounted on the shared
128
- // server per conversation. Takes a model id, a prompt and one system message of ours; no effort scale, no command list.
154
+ // server per conversation, driven over OpenCode 2's session API (runtimes/opencode).
129
155
  export const OPENCODE: AgentCapabilities = {
130
156
  runtime: "opencode",
131
- steering: false,
132
- permissions: "plan",
133
- questions: false,
134
- // Added at runtime under the conversation's own names, each session's own rules showing it only its own (opencode-mcp.ts).
135
- mcp: "http",
136
- execution: ["shell"],
137
- effort: false,
157
+ // The turn's steering queue is sent as OpenCode steers (`session.prompt` with `delivery: "steer"`): taken in at the
158
+ // model's next step, or starting the next execution of the same turn once its work is done; the turn stays open until
159
+ // the session says it took each one in.
160
+ steering: true,
161
+ // `default` puts every edit, fetch and command the rulebook let through on a per-tool card (session rules asking about
162
+ // them, answered from the card, never "always"); `plan` is still the emulated propose-then-approve, since OpenCode's
163
+ // own plan agent has no card of its own.
164
+ permissions: "modes",
165
+ // OpenCode's `question` tool raises a form, drawn as a question card and answered from it; a form no card can carry
166
+ // is cancelled with a sentence telling the model to carry on.
167
+ questions: true,
168
+ // The turn's remote servers and the daemon's own (ui, accounts, secrets, subagents, watch, deps, diagnostics, code),
169
+ // served through the MCP door on the turn's lease (daemon-tools/door-tools.ts) and added at runtime under the
170
+ // conversation's own names, each session's own rules showing it only its own (opencode-mcp.ts); no plugins.
171
+ mcp: "tools",
172
+ // The daemon's JS backend is the door's `code` server, each script consulted against the turn's gate through the
173
+ // relay the loop attaches (runtimes/daemon-tools/turn-relay.ts).
174
+ execution: ["shell", "js"],
175
+ // The turn's tier is the session's model variant, where the model declares that tier: the Grok 3 Mini line on xAI,
176
+ // and every Google model the translator publishes a thinking scale for.
177
+ effort: true,
138
178
  fastMode: false,
179
+ // One `opencode serve` serves every conversation from the daemon's own mount namespace. A server per conversation
180
+ // spawned into its turn's namespace (as Codex and Cursor spawn) is not built, for three reasons found 2026-10-12: its
181
+ // sessions would live in a data dir of their own, so every conversation's existing sessions would have to move there
182
+ // (session.export/import) and back; the xAI sign-in would have to be copied into each (credential.create), and two
183
+ // servers refreshing one OAuth grant is untested; and the shell script's socket and the data dir sit under the
184
+ // daemon's auth dir, which an anchored turn's /work does not show and an agent domain must not reach.
139
185
  isolation: "cwd",
140
- commands: false,
141
- terminals: false,
142
- recovery: false,
186
+ // OpenCode's command list is published each turn, and a `/name` prompt runs as that command.
187
+ commands: true,
188
+ // OpenCode runs its shell as the daemon's script, which runs each command through tmux-run in the conversation's tmux
189
+ // session, and the turn surfaces it with a `terminal` frame (runtimes/opencode/shell/opencode-shell.ts).
190
+ terminals: true,
191
+ // Failures are coded from OpenCode's own failure types: a spent allowance `rate_limit`, a provider's transport,
192
+ // internal or timeout failure (and a retry the turn will not wait out, or a silent provider) `provider-outage`, a
193
+ // session past its window `context-overflow`, an unroutable model `model-unavailable`.
194
+ recovery: true,
143
195
  warm: false,
144
- // The session's own instruction entry, set per message; adds to OpenCode's own prompt, with no seam to replace it.
145
- instructions: "append",
196
+ // A prompt that replaces the base (the owner's own under `custom`, a small window's paragraph) runs on the daemon's
197
+ // twin agents, whose base is blank, so it is the whole system text; anything else is added to OpenCode's own base.
198
+ instructions: "replace",
146
199
  skillDiscovery: "prompt",
147
- // OpenCode raises its permission asks for the commands its config names; the daemon answers from the rulebook, a
148
- // hold parked on a card while the turn's inactivity watchdog is held.
200
+ // OpenCode raises its permission asks for every shell command, a subagent's included (the server's own rules ask about
201
+ // them all, and a child session inherits them); the daemon answers from the rulebook, a hold parked on a card while
202
+ // the turn's inactivity watchdog is held. The JS backend consults the same gate. Not `hooks`: which calls ask is still
203
+ // OpenCode's permission config (the daemon's rules make it every shell command), and an edit or an MCP call is asked
204
+ // about only where `default` mode puts it on a person's card.
149
205
  rulebook: "approval",
150
- secrets: "none",
206
+ // The daemon's shell script resolves a command's secret references after the rulebook judged it, and masks what it
207
+ // prints back to the references before OpenCode reads it (runtimes/opencode/shell/opencode-shell.ts). A file the read tool opens is not.
208
+ secrets: "shell",
151
209
  // The shared server's provider baseURLs are written by the daemon at spawn.
152
210
  privacy: "gateway",
153
- // Its session rules could deny every action off the list, but they apply after the server's and the last match wins,
154
- // so allowing a listed shell command there would override the owner's rulebook asking about it. Not kept until the
155
- // permission judge (opencode-permissions.ts) checks the list on every call instead.
156
- toolAllowlist: false,
211
+ // A turn's list becomes its session's rules: every action denied, the listed ones allowed, the server's asks kept
212
+ // after them, so the model is offered only the listed tools and OpenCode refuses a call to any other; a subagent's
213
+ // session inherits them. Every built-in tool has its own permission action (checked against 2.0.26), and a shell entry
214
+ // narrowed to a command is checked against each command's ask (runtimes/opencode/allowlist/opencode-allowlist.ts).
215
+ toolAllowlist: true,
157
216
  };
158
217
 
159
218
  // The same OpenCode loop, serving Gemini instead of xAI, identical abilities. The Claude Code loop announces itself in
@@ -164,25 +223,44 @@ export const OPENCODE_GEMINI: AgentCapabilities = {
164
223
  };
165
224
 
166
225
  // Any agent speaking the Agent Client Protocol: a documented floor, not the native ceiling. Publishes commands and
167
- // terminals, takes the turn's remote MCP servers when it advertises http MCP, but owns its own model, effort and permission posture.
226
+ // terminals, takes the turn's remote MCP servers when it advertises http MCP, and uses what else the agent advertises
227
+ // per session (its modes, its `thought_level` option, form elicitation); what no agent is bound to advertise stays off.
168
228
  export const ACP: AgentCapabilities = {
169
229
  runtime: "acp",
170
230
  steering: false,
171
- permissions: "plan",
172
- questions: false,
173
- mcp: "http",
174
- execution: ["shell"],
231
+ // Every mode: a `default` turn puts each call the agent asks permission for on a person's card (acp-permissions.ts),
232
+ // a plan turn runs the daemon's two phases with the agent's own plan mode under the first where it has one, and the
233
+ // turn's posture moves the session into the agent's matching mode by id or name (acp-modes.ts).
234
+ permissions: "modes",
235
+ // Form elicitation is advertised, and a form parks on the question card (acp-elicitation.ts); whether the agent asks
236
+ // through it is its own choice, as which calls it asks permission for is.
237
+ questions: true,
238
+ // The turn's remote mounts and the daemon's own servers (ui, accounts, secrets, subagents, watch, deps, diagnostics,
239
+ // code) on its door lease (daemon-servers.ts): over http where the agent advertises http MCP, else through the
240
+ // daemon's stdio proxy (acp-mcp-proxy.ts), the transport every ACP agent takes. A warm session keeps the servers it
241
+ // opened with, so one a later turn adds is seen from the next session on.
242
+ mcp: "tools",
243
+ // The JS backend is the door's `code` server, each script asked of the turn's gate before it runs.
244
+ execution: ["shell", "js"],
245
+ // A session's `thought_level` option takes the turn's effort where the agent offers one (acp-model.ts), but this
246
+ // record is every agent's and most offer none, so the picker shows no scale.
175
247
  effort: false,
176
248
  fastMode: false,
249
+ // One warm process serves every conversation on the agent, each session in its own cwd; a mount namespace belongs to
250
+ // one turn, so a process shared across conversations can stand in none of them. Keying the pool by namespace would
251
+ // give up the warm sessions the pool exists for.
177
252
  isolation: "cwd",
178
253
  commands: true,
179
254
  terminals: true,
255
+ // A failure the agent words as a spent allowance or an outgrown window is coded (acp-agent.ts ACP_FAILURES), but the
256
+ // words are each agent's own and the protocol marks no outage, so nothing promises the codes auto-resume keys off.
180
257
  recovery: false,
181
258
  warm: false,
182
259
  // ACP's calls carry no system field: the agent owns its instructions, the persona note rides the message.
183
260
  instructions: "none",
184
261
  skillDiscovery: "prompt",
185
- // In the protocol floor, so every agent has it; which calls it asks about is entirely its own choice.
262
+ // In the protocol floor, so every agent has it; which calls it asks about is entirely its own choice. A terminal the
263
+ // daemon runs for it (terminal/create) is asked about here too, unless the permission for it already was.
186
264
  rulebook: "approval",
187
265
  secrets: "none",
188
266
  // The agent picks its own provider and endpoint from its own login and environment.
@@ -193,38 +271,91 @@ export const ACP: AgentCapabilities = {
193
271
 
194
272
  // Devin's own CLI (`devin acp`), the ACP floor with one seam an installed agent lacks: the daemon picks the model, from
195
273
  // the account's own list, through the session's model option. Its own runtime name, as OpenCode's Gemini twin has, since
196
- // adapter health keys off this field and Devin's readiness is an account and an engine, not an installed card.
274
+ // adapter health keys off this field and Devin's readiness is an account and an engine, not an installed card. Its
275
+ // modes (`accept-edits`, `smart`, `ask`, `plan`, `bypass` as of CLI 3000.11.3) carry the floor's posture mapping.
197
276
  export const DEVIN: AgentCapabilities = {
198
277
  ...ACP,
199
278
  runtime: "devin",
279
+ // Devin's sessions offer no `thought_level` option (CLI 3000.11.3); the model probe reads one should it appear
280
+ // (devin-catalog.ts), and the floor already forwards the turn's effort to it.
281
+ effort: false,
282
+ };
283
+
284
+ // Moonshot's Kimi Code CLI (`kimi acp`), the ACP floor where the CLI's handshake grants more (2.1.1): the daemon picks the
285
+ // model from the account's own list through the session's `model` option, and a session offers a `thinking` option in
286
+ // the protocol's `thought_level` category, `off` plus the efforts the model declares, which the turn's effort settles
287
+ // (acp-model.ts). Its modes (`default`, `plan`, `auto`, `yolo`) carry the floor's posture mapping, and its ask-user
288
+ // tool raises a form elicitation, which parks on the question card. It advertises http MCP, so the turn's servers
289
+ // reach it directly. Its own runtime name, since its readiness is a CLI sign-in and an engine, not an installed card.
290
+ export const KIMI_CLI: AgentCapabilities = {
291
+ ...ACP,
292
+ runtime: "kimi-cli",
293
+ effort: true,
294
+ };
295
+
296
+ // xAI's Grok Build CLI (`grok agent stdio`), the ACP floor where its handshake grants more (1.0.52): the model through the
297
+ // session's `model` option and the turn's effort through its `thought_level` option (each model's own levels, `low` to
298
+ // `xhigh`). Its modes are `default`, `plan` and `ask`: no bypass mode, so a turn that runs without asking stays in
299
+ // `default` and the floor answers each permission it raises from the rulebook. It advertises http MCP.
300
+ export const GROK_BUILD: AgentCapabilities = {
301
+ ...ACP,
302
+ runtime: "grok-build",
303
+ effort: true,
304
+ // Its ask-user tool reaches its own client through an xAI protocol extension, not form elicitation, so the daemon
305
+ // switches the tool off for every session it opens (grok-build-cli.ts) rather than leave the model asking nobody.
306
+ questions: false,
200
307
  };
201
308
 
202
309
  // Pi driven over its RPC mode: above the ACP floor and below the Claude Code ceiling. Real mid-turn steering, an effort
203
- // scale, and a published command list; no MCP seam, no approval channel, bash runs in-process with no terminal session.
310
+ // scale, a published command list, and a process started per turn with the daemon's instructions, skills, MCP servers
311
+ // and namespace (runtimes/pi/pi-rpc.ts). No approval channel: the daemon's own Pi extension stands in front of every
312
+ // tool instead (runtimes/pi/pi-hooks.ts).
204
313
  export const PI: AgentCapabilities = {
205
314
  runtime: "pi",
206
315
  steering: true,
207
316
  permissions: "plan",
208
- questions: false,
209
- mcp: "none",
210
- execution: ["shell"],
317
+ // The daemon's own Pi extension registers an `ask_user` tool on Pi's dialogs, which park on the question card
318
+ // (pi-extension.ts, pi-extension-ui.ts).
319
+ questions: true,
320
+ // The turn's remote servers and the daemon's own (daemon-servers.ts), mounted through the same extension with Pi's
321
+ // built-in MCP and declared to the model; Pi has it from 0.99, an older Pi mounts none, and the turn says so rather
322
+ // than the picker.
323
+ mcp: "tools",
324
+ // The JS backend is the door's `code` server, each script asked of the turn's gate before it runs.
325
+ execution: ["shell", "js"],
211
326
  effort: true,
212
327
  fastMode: false,
213
- isolation: "cwd",
328
+ // An anchored turn's Pi is entered into its mount namespace (nsenter), as Codex's and Cursor's are.
329
+ isolation: "namespace",
214
330
  commands: true,
215
- terminals: false,
216
- recovery: false,
331
+ // The extension's `tool_call` hook rewrites each bash line through bin/tmux-run into the conversation's agent tmux
332
+ // session, as the Claude loop's Bash hook does (pi-hooks.ts).
333
+ terminals: true,
334
+ // Pi reports a provider failure as its own event (auto_retry_end, an errored message_end) carrying the provider's
335
+ // sentence, status code included, which the shared rules code (pi-events.ts PI_FAILURES).
336
+ recovery: true,
217
337
  warm: false,
218
- // Pi's RPC sets no standing instructions; like ACP, it hears the persona note through the user message.
219
- instructions: "none",
220
- skillDiscovery: "prompt",
221
- // The one runtime with no seam at all: bash runs in-process, and no rule the owner writes can apply.
222
- rulebook: "none",
223
- secrets: "none",
338
+ // `--system-prompt` replaces Pi's base and `--append-system-prompt` adds to it, both per process; `--no-context-files`
339
+ // keeps Pi from loading AGENTS.md a second time beside the daemon's own note.
340
+ instructions: "replace",
341
+ // `--skill` per loaded skill, the persona's filter applied, with Pi's own discovery off (`--no-skills`).
342
+ skillDiscovery: "native",
343
+ // The daemon's extension, loaded with `-e` ahead of any other and whatever the card's command says of discovery, puts
344
+ // every tool call (MCP and codemode calls included) to the turn's own hook socket before it runs, where a held one
345
+ // parks on a card while Pi waits (pi-hooks.ts). A call it cannot ask about is refused, as a handler that throws is,
346
+ // and the turn does not start on a Pi whose extension never said its hooks are in place. It declines trust in the
347
+ // project, so nothing the agent writes into the workspace loads as an extension after it; one the owner installed
348
+ // for Pi itself still sees a call after the rules did.
349
+ rulebook: "hooks",
350
+ // Shell lines resolve `{{secret:…}}` before they run, and the extension's `tool_result` hook masks every stored value
351
+ // in every tool's result, file reads included, as the Claude loop's PostToolUse does; a result it cannot mask is
352
+ // withheld.
353
+ secrets: "masked",
224
354
  // Pi calls its provider from its own login; the daemon names no endpoint.
225
355
  privacy: "none",
226
- // No seam before a tool runs.
227
- toolAllowlist: false,
356
+ // The same socket refuses every call off the turn's list, Pi's tools read under the Claude loop's names (bash as
357
+ // Bash, find as Glob) and MCP tools under the same `mcp__<server>__<tool>`; fail closed as above.
358
+ toolAllowlist: true,
228
359
  };
229
360
 
230
361
  // Cursor's own runtime, driven through `@cursor/sdk` from this daemon's process; an embedding surface, so most seams
@@ -233,8 +364,12 @@ export const CURSOR: AgentCapabilities = {
233
364
  runtime: "cursor",
234
365
  // Run.steer injects into the live run; only a `complete_delivered` ack transfers ownership of the message.
235
366
  steering: true,
236
- // Cursor's own plan mode, not this repo's emulation; not "modes" since the hook can't gate the whole surface.
237
- permissions: "plan",
367
+ // Every mode. `default` puts each call that changes something on the per-tool card: the preToolUse and
368
+ // beforeMCPExecution hooks park on it for Cursor's own tools and an MCP server's, a shell command after the rules
369
+ // unless their own card stood in, and the daemon's custom tools ask in their handlers, which Cursor never asks about
370
+ // (cursor-approvals.ts). `plan` runs the daemon's two phases with Cursor's read-only plan mode under the first; the
371
+ // rest run.
372
+ permissions: "modes",
238
373
  // True since the daemon supplies its own ask tool; Cursor's own can fabricate an answer, so it's disallowed.
239
374
  questions: true,
240
375
  // The turn's remote MCP servers plus host callbacks; everything but a Claude Code plugin checkout.
@@ -248,27 +383,90 @@ export const CURSOR: AgentCapabilities = {
248
383
  // The SDK agent of an anchored turn runs in a runtime process born in the turn's mount namespace (the daemon keeps
249
384
  // the loop, the tools and the hook gate), so its shell and its edits see the worktree at /work.
250
385
  isolation: "namespace",
251
- // Cursor's commands are files on disk the SDK loads but doesn't publish back, so there's no list to offer.
252
- commands: false,
253
- // The SDK runs its shell itself, as a plain child; there is no tmux session for the terminal panel to attach to.
254
- terminals: false,
386
+ // Cursor's commands are files on disk (`.cursor/commands/*.md`) the SDK loads but doesn't publish back, so the daemon
387
+ // reads them itself, publishes the list and sends `/name` as the command's text (cursor-commands.ts).
388
+ commands: true,
389
+ // Every shell command runs through the hook script, which the daemon hands the command as a bin/tmux-run line, so it
390
+ // runs in the `agent-<session>` tmux session the panel attaches to, as a Claude Code turn's Bash does
391
+ // (cursor-terminal.ts). Where tmux-run is not in the image the command runs as the script's own child.
392
+ terminals: true,
255
393
  // The SDK throws typed errors instead of dissolving a refusal into prose, so the adapter files coded frames.
256
394
  recovery: true,
257
395
  warm: false,
258
- // append, reached differently: `beforeSubmitPrompt`'s reply folds the daemon's text onto Cursor's base. The SDK can
259
- // replace that base outright, but only for an entitled account, so the seam every turn can reach is this one.
260
- instructions: "append",
261
- skillDiscovery: "prompt",
396
+ // The owner's replacement prompt replaces Cursor's base through AgentOptions.systemPrompt on an account entitled to
397
+ // it. The entitlement is server-side and answered only by a send, so a turn tries it and an account refused runs the
398
+ // turn again with the replacement folded onto the base through `beforeSubmitPrompt`, remembered for its later turns
399
+ // (cursor-agent.ts); everything else the daemon adds always rides that append.
400
+ instructions: "replace",
401
+ // Cursor's own loader reads every SKILL.md under `.agents/skills/` of each workspace root it is given: the cwd holds
402
+ // the loaded skills, and the worn persona's kit is laid out as a second root (`local.dirs`); a skill the persona
403
+ // withholds is refused at the read tool, the seam the Claude loop's `Skill(<name>)` denial is (cursor-skills.ts).
404
+ skillDiscovery: "native",
262
405
  // The full hook tier, the only foreign runtime to reach it: a hold parks since the vendor waits on the hook.
263
406
  rulebook: "hooks",
264
- // none, structurally: its hooks only allow/deny or are discarded, nothing here to substitute a reference into.
265
- secrets: "none",
407
+ // Every shell command runs through the hook script: a `{{secret:name}}` reference resolves into what the script runs once
408
+ // the rules let the command through, and what it prints is masked back before Cursor reads it. Not "masked": Cursor's
409
+ // read tool can be refused but not rewritten, so a file holding a stored value is refused there, and what grep, list
410
+ // or an MCP server hands back is not read at all (cursor-secrets.ts).
411
+ secrets: "shell",
266
412
  // Cursor's own RPC to its own servers, so no gateway; but the daemon owns every channel the model reads through:
267
413
  // it hands over the prompt and instructions, its hooks stand before each file read and rewrite each shell command
268
414
  // to mask the output, the tools that read past both (grep, ls, glob, semantic search, fetch, lints) are withheld,
269
415
  // and MCP goes through the masking proxy (runtimes/cursor/cursor-shield.ts).
270
416
  privacy: "hooks",
271
- // Its hooks are installed only for a shielded turn, and a hook that cannot tell which turn is asking allows; the
272
- // built-in tool set is withheld by name, not granted. None of that is a list nothing gets past.
417
+ // Three layers (cursor-tool-list.ts): the `tools` option offers only the listed built-ins, a header Cursor's backend
418
+ // holds the main loop and, with `subagentInherit`, every `Task` child to; only the listed MCP servers and custom
419
+ // tools are mounted, and each custom tool checks the list in its handler; and every call Cursor's local executors run
420
+ // passes preToolUse or beforeMCPExecution, which check it (a `Bash(...)` narrowing included) and refuse a call they
421
+ // cannot place in a turn. A turn carrying a list does not start unless those hooks are installed, and a hook the
422
+ // daemon cannot answer refuses a live turn's call.
423
+ toolAllowlist: true,
424
+ };
425
+
426
+ // Muse Code, Meta's coding agent, driven over the Muse Session Protocol: one `muse serve` host per turn, born in the turn's
427
+ // mount namespace, its session kept in Muse's own store so the next turn resumes it (runtimes/muse). Meta's own loop for
428
+ // Meta's key, and a host for any other model API whose turn the daemon's translator serves on the Responses wire.
429
+ export const MUSE: AgentCapabilities = {
430
+ runtime: "muse",
431
+ // turn/steer into the running turn; a steer that arrives after it ended starts the next turn of the same phase.
432
+ steering: true,
433
+ // Every mode: sessions run under approval mode `promptUnmatched`, so Muse raises every shell command off its own allow
434
+ // rules. `default` puts each one the rulebook lets through on the per-tool card; plan runs a planning phase on Muse's
435
+ // `:read-only` profile (every approval refused) under the shared plan card, then executes on `:ask-me`; the rest run,
436
+ // the rulebook still answering every command. Workspace file edits raise nothing on `:ask-me`, so they get no card.
437
+ permissions: "modes",
438
+ // userInput/request parks on the question card and is answered back (userInput/answer, userInput/cancel).
439
+ questions: true,
440
+ // The turn's remote list and the daemon's own servers through the door (daemonServersFor), both as the session's
441
+ // own streamable-HTTP MCP config; no plugin checkouts.
442
+ mcp: "tools",
443
+ // The door's `code` server mounts the JS backend, its scripts consulted against the gate the loop attaches.
444
+ execution: ["shell", "js"],
445
+ // turn/start's reasoningEffort, in Muse's own tier names (the model list says which a model takes).
446
+ effort: true,
447
+ fastMode: false,
448
+ // The host is nsentered into an anchored turn's namespace, so its shell and edits see the worktree at /work.
449
+ isolation: "namespace",
450
+ // Muse's skills are not published back as a list, and a `/name` prompt is not turned into a skill input part.
451
+ commands: false,
452
+ terminals: false,
453
+ // Failures arrive typed (TurnError): a quota-exhausted one is coded rate_limit, a window the session outgrew
454
+ // context-overflow, a model call Muse retried and could not get through provider-outage, a host gone quiet
455
+ // provider-outage too; turn/retryScheduled is shown as the in-turn retry it is (muse-stream.ts).
456
+ recovery: true,
457
+ warm: false,
458
+ // No MSP call carries a system prompt (session config takes only MCP servers), so the daemon's notes ride the
459
+ // message and the owner's replacement prompt is not applied.
460
+ instructions: "none",
461
+ // Muse loads a trusted workspace's own skills from its own directories, which are not the daemon's; the daemon names
462
+ // the loaded ones in the opener.
463
+ skillDiscovery: "prompt",
464
+ // Muse's approval channel, answered from the rulebook; which commands Muse runs without asking (its own allow rules)
465
+ // is its choice.
466
+ rulebook: "approval",
467
+ secrets: "none",
468
+ // A Meta turn talks to api.meta.ai, a base the daemon does not name; Muse also reads files it never shows the daemon.
469
+ privacy: "none",
470
+ // Edits and reads run without asking anyone, so no answer the daemon gives can keep a call off a list.
273
471
  toolAllowlist: false,
274
472
  };
@@ -47,7 +47,9 @@ const LEDGER: Record<keyof AgentCapabilities, Backing> = {
47
47
  commands: "descriptive",
48
48
  terminals: "descriptive",
49
49
  recovery: "descriptive",
50
- // descriptive: only the Claude Code loop can mask a secret before the model reads it, via its PostToolUse hook.
50
+ // descriptive: where masking happens is each runtime's own wiring (the Claude Code loop's PostToolUse hook, Pi's
51
+ // extension, the shell wrappers of Codex and Cursor, OpenCode's daemon-run shell script); nothing gates a turn on it,
52
+ // and each answer short of the ceiling reaches the user as its own limitationsOf sentence.
51
53
  secrets: "descriptive",
52
54
  };
53
55
 
@@ -86,6 +88,15 @@ describe("a descriptive claim reaches the user", () => {
86
88
  });
87
89
  });
88
90
 
91
+ // A middle answer has words of its own, distinct from the floor's: a runtime that masks only its shell says so.
92
+ it("secrets' middle answer is disclosed apart from its floor", () => {
93
+ const shell = limitationsOf({ ...CEILING, secrets: "shell" });
94
+ const none = limitationsOf({ ...CEILING, secrets: "none" });
95
+ expect(shell).toHaveLength(1);
96
+ expect(none).toHaveLength(1);
97
+ expect(shell).not.toEqual(none);
98
+ });
99
+
89
100
  it("is a real split: neither side is empty", () => {
90
101
  expect(fieldsWhere("enforced").length).toBeGreaterThan(0);
91
102
  expect(fieldsWhere("descriptive").length).toBeGreaterThan(0);
@@ -1,5 +1,5 @@
1
1
  import { PROVIDER_BRAND_PATHS } from "@intentic/constants";
2
- import { accessFor, capabilitiesOf, harnessChoosable, PROVIDER_ACCESS, PROVIDER_VENDOR, PROVIDERS, providerLabel } from "./agent-catalog.js";
2
+ import { accessFor, capabilitiesOf, harnessChoosable, limitationsOf, PROVIDER_ACCESS, PROVIDER_VENDOR, PROVIDERS, providerLabel } from "./agent-catalog.js";
3
3
  import {
4
4
  ACCESS_COST,
5
5
  cliProxyIdOf,
@@ -102,18 +102,21 @@ test("no provider id can be mistaken for an endpoint or a pinned selection", ()
102
102
  }
103
103
  });
104
104
 
105
- test("the harness chip is offered exactly where the two harnesses run different loops", () => {
105
+ test("every provider offers its own default loop, and each offered loop runs a different runtime", () => {
106
106
  for (const spec of PROVIDER_SPECS) {
107
- const native = capabilitiesOf(spec.id, "native");
108
- const claudeCode = capabilitiesOf(spec.id, "claude-code");
109
- expect(harnessChoosable(spec.id), `${spec.id}`).toBe(native.runtime !== claudeCode.runtime);
107
+ const offered = Object.entries(spec.loops.offered);
108
+ expect(Object.keys(spec.loops.offered), `${spec.id}'s default loop is not offered`).toContain(spec.loops.default);
109
+ // Two loops on one runtime would be a chip whose positions run the same thing.
110
+ const runtimes = offered.map(([, offer]) => offer?.capabilities.runtime);
111
+ expect(new Set(runtimes).size, `${spec.id} offers two loops on one runtime`).toBe(runtimes.length);
112
+ expect(harnessChoosable(spec.id), spec.id).toBe(offered.length > 1);
110
113
  }
111
114
  });
112
115
 
113
- test("a minted provider runs the Claude Code loop on both harnesses", () => {
116
+ test("a minted provider runs Claude Code where asked, and Meta's key its own Muse Code by default", () => {
114
117
  for (const provider of MINTED_PROVIDERS) {
115
- expect(capabilitiesOf(provider, "native").runtime, provider).toBe("claude-code");
116
118
  expect(capabilitiesOf(provider, "claude-code").runtime, provider).toBe("claude-code");
119
+ expect(capabilitiesOf(provider, "native").runtime, provider).toBe(provider === "meta" ? "muse" : "claude-code");
117
120
  }
118
121
  });
119
122
 
@@ -136,3 +139,35 @@ test("a list of ids names known providers once each, in its own order", () => {
136
139
  expect(nativeProvidersIn(null)).toEqual([]);
137
140
  expect(nativeProvidersIn(undefined)).toEqual([]);
138
141
  });
142
+
143
+ // Cursor's own runtime: its project commands are read and published by the daemon, and the owner's replacement prompt
144
+ // replaces Cursor's base where the account is entitled to (the fallback for one that is not is the append).
145
+ test("cursor publishes its commands and replaces its base prompt", () => {
146
+ const cursor = capabilitiesOf("cursor", "native");
147
+ expect(cursor.runtime).toBe("cursor");
148
+ expect(cursor.commands).toBe(true);
149
+ expect(cursor.instructions).toBe("replace");
150
+ });
151
+
152
+ // What the hook script and the SDK's own seams carry on Cursor's runtime: per-tool cards in `default` mode, the shell in
153
+ // the terminal panel, its own skill loader, and a tool list nothing gets past (runtimes/cursor/).
154
+ test("cursor holds every mode, a tool list, the terminal panel and its own skills", () => {
155
+ const cursor = capabilitiesOf("cursor", "native");
156
+ expect(cursor.permissions).toBe("modes");
157
+ expect(cursor.terminals).toBe(true);
158
+ expect(cursor.skillDiscovery).toBe("native");
159
+ expect(cursor.toolAllowlist).toBe(true);
160
+ expect(limitationsOf(cursor)).toEqual(["no plugins", "secrets masked in shell output only, not in files the agent reads"]);
161
+ });
162
+
163
+ // Devin runs the ACP floor under its own name: what the floor gained (approvals, questions) it has too, and the effort
164
+ // scale it does not advertise stays off.
165
+ test("devin is the ACP floor under its own name", () => {
166
+ const devin = capabilitiesOf("devin", "native");
167
+ const floor = capabilitiesOf("some-installed-agent", "native");
168
+ expect(devin.runtime).toBe("devin");
169
+ expect({ ...devin, runtime: floor.runtime }).toEqual(floor);
170
+ expect(devin.permissions).toBe("modes");
171
+ expect(devin.questions).toBe(true);
172
+ expect(devin.effort).toBe(false);
173
+ });