@intentic/sandbox-contract 1.334.0 → 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
@@ -14,9 +14,18 @@ import {
14
14
  PROVIDERS,
15
15
  sendableEffort,
16
16
  sendableThinking,
17
+ termsOf,
18
+ loopsOf,
19
+ harnessChoosable,
20
+ loopOf,
21
+ canonicalHarness,
22
+ loopLabel,
23
+ loopDoor,
24
+ loopDoors,
25
+ accountServesLoop,
17
26
  } from "./agent-catalog.js";
18
27
  import type { AgentCapabilities } from "./agent-runtimes.js";
19
- import { type AgentHarness, type AgentProvider, PermissionModeSchema } from "../schemas/agent.js";
28
+ import { type AgentHarness, type AgentProvider, PermissionModeSchema, withRuntimeDefaults } from "../schemas/agent.js";
20
29
 
21
30
  // Every provider × harness pair, generated from the catalog itself, must declare what it can do. A failing pair means a
22
31
  // missing row in capabilitiesOf, never a special case in the caller.
@@ -29,7 +38,9 @@ describe("every provider/harness pair declares what it can do", () => {
29
38
  it.each(pairs)("$provider on the $harness harness", ({ provider, harness }) => {
30
39
  const capabilities = capabilitiesOf(provider, harness);
31
40
 
32
- expect(["claude-code", "codex", "cursor", "opencode", "opencode-gemini", "acp", "devin", "pi"]).toContain(capabilities.runtime);
41
+ expect(["claude-code", "codex", "cursor", "opencode", "opencode-gemini", "acp", "devin", "kimi-cli", "grok-build", "pi", "muse"]).toContain(
42
+ capabilities.runtime,
43
+ );
33
44
  expect(["modes", "plan"]).toContain(capabilities.permissions);
34
45
  expect(["full", "tools", "http", "none"]).toContain(capabilities.mcp);
35
46
  expect(capabilities.execution).toContain("shell");
@@ -39,9 +50,10 @@ describe("every provider/harness pair declares what it can do", () => {
39
50
  expect(["namespace", "cwd"]).toContain(capabilities.isolation);
40
51
  expect(["replace", "append", "none"]).toContain(capabilities.instructions);
41
52
  expect(["hooks", "approval", "refuse-only", "none"]).toContain(capabilities.rulebook);
42
- expect(["masked", "none"]).toContain(capabilities.secrets);
43
- // Masking is a PostToolUse hook and nothing else can edit what a model reads; hooks without masking is the one
44
- // legal direction, so this is an implication, not an equality.
53
+ expect(["masked", "shell", "none"]).toContain(capabilities.secrets);
54
+ // Masking everything the model reads needs a hook in front of every result, which only a hooks-tier loop has;
55
+ // hooks without masking is the one legal direction, so this is an implication, not an equality. The shell-only
56
+ // answer needs no such hook: a wrapper around the command masks what it prints.
45
57
  if (capabilities.secrets === "masked") {
46
58
  expect(capabilities.rulebook).toBe("hooks");
47
59
  }
@@ -57,8 +69,9 @@ test("an unknown provider id is an ACP agent, on either harness", () => {
57
69
  }
58
70
  });
59
71
 
60
- // The `pi` id is reserved for the Pi coding agent's own RPC runtime: real mid-turn steering, an effort scale and a
61
- // published command list, none of which the plain ACP floor carries.
72
+ // The `pi` id is reserved for the Pi coding agent's own RPC runtime: real mid-turn steering, an effort scale, a
73
+ // published command list, a process started per turn with the daemon's instructions, skills and namespace, and the
74
+ // daemon's own extension in front of every tool, none of which the plain ACP floor carries.
62
75
  describe("the pi provider", () => {
63
76
  it("runs the pi runtime on either harness: pi is its own loop", () => {
64
77
  for (const harness of HARNESSES) {
@@ -66,18 +79,49 @@ describe("the pi provider", () => {
66
79
  }
67
80
  });
68
81
 
69
- it("sits above the ACP floor: steering, effort and commands are real; terminals and MCP are not", () => {
82
+ it("sits above the ACP floor: steering, effort, commands and the terminal panel are real", () => {
70
83
  const pi = capabilitiesOf("pi", "native");
71
84
  expect(pi.steering).toBe(true);
72
85
  expect(pi.effort).toBe(true);
73
86
  expect(pi.commands).toBe(true);
74
- expect(pi.mcp).toBe("none");
75
- expect(pi.terminals).toBe(false);
87
+ expect(pi.terminals).toBe(true);
76
88
  const limitations = limitationsOf(pi);
77
89
  expect(limitations.length).toBeGreaterThan(0);
78
- expect(limitations).not.toContain("no mid-turn steering");
79
- expect(limitations).not.toContain("no effort control");
80
- expect(limitations).not.toContain("no slash commands");
90
+ for (const gone of ["no mid-turn steering", "no effort control", "no slash commands", "no terminal panel"]) {
91
+ expect(limitations).not.toContain(gone);
92
+ }
93
+ });
94
+
95
+ // The extension's `tool_call` and `tool_result` hooks, answered on the turn's own socket (runtimes/pi/pi-hooks.ts):
96
+ // the owner's rules and the turn's tool list before a call runs, every result masked after.
97
+ it("holds every call to the owner's rules and the turn's tool list, and masks every result", () => {
98
+ const pi = capabilitiesOf("pi", "native");
99
+ expect(pi.rulebook).toBe("hooks");
100
+ expect(pi.toolAllowlist).toBe(true);
101
+ expect(pi.secrets).toBe("masked");
102
+ const limitations = limitationsOf(pi);
103
+ for (const gone of ["command rules not applied", "secrets reach the model unmasked", "no tool lists: an automation limited to some tools won't run here"]) {
104
+ expect(limitations).not.toContain(gone);
105
+ }
106
+ });
107
+
108
+ // Each started with the process (runtimes/pi/pi-rpc.ts): the instruction flags, `--skill` paths with Pi's own
109
+ // discovery off, the ask tool and MCP servers through the daemon's extension, and nsenter into the turn's namespace.
110
+ it("takes the turn's instructions, skills, questions, MCP servers, the daemon's own tools and namespace", () => {
111
+ const pi = capabilitiesOf("pi", "native");
112
+ expect(pi.instructions).toBe("replace");
113
+ expect(pi.skillDiscovery).toBe("native");
114
+ expect(pi.questions).toBe(true);
115
+ expect(pi.mcp).toBe("tools");
116
+ expect(pi.execution).toEqual(["shell", "js"]);
117
+ expect(pi.isolation).toBe("namespace");
118
+ expect(pi.recovery).toBe(true);
119
+ const limitations = limitationsOf(pi);
120
+ for (const gone of ["no clarifying questions", "worktree by cwd only", "no auto-resume after outage", "system prompt not applied"]) {
121
+ expect(limitations).not.toContain(gone);
122
+ }
123
+ expect(limitations).toContain("no plugins");
124
+ expect(limitations).not.toContain("no code runs, shell only");
81
125
  });
82
126
 
83
127
  it("is an installed capability, so it carries no access requirement to connect", () => {
@@ -85,10 +129,80 @@ describe("the pi provider", () => {
85
129
  });
86
130
  });
87
131
 
88
- test("only codex and grok change runtime with the harness", () => {
89
- const switched = PROVIDERS.filter((provider) => capabilitiesOf(provider.value, "native") !== capabilitiesOf(provider.value, "claude-code"));
132
+ // Exactly today's provider × loop pairs; a new pair is a row in provider-specs.ts and a line here.
133
+ test("each provider offers exactly its loops, its default first", () => {
134
+ expect(Object.fromEntries(PROVIDERS.map((provider) => [provider.value, loopsOf(provider.value)]))).toEqual({
135
+ claude: ["claude-code"],
136
+ codex: ["codex", "claude-code", "opencode"],
137
+ grok: ["opencode", "claude-code", "codex", "vendor"],
138
+ kimi: ["claude-code", "codex", "opencode", "vendor"],
139
+ gemini: ["opencode"],
140
+ cursor: ["vendor"],
141
+ devin: ["vendor"],
142
+ meta: ["muse", "claude-code"],
143
+ zai: ["claude-code", "codex", "opencode"],
144
+ });
145
+ expect(loopsOf("endpoint/ollama")).toEqual(["claude-code", "codex", "opencode", "muse"]);
146
+ // The trial's one model id is the translator's Anthropic route, and its pool cannot replay Muse's tool history: it
147
+ // stays on the loop it discloses itself under.
148
+ expect(loopsOf("endpoint/free-trial")).toEqual(["claude-code"]);
149
+ expect(loopsOf("pi")).toEqual(["pi"]);
150
+ expect(loopsOf("some-installed-agent")).toEqual(["vendor"]);
151
+ });
152
+
153
+ test("every provider with a route a second loop can drive offers a choice of loop", () => {
154
+ expect(PROVIDERS.filter((provider) => harnessChoosable(provider.value)).map((provider) => provider.value)).toEqual(["codex", "grok", "kimi", "meta", "zai"]);
155
+ expect(harnessChoosable("endpoint/ollama")).toBe(true);
156
+ expect(harnessChoosable("endpoint/free-trial")).toBe(false);
157
+ expect(harnessChoosable("some-installed-agent")).toBe(false);
158
+ });
159
+
160
+ describe("resolving a harness to the loop that runs", () => {
161
+ it("reads native as the provider's default loop", () => {
162
+ expect(loopOf("codex", "native")).toBe("codex");
163
+ expect(loopOf("grok", "native")).toBe("opencode");
164
+ expect(loopOf("claude", "native")).toBe("claude-code");
165
+ expect(loopOf("cursor", "native")).toBe("vendor");
166
+ });
167
+
168
+ it("keeps an offered loop and runs the default for one the provider lacks", () => {
169
+ expect(loopOf("codex", "claude-code")).toBe("claude-code");
170
+ expect(loopOf("codex", "codex")).toBe("codex");
171
+ expect(loopOf("cursor", "claude-code")).toBe("vendor");
172
+ expect(loopOf("gemini", "claude-code")).toBe("opencode");
173
+ expect(loopOf("claude", "opencode")).toBe("claude-code");
174
+ });
175
+
176
+ it("spells a turn naming its default loop the same as one naming none", () => {
177
+ expect(canonicalHarness("codex", "codex")).toBe("native");
178
+ expect(canonicalHarness("codex", undefined)).toBe("native");
179
+ expect(canonicalHarness("codex", "claude-code")).toBe("claude-code");
180
+ expect(canonicalHarness("cursor", "claude-code")).toBe("native");
181
+ // Typed as the wire carries it, not as a literal the intersection in the return type would narrow.
182
+ const stamped = (agent: AgentProvider, harness: AgentHarness): AgentHarness => withRuntimeDefaults({ agent, harness }).harness;
183
+ expect(stamped("grok", "opencode")).toBe("native");
184
+ expect(stamped("grok", "claude-code")).toBe("claude-code");
185
+ expect(stamped("devin", "claude-code")).toBe("native");
186
+ });
90
187
 
91
- expect(switched.map((provider) => provider.value)).toEqual(["codex", "grok"]);
188
+ // Only Claude Code turns travel to a runner unasked (runner-scheduler.ts credentialsTravel), and a runner built
189
+ // before the enum widened knows just `native` and `claude-code`: every such turn must be spelled in those.
190
+ it("spells every Claude Code turn in the two values every runner build knows", () => {
191
+ for (const provider of [...PROVIDERS.map((row) => row.value), "endpoint/x", "pi", "some-installed-agent"]) {
192
+ for (const harness of HARNESSES.map((entry) => entry.value)) {
193
+ if (capabilitiesOf(provider, harness).runtime === "claude-code") {
194
+ expect(["native", "claude-code"], `${provider}/${harness}`).toContain(canonicalHarness(provider, harness));
195
+ }
196
+ }
197
+ }
198
+ });
199
+
200
+ it("names each loop, the provider's own agent by the provider", () => {
201
+ expect(loopsOf("codex").map((loop) => loopLabel("codex", loop))).toEqual(["Codex", "Claude Code", "OpenCode"]);
202
+ expect(loopLabel("grok", "opencode")).toBe("OpenCode");
203
+ expect(loopLabel("cursor", "vendor")).toBe("Cursor");
204
+ expect(loopLabel("meta", "muse")).toBe("Muse Code");
205
+ });
92
206
  });
93
207
 
94
208
  test("gemini answers with its own runtime whatever harness is asked for", () => {
@@ -105,41 +219,75 @@ test("codex and grok under the Claude Code harness get the full ceiling, it is t
105
219
  }
106
220
  });
107
221
 
222
+ // Made up rather than borrowed: real runtimes gain modes as their seams are wired, and a test pinned to one stops testing
223
+ // the plan-only answer the day it does.
224
+ const PLAN_ONLY: AgentCapabilities = { ...capabilitiesOf("claude", "native"), permissions: "plan" };
225
+
108
226
  test("a plan-only runtime offers the two postures it has", () => {
109
- expect(modesFor(capabilitiesOf("codex", "native"))).toEqual(["plan", "bypassPermissions"]);
227
+ expect(modesFor(PLAN_ONLY)).toEqual(["plan", "bypassPermissions"]);
110
228
  expect(modesFor(capabilitiesOf("claude", "native"))).toEqual(["default", "plan", "bypassPermissions"]);
229
+ // Codex answers its own approval requests per mode, so it offers the ask-each-tool posture too.
230
+ expect(modesFor(capabilitiesOf("codex", "native"))).toEqual(["default", "plan", "bypassPermissions"]);
111
231
  });
112
232
 
113
233
  test("a mode the runtime can't hold falls back to the one it runs; one it can holds", () => {
114
- const codex = capabilitiesOf("codex", "native");
115
-
116
- expect(clampMode("default", codex)).toBe("bypassPermissions");
117
- expect(clampMode("plan", codex)).toBe("plan");
234
+ expect(clampMode("default", PLAN_ONLY)).toBe("bypassPermissions");
235
+ expect(clampMode("plan", PLAN_ONLY)).toBe("plan");
118
236
  expect(clampMode("default", capabilitiesOf("claude", "native"))).toBe("default");
237
+ expect(clampMode("default", capabilitiesOf("codex", "native"))).toBe("default");
119
238
  });
120
239
 
121
240
  test("the ceiling has nothing to disclose; a floor names what it lacks", () => {
122
241
  expect(limitationsOf(capabilitiesOf("claude", "native"))).toEqual([]);
123
242
 
124
243
  const grokCaps = capabilitiesOf("grok", "native");
125
- expect(grokCaps.permissions).toBe("plan");
126
- expect(grokCaps.steering).toBe(false);
244
+ // OpenCode 2's session API carries steers, per-tool asks, forms, variants, commands and typed failures, the daemon runs
245
+ // its shell (terminal pane, secrets) and serves it the daemon's own tools over the door, and its session rules keep a
246
+ // tool list; what Grok still lacks is the namespace, plugins and masking beyond the shell.
247
+ expect(grokCaps.permissions).toBe("modes");
248
+ expect(grokCaps.steering).toBe(true);
249
+ expect(grokCaps.questions).toBe(true);
250
+ expect(grokCaps.effort).toBe(true);
251
+ expect(grokCaps.commands).toBe(true);
252
+ expect(grokCaps.recovery).toBe(true);
127
253
  expect(grokCaps.isolation).toBe("cwd");
254
+ expect(grokCaps.terminals).toBe(true);
255
+ expect(grokCaps.toolAllowlist).toBe(true);
256
+ expect(grokCaps.execution).toEqual(["shell", "js"]);
128
257
  expect(limitationsOf(grokCaps).length).toBeGreaterThan(0);
129
- // The turn's remote servers, as Codex takes them: mounted on the shared OpenCode server per conversation.
130
- expect(grokCaps.mcp).toBe("http");
131
- expect(limitationsOf(grokCaps)).toContain("MCP servers only, none of the daemon's own tools, no plugins");
132
-
258
+ expect(limitationsOf(grokCaps)).not.toContain("no mid-turn steering");
259
+ expect(limitationsOf(grokCaps)).not.toContain("no per-tool approvals");
260
+ expect(limitationsOf(grokCaps)).not.toContain("no auto-resume after outage");
261
+ // The turn's remote servers and the daemon's own, through the door, mounted on the shared OpenCode server per
262
+ // conversation.
263
+ expect(grokCaps.mcp).toBe("tools");
264
+ expect(limitationsOf(grokCaps)).toContain("no plugins");
265
+ expect(limitationsOf(grokCaps)).toEqual(
266
+ expect.not.arrayContaining(["no terminal panel", "no code runs, shell only", "no tool lists: an automation limited to some tools won't run here"]),
267
+ );
268
+
269
+ // The daemon's own servers reach every ACP agent through the door, over http or the daemon's stdio proxy.
133
270
  const acpCaps = capabilitiesOf("some-installed-agent", "native");
134
- expect(acpCaps.mcp).toBe("http");
271
+ expect(acpCaps.mcp).toBe("tools");
272
+ expect(acpCaps.execution).toEqual(["shell", "js"]);
135
273
  const acp = limitationsOf(acpCaps);
136
- expect(acp.some((line) => line.includes("MCP"))).toBe(true);
274
+ expect(acp).toContain("no plugins");
275
+ expect(acp).not.toContain("no code runs, shell only");
137
276
  expect(acpCaps.commands).toBe(true);
138
277
  expect(acpCaps.terminals).toBe(true);
139
-
278
+ // Per-tool approvals and questions ride the protocol's own seams (request_permission, form elicitation); effort,
279
+ // isolation and recovery stay off, since the record is every agent's and the pooled process is every conversation's.
280
+ expect(acpCaps.permissions).toBe("modes");
281
+ expect(acpCaps.questions).toBe(true);
282
+ expect(acp).not.toContain("no per-tool approvals");
283
+ expect(acp).not.toContain("no clarifying questions");
284
+ expect(acp).toEqual(expect.arrayContaining(["no effort control", "worktree by cwd only", "no auto-resume after outage"]));
285
+
286
+ // Codex takes the daemon's own servers too, over the door, so only plugins are missing.
140
287
  const codexCaps = capabilitiesOf("codex", "native");
141
- expect(codexCaps.mcp).toBe("http");
142
- expect(limitationsOf(codexCaps)).toContain("MCP servers only, none of the daemon's own tools, no plugins");
288
+ expect(codexCaps.mcp).toBe("tools");
289
+ expect(limitationsOf(codexCaps)).toContain("no plugins");
290
+ expect(limitationsOf(codexCaps)).not.toContain("MCP servers only, none of the daemon's own tools, no plugins");
143
291
  expect(limitationsOf(codexCaps).length).toBeLessThan(limitationsOf({ ...grokCaps, mcp: "none" }).length + 5);
144
292
  });
145
293
 
@@ -181,8 +329,12 @@ test("the safety axes disclose the middle answer differently from the floor", ()
181
329
  expect(claudeCaps.rulebook).toBe("hooks");
182
330
  expect(claudeCaps.secrets).toBe("masked");
183
331
 
184
- expect(codexCaps.rulebook).toBe("approval");
185
- expect(piCaps.rulebook).toBe("none");
332
+ // Codex's own per-thread hooks put every call to the gate before it runs, so its rules hold any call, as Claude's do.
333
+ expect(codexCaps.rulebook).toBe("hooks");
334
+ expect(limitationsOf(codexCaps).join(" ")).not.toContain("command rules");
335
+ // Pi's own extension puts every call to the gate before it runs and masks every result after (runtimes/pi/pi-hooks.ts).
336
+ expect(piCaps.rulebook).toBe("hooks");
337
+ expect(piCaps.secrets).toBe("masked");
186
338
  expect(grokCaps.rulebook).toBe("approval");
187
339
  // A hold parks on a card now that the turn's watchdog is held while it waits, so nothing says it cannot hold.
188
340
  expect(limitationsOf(grokCaps)).toContain("command rules apply only to calls this agent raises");
@@ -193,31 +345,67 @@ test("the safety axes disclose the middle answer differently from the floor", ()
193
345
 
194
346
  expect(limitationsOf(codexCaps).length).toBeGreaterThan(limitationsOf(claudeCaps).length);
195
347
  expect(limitationsOf(piCaps).length).toBeGreaterThan(limitationsOf(claudeCaps).length);
348
+ expect(limitationsOf(piCaps).join(" ")).not.toContain("command rules");
196
349
  expect(limitationsOf(grokCaps).join(" ")).toBe(limitationsOf(capabilitiesOf("gemini", "native")).join(" "));
197
350
 
198
- for (const provider of ["codex", "grok", "gemini", "pi", "some-installed-agent"] as const) {
351
+ // The middle answer: references resolved and output masked in the shell, but not in what the loop's own file tools read.
352
+ // Codex masks through its hooks' wrapper, OpenCode through the daemon's shell script.
353
+ for (const caps of [codexCaps, grokCaps]) {
354
+ expect(caps.secrets).toBe("shell");
355
+ expect(limitationsOf(caps)).toContain("secrets masked in shell output only, not in files the agent reads");
356
+ expect(limitationsOf(caps)).not.toContain("secrets reach the model unmasked");
357
+ }
358
+
359
+ for (const provider of ["devin", "some-installed-agent"] as const) {
199
360
  expect(capabilitiesOf(provider, "native").secrets).toBe("none");
200
361
  }
201
362
  });
202
363
 
364
+ // Cursor's shell runs through the daemon's hook script, which resolves a reference and masks what a command prints; its
365
+ // read tool can only be refused, so the picker says the masking stops at the shell rather than claiming the ceiling's.
366
+ test("secrets on Cursor's runtime are disclosed as reaching the shell only", () => {
367
+ const cursorCaps = capabilitiesOf("cursor", "native");
368
+ expect(cursorCaps.secrets).toBe("shell");
369
+ const limitations = limitationsOf(cursorCaps);
370
+ expect(limitations).toContain("secrets masked in shell output only, not in files the agent reads");
371
+ expect(limitations).not.toContain("secrets reach the model unmasked");
372
+ });
373
+
203
374
  test("the instruction axis discloses its two weaker answers, differently", () => {
204
- const grokCaps = capabilitiesOf("grok", "native");
375
+ // No runtime appends any more (each found a seam to replace the base), so the append answer is made up.
376
+ const appending: AgentCapabilities = { ...capabilitiesOf("claude", "native"), instructions: "append" };
205
377
  const acpCaps = capabilitiesOf("some-installed-agent", "native");
206
378
  const codexCaps = capabilitiesOf("codex", "native");
207
379
 
208
- expect(grokCaps.instructions).toBe("append");
209
380
  expect(acpCaps.instructions).toBe("none");
210
381
  expect(codexCaps.instructions).toBe("replace");
382
+ // Grok's owner prompt runs on OpenCode's blank-base agents, so it replaces the base as Codex's does.
383
+ expect(capabilitiesOf("grok", "native").instructions).toBe("replace");
211
384
 
212
- expect(limitationsOf(grokCaps).length).toBeGreaterThan(limitationsOf(codexCaps).length);
213
- expect(limitationsOf(acpCaps).length).toBeGreaterThan(limitationsOf(codexCaps).length);
385
+ expect(limitationsOf(appending)).toEqual(["system prompt appended, not replaced"]);
386
+ expect(limitationsOf(acpCaps)).toContain("system prompt not applied");
387
+ expect(limitationsOf(codexCaps).join(" ")).not.toContain("system prompt");
214
388
  });
215
389
 
216
- // The backend is one of the daemon's own functions, so it takes a runtime that can call one directly AND a gate the
217
- // daemon controls: Claude Code has the SDK server plus its PreToolUse matcher, Cursor has customTools plus a consult
218
- // inside the handler. Every other runtime is a child process with neither, where a script would run unread.
219
- test("the js execution backend rides the two runtimes that can host the daemon's own tools", () => {
220
- const hosts: ReadonlySet<string> = new Set(["claude-code", "cursor"]);
390
+ // The backend is one of the daemon's own functions, so it takes a runtime that can call one AND a gate the daemon
391
+ // controls: Claude Code has the SDK server plus its PreToolUse matcher, Cursor has customTools plus a consult inside the
392
+ // handler, and Codex, OpenCode (Grok, Gemini), an ACP agent, Devin, the Kimi and Grok CLIs, Pi and Muse reach the door's
393
+ // `code` server, whose every script is consulted against the gate its loop attaches to the turn's relay. Every other
394
+ // runtime is a child process with neither, where a script would run unread.
395
+ test("the js execution backend rides the runtimes that can host the daemon's own tools", () => {
396
+ const hosts: ReadonlySet<string> = new Set([
397
+ "claude-code",
398
+ "cursor",
399
+ "codex",
400
+ "opencode",
401
+ "opencode-gemini",
402
+ "acp",
403
+ "devin",
404
+ "kimi-cli",
405
+ "grok-build",
406
+ "pi",
407
+ "muse",
408
+ ]);
221
409
  for (const { provider, harness } of pairs) {
222
410
  const capabilities = capabilitiesOf(provider, harness);
223
411
  expect(capabilities.execution.includes("js")).toBe(hosts.has(capabilities.runtime));
@@ -259,14 +447,22 @@ describe("the max-effort rule", () => {
259
447
  });
260
448
  });
261
449
 
262
- // An `endpoint/<id>` provider is a user's own model API, told apart from an ACP agent only by its id: it runs the
263
- // Claude Code loop at full ceiling, never the ACP floor.
450
+ // An `endpoint/<id>` provider is a user's own model API, told apart from an ACP agent only by its id: by default it runs
451
+ // the Claude Code loop at full ceiling, never the ACP floor, and Codex and OpenCode as the owner picks them.
264
452
  describe("a configured model endpoint", () => {
265
- it("runs the Claude Code loop at full ceiling, on either harness", () => {
453
+ it("runs the Claude Code loop at full ceiling by default, and Codex, OpenCode or Muse Code where asked", () => {
266
454
  for (const harness of HARNESSES.map((entry) => entry.value)) {
267
- expect(capabilitiesOf("endpoint/ollama", harness)).toEqual(capabilitiesOf("claude", harness));
455
+ const hosted =
456
+ harness === "codex" || harness === "opencode"
457
+ ? capabilitiesOf("grok", harness)
458
+ : harness === "muse"
459
+ ? capabilitiesOf("meta", "muse")
460
+ : capabilitiesOf("claude", harness);
461
+ expect(capabilitiesOf("endpoint/ollama", harness), harness).toEqual(hosted);
268
462
  }
269
463
  expect(limitationsOf(capabilitiesOf("endpoint/ollama", "native"))).toEqual([]);
464
+ expect(capabilitiesOf("endpoint/ollama", "muse").runtime).toBe("muse");
465
+ expect(capabilitiesOf("endpoint/free-trial", "muse").runtime).toBe("claude-code");
270
466
  });
271
467
 
272
468
  it("is told apart from an ACP agent by its id alone, so no manifest lookup is needed to read the record", () => {
@@ -284,7 +480,7 @@ describe("a configured model endpoint", () => {
284
480
  });
285
481
  });
286
482
 
287
- // Fast speed needs all three conditions at once; a translator-routed provider or a Claude model with no `fast` badge
483
+ // Fast speed needs every condition at once; a translator-routed provider or a Claude model with no `fast` badge
288
484
  // fails silently otherwise: the turn runs at standard speed and only the bill shows it.
289
485
  describe("offering fast speed", () => {
290
486
  it("is offered for a Claude model that publishes the badge", () => {
@@ -302,10 +498,14 @@ describe("offering fast speed", () => {
302
498
  expect(fastAllowed(capabilitiesOf("claude", "native"), "claude", undefined)).toBe(false);
303
499
  });
304
500
 
305
- it("is refused by every runtime that isn't the Claude Code loop", () => {
306
- for (const provider of ["codex", "grok"] as const) {
307
- expect(fastAllowed(capabilitiesOf(provider, "native"), provider, ["fast"])).toBe(false);
308
- }
501
+ // Codex asks its own vendor for the tier its own catalog published, so there is no route to second-guess.
502
+ it("is offered by Codex for a model that publishes the badge, and only for one", () => {
503
+ expect(fastAllowed(capabilitiesOf("codex", "native"), "codex", ["fast"])).toBe(true);
504
+ expect(fastAllowed(capabilitiesOf("codex", "native"), "codex", undefined)).toBe(false);
505
+ });
506
+
507
+ it("is refused by every runtime that cannot ask for it", () => {
508
+ expect(fastAllowed(capabilitiesOf("grok", "native"), "grok", ["fast"])).toBe(false);
309
509
  expect(fastAllowed(capabilitiesOf("some-installed-agent", "native"), "some-installed-agent", ["fast"])).toBe(false);
310
510
  });
311
511
  });
@@ -329,3 +529,67 @@ describe("the free providers", () => {
329
529
  }
330
530
  });
331
531
  });
532
+
533
+ // A vendor's own terms ride beside the loop they speak to, said quietly (an icon's hover card). Resolved as the turn is:
534
+ // a loop the provider does not offer reads as the one it runs, so the note follows what actually runs.
535
+ describe("the vendor's terms for a loop", () => {
536
+ it("marks the routes a vendor forbids, whichever loop is asked for", () => {
537
+ expect(termsOf("gemini", "native")?.status).toBe("against-terms");
538
+ expect(termsOf("gemini", "claude-code")).toEqual(termsOf("gemini", "native"));
539
+ expect(termsOf("meta", "claude-code")?.status).toBe("against-terms");
540
+ });
541
+
542
+ it("says nothing for Meta's key in Muse Code, the use Meta's terms sanction", () => {
543
+ expect(termsOf("meta", "native")).toBeUndefined();
544
+ expect(termsOf("meta", "muse")).toBeUndefined();
545
+ expect(termsOf("endpoint/local", "muse")).toBeUndefined();
546
+ });
547
+
548
+ it("marks a loop the vendor does not list, and only that loop", () => {
549
+ expect(termsOf("codex", "claude-code")?.status).toBe("unlisted");
550
+ expect(termsOf("codex", "native")).toBeUndefined();
551
+ expect(termsOf("grok", "claude-code")?.status).toBe("unlisted");
552
+ expect(termsOf("grok", "native")).toBeUndefined();
553
+ });
554
+
555
+ it("says nothing where the vendor sanctions the loop, or for a provider it knows nothing about", () => {
556
+ for (const provider of ["claude", "kimi", "zai", "cursor", "devin", "endpoint/local", "some-installed-agent"]) {
557
+ expect(termsOf(provider, "native"), provider).toBeUndefined();
558
+ }
559
+ });
560
+
561
+ it("always says where the vendor says it", () => {
562
+ for (const provider of PROVIDERS) {
563
+ for (const harness of HARNESSES) {
564
+ const terms = termsOf(provider.value, harness.value);
565
+ if (terms !== undefined) {
566
+ expect(terms.source).toMatch(/^https:\/\//);
567
+ expect(terms.note).toMatch(/\S/);
568
+ }
569
+ }
570
+ }
571
+ });
572
+ });
573
+
574
+ // A vendor CLI's loop names the program, since the provider's own name is on its other loops too; every other loop keeps
575
+ // its own name, and an installed agent's `vendor` loop is still the agent's.
576
+ test("the vendor loop of Grok and Kimi is named for the CLI that runs it", () => {
577
+ expect(loopLabel("grok", "vendor")).toBe("Grok Build");
578
+ expect(loopLabel("kimi", "vendor")).toBe("Kimi CLI");
579
+ expect(loopLabel("grok", "opencode")).toBe("OpenCode");
580
+ expect(loopLabel("devin", "vendor")).toBe("Devin");
581
+ });
582
+
583
+ // The CLI loops sign in through a door variant of their own; the other loops spend the provider's default credential, so
584
+ // one account list serves both and the variant says which loop a row belongs to.
585
+ test("an account serves the loop whose door its variant names", () => {
586
+ expect(loopDoor("grok", "vendor")).toBe("grok-build");
587
+ expect(loopDoor("grok", "native")).toBeUndefined();
588
+ expect(loopDoors("kimi")).toEqual([{ loop: "vendor", door: "kimi-cli" }]);
589
+ expect(accountServesLoop("grok", "vendor", { variant: "grok-build" })).toBe(true);
590
+ expect(accountServesLoop("grok", "native", { variant: "grok-build" })).toBe(false);
591
+ expect(accountServesLoop("grok", "opencode", {})).toBe(true);
592
+ expect(accountServesLoop("grok", "vendor", {})).toBe(false);
593
+ // Z.ai's variants are estates, not doors: every account serves its one loop.
594
+ expect(accountServesLoop("zai", "native", { variant: "bigmodel" })).toBe(true);
595
+ });