@intentic/sandbox-contract 1.225.0 → 1.226.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/dist/agent-catalog.d.ts +2 -0
  2. package/dist/agent-catalog.d.ts.map +1 -1
  3. package/dist/agent-catalog.js +16 -0
  4. package/dist/agent-catalog.js.map +1 -1
  5. package/dist/contracts/activity.contract.d.ts.map +1 -1
  6. package/dist/contracts/activity.contract.js +17 -2
  7. package/dist/contracts/activity.contract.js.map +1 -1
  8. package/dist/contracts/agent.contract.d.ts.map +1 -1
  9. package/dist/contracts/agent.contract.js +71 -8
  10. package/dist/contracts/agent.contract.js.map +1 -1
  11. package/dist/contracts/agents.contract.d.ts +1 -0
  12. package/dist/contracts/agents.contract.d.ts.map +1 -1
  13. package/dist/contracts/agents.contract.js +164 -19
  14. package/dist/contracts/agents.contract.js.map +1 -1
  15. package/dist/contracts/automations.contract.d.ts.map +1 -1
  16. package/dist/contracts/automations.contract.js +78 -9
  17. package/dist/contracts/automations.contract.js.map +1 -1
  18. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  19. package/dist/contracts/capabilities.contract.js +98 -11
  20. package/dist/contracts/capabilities.contract.js.map +1 -1
  21. package/dist/contracts/chores.contract.d.ts.map +1 -1
  22. package/dist/contracts/chores.contract.js +26 -3
  23. package/dist/contracts/chores.contract.js.map +1 -1
  24. package/dist/contracts/ci.contract.d.ts.map +1 -1
  25. package/dist/contracts/ci.contract.js +52 -6
  26. package/dist/contracts/ci.contract.js.map +1 -1
  27. package/dist/contracts/claude.contract.d.ts.map +1 -1
  28. package/dist/contracts/claude.contract.js +44 -5
  29. package/dist/contracts/claude.contract.js.map +1 -1
  30. package/dist/contracts/drafts.contract.d.ts.map +1 -1
  31. package/dist/contracts/drafts.contract.js +26 -3
  32. package/dist/contracts/drafts.contract.js.map +1 -1
  33. package/dist/contracts/endpoints.contract.d.ts.map +1 -1
  34. package/dist/contracts/endpoints.contract.js +17 -2
  35. package/dist/contracts/endpoints.contract.js.map +1 -1
  36. package/dist/contracts/exit.contract.d.ts.map +1 -1
  37. package/dist/contracts/exit.contract.js +62 -7
  38. package/dist/contracts/exit.contract.js.map +1 -1
  39. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  40. package/dist/contracts/extensions.contract.js +124 -15
  41. package/dist/contracts/extensions.contract.js.map +1 -1
  42. package/dist/contracts/git.contract.d.ts.map +1 -1
  43. package/dist/contracts/git.contract.js +384 -43
  44. package/dist/contracts/git.contract.js.map +1 -1
  45. package/dist/contracts/grok.contract.d.ts.map +1 -1
  46. package/dist/contracts/grok.contract.js +25 -3
  47. package/dist/contracts/grok.contract.js.map +1 -1
  48. package/dist/contracts/history.contract.d.ts.map +1 -1
  49. package/dist/contracts/history.contract.js +35 -4
  50. package/dist/contracts/history.contract.js.map +1 -1
  51. package/dist/contracts/intentic.contract.d.ts.map +1 -1
  52. package/dist/contracts/intentic.contract.js +25 -3
  53. package/dist/contracts/intentic.contract.js.map +1 -1
  54. package/dist/contracts/inventory.contract.d.ts.map +1 -1
  55. package/dist/contracts/inventory.contract.js +26 -3
  56. package/dist/contracts/inventory.contract.js.map +1 -1
  57. package/dist/contracts/logs.contract.d.ts.map +1 -1
  58. package/dist/contracts/logs.contract.js +17 -2
  59. package/dist/contracts/logs.contract.js.map +1 -1
  60. package/dist/contracts/loops.contract.d.ts.map +1 -1
  61. package/dist/contracts/loops.contract.js +52 -6
  62. package/dist/contracts/loops.contract.js.map +1 -1
  63. package/dist/contracts/panels.contract.d.ts.map +1 -1
  64. package/dist/contracts/panels.contract.js +26 -3
  65. package/dist/contracts/panels.contract.js.map +1 -1
  66. package/dist/contracts/personas.contract.d.ts.map +1 -1
  67. package/dist/contracts/personas.contract.js +71 -8
  68. package/dist/contracts/personas.contract.js.map +1 -1
  69. package/dist/contracts/ports.contract.d.ts.map +1 -1
  70. package/dist/contracts/ports.contract.js +26 -3
  71. package/dist/contracts/ports.contract.js.map +1 -1
  72. package/dist/contracts/prepush.contract.d.ts.map +1 -1
  73. package/dist/contracts/prepush.contract.js +24 -3
  74. package/dist/contracts/prepush.contract.js.map +1 -1
  75. package/dist/contracts/providers.contract.d.ts.map +1 -1
  76. package/dist/contracts/providers.contract.js +9 -1
  77. package/dist/contracts/providers.contract.js.map +1 -1
  78. package/dist/contracts/public.contract.d.ts.map +1 -1
  79. package/dist/contracts/public.contract.js +26 -3
  80. package/dist/contracts/public.contract.js.map +1 -1
  81. package/dist/contracts/push.contract.d.ts.map +1 -1
  82. package/dist/contracts/push.contract.js +35 -4
  83. package/dist/contracts/push.contract.js.map +1 -1
  84. package/dist/contracts/secrets.contract.d.ts.map +1 -1
  85. package/dist/contracts/secrets.contract.js +43 -5
  86. package/dist/contracts/secrets.contract.js.map +1 -1
  87. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  88. package/dist/contracts/sessions.contract.js +15 -2
  89. package/dist/contracts/sessions.contract.js.map +1 -1
  90. package/dist/contracts/settings.contract.d.ts.map +1 -1
  91. package/dist/contracts/settings.contract.js +43 -5
  92. package/dist/contracts/settings.contract.js.map +1 -1
  93. package/dist/contracts/share.contract.d.ts.map +1 -1
  94. package/dist/contracts/share.contract.js +35 -4
  95. package/dist/contracts/share.contract.js.map +1 -1
  96. package/dist/contracts/skills.contract.d.ts.map +1 -1
  97. package/dist/contracts/skills.contract.js +35 -4
  98. package/dist/contracts/skills.contract.js.map +1 -1
  99. package/dist/contracts/system.contract.d.ts.map +1 -1
  100. package/dist/contracts/system.contract.js +107 -14
  101. package/dist/contracts/system.contract.js.map +1 -1
  102. package/dist/contracts/translator.contract.d.ts.map +1 -1
  103. package/dist/contracts/translator.contract.js +29 -4
  104. package/dist/contracts/translator.contract.js.map +1 -1
  105. package/dist/contracts/usage.contract.d.ts.map +1 -1
  106. package/dist/contracts/usage.contract.js +9 -1
  107. package/dist/contracts/usage.contract.js.map +1 -1
  108. package/dist/contracts/vpn.contract.d.ts.map +1 -1
  109. package/dist/contracts/vpn.contract.js +32 -4
  110. package/dist/contracts/vpn.contract.js.map +1 -1
  111. package/dist/contracts/workflows.contract.d.ts.map +1 -1
  112. package/dist/contracts/workflows.contract.js +70 -8
  113. package/dist/contracts/workflows.contract.js.map +1 -1
  114. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  115. package/dist/contracts/workspace.contract.js +216 -25
  116. package/dist/contracts/workspace.contract.js.map +1 -1
  117. package/dist/events.d.ts.map +1 -1
  118. package/dist/events.js +179 -88
  119. package/dist/events.js.map +1 -1
  120. package/dist/history-state.d.ts.map +1 -1
  121. package/dist/history-state.js +1 -0
  122. package/dist/history-state.js.map +1 -1
  123. package/dist/index.d.ts +1 -0
  124. package/dist/index.d.ts.map +1 -1
  125. package/dist/schemas.d.ts +1 -0
  126. package/dist/schemas.d.ts.map +1 -1
  127. package/dist/schemas.js +2856 -1276
  128. package/dist/schemas.js.map +1 -1
  129. package/package.json +4 -4
  130. package/src/agent-catalog.test.ts +48 -3
  131. package/src/agent-catalog.ts +88 -0
  132. package/src/capability-ledger.test.ts +21 -2
  133. package/src/contracts/activity.contract.ts +18 -2
  134. package/src/contracts/agent.contract.ts +78 -8
  135. package/src/contracts/agents.contract.ts +180 -19
  136. package/src/contracts/automations.contract.ts +80 -9
  137. package/src/contracts/capabilities.contract.ts +106 -11
  138. package/src/contracts/chores.contract.ts +29 -3
  139. package/src/contracts/ci.contract.ts +55 -6
  140. package/src/contracts/claude.contract.ts +47 -5
  141. package/src/contracts/drafts.contract.ts +26 -3
  142. package/src/contracts/endpoints.contract.ts +19 -2
  143. package/src/contracts/exit.contract.ts +69 -7
  144. package/src/contracts/extensions.contract.ts +134 -15
  145. package/src/contracts/git.contract.ts +421 -43
  146. package/src/contracts/grok.contract.ts +26 -3
  147. package/src/contracts/history.contract.ts +37 -4
  148. package/src/contracts/intentic.contract.ts +28 -3
  149. package/src/contracts/inventory.contract.ts +27 -3
  150. package/src/contracts/logs.contract.ts +18 -2
  151. package/src/contracts/loops.contract.ts +58 -6
  152. package/src/contracts/panels.contract.ts +26 -3
  153. package/src/contracts/personas.contract.ts +77 -8
  154. package/src/contracts/ports.contract.ts +27 -3
  155. package/src/contracts/prepush.contract.ts +26 -3
  156. package/src/contracts/providers.contract.ts +10 -1
  157. package/src/contracts/public.contract.ts +29 -3
  158. package/src/contracts/push.contract.ts +37 -4
  159. package/src/contracts/secrets.contract.ts +46 -5
  160. package/src/contracts/sessions.contract.ts +16 -2
  161. package/src/contracts/settings.contract.ts +46 -5
  162. package/src/contracts/share.contract.ts +37 -4
  163. package/src/contracts/skills.contract.ts +39 -4
  164. package/src/contracts/system.contract.ts +119 -14
  165. package/src/contracts/translator.contract.ts +32 -4
  166. package/src/contracts/usage.contract.ts +10 -1
  167. package/src/contracts/vpn.contract.ts +35 -4
  168. package/src/contracts/workflows.contract.ts +77 -8
  169. package/src/contracts/workspace.contract.ts +231 -25
  170. package/src/events.ts +207 -88
  171. package/src/history-state.ts +6 -0
  172. package/src/schemas.ts +3506 -1260
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentic/sandbox-contract",
3
- "version": "1.225.0",
3
+ "version": "1.226.1",
4
4
  "description": "oRPC wire contract for the intentic sandbox daemon, shared by the daemon and its browser client",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -89,9 +89,9 @@
89
89
  "@orpc/contract": "1.14.13",
90
90
  "tslib": "2.8.1",
91
91
  "zod": "4.4.3",
92
- "@intentic/constants": "1.225.0",
93
- "@intentic/registry": "1.225.0",
94
- "@intentic/extension-manifest": "1.225.0"
92
+ "@intentic/constants": "1.226.1",
93
+ "@intentic/extension-manifest": "1.226.1",
94
+ "@intentic/registry": "1.226.1"
95
95
  },
96
96
  "devDependencies": {
97
97
  "@types/node": "24.13.2",
@@ -49,6 +49,15 @@ describe("every provider/harness pair declares what it can do", () => {
49
49
  }
50
50
  expect(["namespace", "cwd"]).toContain(capabilities.isolation);
51
51
  expect(["replace", "append", "none"]).toContain(capabilities.instructions);
52
+ expect(["hooks", "approval", "refuse-only", "none"]).toContain(capabilities.rulebook);
53
+ expect(["masked", "none"]).toContain(capabilities.secrets);
54
+ /* Masking is a PostToolUse hook and nothing else can edit what a model reads, so a record claiming
55
+ * masked without hooks would be claiming a seam that does not exist (see the axis doc). The one
56
+ * direction that IS legal is hooks without masking, which is why this is an implication and not
57
+ * equality. */
58
+ if (capabilities.secrets === "masked") {
59
+ expect(capabilities.rulebook).toBe("hooks");
60
+ }
52
61
  // The permission modes offered must include the mode a clamp falls back to: a floor that isn't in the
53
62
  // list would leave the composer showing a posture the runtime can't hold.
54
63
  expect(modesFor(capabilities)).toContain(clampMode("default", capabilities));
@@ -176,16 +185,52 @@ test("every axis a record can lack has words for it", () => {
176
185
  terminals: false,
177
186
  recovery: false,
178
187
  instructions: "none",
188
+ rulebook: "none",
189
+ secrets: "none",
179
190
  };
180
191
 
181
- // Eleven DISCLOSABLE axes, eleven sentences: an axis added to the interface without one would silently
182
- // never be disclosed. fastMode is the deliberate twelfth: a record alone can't tell the truth about it (a
192
+ // Thirteen DISCLOSABLE axes, thirteen sentences: an axis added to the interface without one would silently
193
+ // never be disclosed. fastMode is the deliberate exception: a record alone can't tell the truth about it (a
183
194
  // translator-routed turn reads true here and still can't go fast), so it is answered by fastAllowed
184
195
  // instead. Anything else added to the interface has to move this number.
185
- expect(limitationsOf(nothing)).toHaveLength(11);
196
+ expect(limitationsOf(nothing)).toHaveLength(13);
186
197
  expect(limitationsOf(nothing).join(" ")).not.toContain("fast");
187
198
  });
188
199
 
200
+ /* The two safety axes, checked the same way the instruction axis is below and for the same reason: `rulebook`
201
+ * has a middle value, and disclosing the floor's words for it would tell a Codex user their rules are ignored
202
+ * when they are in fact being applied to everything Codex asks about. */
203
+ test("the safety axes disclose the middle answer differently from the floor", () => {
204
+ const claude = limitationsOf(capabilitiesOf("claude", "native")).join(" ");
205
+ const codex = limitationsOf(capabilitiesOf("codex", "native")).join(" ");
206
+ const pi = limitationsOf(capabilitiesOf("pi", "native")).join(" ");
207
+
208
+ // The ceiling says nothing about either axis.
209
+ expect(claude).not.toContain("command rules");
210
+ expect(claude).not.toContain("stored secrets");
211
+
212
+ // The middle: rules DO apply, to what the vendor raises. Never the floor's flat "aren't applied".
213
+ expect(codex).toContain("only to calls this agent asks about");
214
+ expect(codex).not.toContain("aren't applied");
215
+
216
+ // The floor: no seam at all, said plainly.
217
+ expect(pi).toContain("your command rules aren't applied");
218
+
219
+ /* The third answer, which exists because OpenCode's watchdog aborts a turn that pauses. Its sentence must
220
+ * say the rules DO bite (unlike Pi's) and that a hold cannot ask (unlike Codex's). */
221
+ const grok = limitationsOf(capabilitiesOf("grok", "native")).join(" ");
222
+ expect(grok).toContain("a rule set to hold refuses instead");
223
+ expect(grok).not.toContain("aren't applied");
224
+ expect(grok).not.toContain("only to calls this agent asks about");
225
+ // Gemini rides the same loop, so it must read the same way.
226
+ expect(limitationsOf(capabilitiesOf("gemini", "native")).join(" ")).toBe(grok);
227
+
228
+ // Masking is binary and structural, so every non-Claude runtime says the same thing.
229
+ for (const provider of ["codex", "grok", "gemini", "pi", "some-installed-agent"] as const) {
230
+ expect(limitationsOf(capabilitiesOf(provider, "native")).join(" ")).toContain("stored secrets reach the model unmasked");
231
+ }
232
+ });
233
+
189
234
  /* The instruction axis has THREE values and only two of them are worth a sentence, which is the one shape the
190
235
  * count above cannot check: a middle value that discloses the same words as the floor would tell a Grok user
191
236
  * their prompt is ignored when it is in fact being sent. */
@@ -286,6 +286,49 @@ export interface AgentCapabilities {
286
286
  * only the Claude Code loop's: Codex's own base describes Codex's own tools, so swapping it for a prompt
287
287
  * written about another harness is the owner's deliberate act (their custom text), never ours. */
288
288
  readonly instructions: "replace" | "append" | "none";
289
+ /* WHETHER THE OWNER'S COMMAND RULEBOOK REACHES THIS RUNTIME (SandboxSettings.commandRules, decided by
290
+ * guard/actions.ts commandRun, delivered by guard/command-gate.ts).
291
+ *
292
+ * It exists because the rulebook was silently a Claude Code rulebook. The gate is a PreToolUse hook, which
293
+ * is an Agent SDK seam, so an owner who set `files.destructive: hold` was asked on a Claude turn and never
294
+ * on a Codex, Grok, Gemini, Pi or ACP one, with nothing on screen saying so. Same failure mode as the
295
+ * `instructions` axis above, and the same fix: name it once, let every surface read it.
296
+ *
297
+ * "hooks" , the runtime's own pre-execution hook carries the verdict and a HOLD can park the call.
298
+ * The Claude Code loop, whose PreToolUse hook fires even under bypassPermissions.
299
+ * "approval" , the vendor publishes a per-call approval channel the daemon answers from the same
300
+ * rulebook, and a hold parks on a card because the vendor is blocked on the answer
301
+ * (Codex's `item/commandExecution/requestApproval`, ACP's `session/request_permission`).
302
+ * Weaker than "hooks" in one stated way: the vendor decides WHICH calls it asks about, so a
303
+ * class it never raises is a class the rulebook cannot see. What it does raise is judged by
304
+ * the same decide fn.
305
+ * "refuse-only", the same channel, but the vendor puts a CLOCK on the wait, so a hold cannot park and
306
+ * arrives as a refusal instead. OpenCode's turn has an inactivity watchdog that reads a
307
+ * paused approval as a stalled turn; a card there would break the turn rather than gate it.
308
+ * `deny` rules work fully; `hold` rules stop the command and say they could not ask.
309
+ * "none" , the runtime publishes no seam before it runs a command, so no rule can apply. Pi runs its
310
+ * bash in-process with no approval channel at all.
311
+ *
312
+ * The taint floor rides this axis too: a runtime with no consult has no place to apply it, which is why
313
+ * `conversationTainted` must read a "none" runtime as tainted rather than as clean (guard/turn-taint.ts). */
314
+ readonly rulebook: "hooks" | "approval" | "refuse-only" | "none";
315
+ /* WHETHER A STORED CREDENTIAL IS MASKED IN WHAT THIS RUNTIME'S MODEL READS (secrets/secret-registry.ts and
316
+ * the two seams around it).
317
+ *
318
+ * "masked" is the full round trip: every stored value is replaced by its `{{secret:name}}` reference on the
319
+ * way into the model's context, and the same reference resolves back to the value at the two exits that
320
+ * spend it (a shell command, a script). The Claude Code loop, via PostToolUse and PreToolUse.
321
+ *
322
+ * "none" is a STRUCTURAL limit, not an unfinished wire, and it is the reason this axis is honest rather
323
+ * than aspirational. On every other runtime the tool runs inside the VENDOR'S own loop: the model has read
324
+ * the result before the daemon sees any frame about it, so there is no seam left to rewrite. A PostToolUse
325
+ * hook is the only thing that can edit what a model reads, and only the Claude Code loop has one. Nothing
326
+ * about wiring more transports changes that, which is why the answer here is a disclosure and the real fix
327
+ * is to stop putting credentials where a vendor's tool can read them at all.
328
+ *
329
+ * Read by limitationsOf, and by agent/system-prompt.ts, which must not teach the reference language to a
330
+ * runtime that has no exit for it. */
331
+ readonly secrets: "masked" | "none";
289
332
  }
290
333
 
291
334
  // The Claude Code Agent SDK loop, the ceiling every other runtime is measured against, and the only one that
@@ -307,6 +350,10 @@ const CLAUDE_CODE: AgentCapabilities = {
307
350
  terminals: true,
308
351
  recovery: true,
309
352
  instructions: "replace",
353
+ // The only runtime with a pre-execution hook of its own, which is why it is the only one where a HOLD can
354
+ // park the call and wait for a card rather than having to refuse it.
355
+ rulebook: "hooks",
356
+ secrets: "masked",
310
357
  };
311
358
 
312
359
  /* Codex app-server: item-level events, process-backed MCP servers, and the four interactive seams its protocol
@@ -336,6 +383,13 @@ const CODEX: AgentCapabilities = {
336
383
  * message ahead of its skills and team blocks. Verified against codex-cli 0.147 by reading what actually
337
384
  * reached the wire, the keys are undocumented, and a strings dump proves only that they parse. */
338
385
  instructions: "replace",
386
+ /* App-server publishes `item/commandExecution/requestApproval`, whose params carry the command text, and
387
+ * takes `accept`/`decline` back (codex-cli 0.147's own generated JSON Schema, read with
388
+ * `codex app-server generate-json-schema`). The daemon only asks Codex to raise those requests when the
389
+ * owner has written command rules, so an unconfigured workspace keeps `approvalPolicy: "never"` and pays
390
+ * nothing (codex/codex-agent.ts threadOptions). */
391
+ rulebook: "approval",
392
+ secrets: "none",
339
393
  };
340
394
 
341
395
  // OpenCode (the Grok runtime): its own agentic loop, its own tools, allow-all permissions. It takes a model id,
@@ -357,6 +411,16 @@ const OPENCODE: AgentCapabilities = {
357
411
  // replacing that, so a custom prompt lands here as extra instructions, and the settings page says so
358
412
  // rather than letting "replaces everything" quietly mean something else on two providers.
359
413
  instructions: "append",
414
+ /* OpenCode asks over its own permission channel (`permission.updated`, replied on
415
+ * `/session/{id}/permissions/{permissionID}`, vocabulary once/always/reject), and the daemon judges what it
416
+ * raises with the same decide fn every other runtime uses.
417
+ *
418
+ * REFUSE-ONLY because of its watchdog, not because of its protocol. A Grok/Gemini turn is aborted after two
419
+ * minutes without an event for its session (grok/grok-agent.ts GROK_INACTIVITY_MS), and a permission paused
420
+ * on a person is exactly that silence, so a parked card would turn "ask me" into a broken turn. A `deny`
421
+ * rule is enforced in full; a `hold` stops the command and tells the agent it could not be asked about. */
422
+ rulebook: "refuse-only",
423
+ secrets: "none",
360
424
  };
361
425
 
362
426
  /* The same OpenCode loop, serving Gemini instead of xAI, identical abilities, which is the point of giving it
@@ -396,6 +460,12 @@ const ACP: AgentCapabilities = {
396
460
  // ACP's `session/new` and `session/prompt` carry no system field: the agent owns its own instructions the
397
461
  // same way it owns its model and its permission posture. The persona note takes the user message instead.
398
462
  instructions: "none",
463
+ /* `session/request_permission` is in the protocol floor, so every conforming agent has the channel and the
464
+ * daemon answers it from the rulebook (acp/acp-permissions.ts). The caveat the "approval" value already
465
+ * carries is at its widest here: WHICH calls an agent asks about is entirely the agent's choice, and one
466
+ * that never asks is one no rule can reach. */
467
+ rulebook: "approval",
468
+ secrets: "none",
399
469
  };
400
470
 
401
471
  /* THE PI CAPABILITY ID IS RESERVED, the same way the five native ids are: an `agent`-kind capability installed
@@ -426,6 +496,12 @@ const PI: AgentCapabilities = {
426
496
  // Pi's RPC opens a session with a prompt and steers it; nothing in that protocol sets standing
427
497
  // instructions, so like ACP it hears the persona note through the user message.
428
498
  instructions: "none",
499
+ /* THE ONE RUNTIME WITH NO SEAM AT ALL. Pi runs bash in-process and its RPC publishes no approval request,
500
+ * so there is nothing to consult before a command runs and no rule the owner writes can apply here. Said
501
+ * out loud rather than left to be discovered: limitationsOf renders it, and the taint floor treats a "none"
502
+ * runtime as permanently tainted, because a bit nobody can act on is worse than no bit. */
503
+ rulebook: "none",
504
+ secrets: "none",
429
505
  };
430
506
 
431
507
  // The pair → its record. An `endpoint/<id>` provider is a model API the user configured, driven BY the Claude
@@ -501,6 +577,18 @@ export const limitationsOf = (capabilities: AgentCapabilities): string[] => [
501
577
  * mechanism, because that is the thing they wrote and the thing that will or will not be in force. */
502
578
  ...(capabilities.instructions === "append" ? ["your system prompt is added to theirs, not replacing it"] : []),
503
579
  ...(capabilities.instructions === "none" ? ["your system prompt isn't applied"] : []),
580
+ /* THE TWO SAFETY AXES, phrased as what the OWNER loses rather than as which seam is missing, because both
581
+ * describe something they configured on a settings page and would otherwise assume was in force everywhere.
582
+ *
583
+ * "hooks" and "masked" are the ceiling and disclose nothing. The "approval" middle answer discloses the one
584
+ * thing that genuinely differs from a hook: the vendor picks which calls it asks about, so a rule can only
585
+ * reach what it chose to raise. */
586
+ ...(capabilities.rulebook === "approval" ? ["your command rules apply only to calls this agent asks about"] : []),
587
+ ...(capabilities.rulebook === "refuse-only"
588
+ ? ["your command rules can stop a command here but not pause to ask: a rule set to hold refuses instead"]
589
+ : []),
590
+ ...(capabilities.rulebook === "none" ? ["your command rules aren't applied"] : []),
591
+ ...(capabilities.secrets === "none" ? ["stored secrets reach the model unmasked, and `{{secret:name}}` isn't substituted"] : []),
504
592
  ];
505
593
 
506
594
  // Claude's compile-time model floor, shared by the daemon's catalog (claude-models.ts, its last rung, reached
@@ -57,6 +57,11 @@ const LEDGER: Record<keyof AgentCapabilities, Backing> = {
57
57
  // turn-plan.ts (honoured) plans the JS backend only where "js" is declared, so a runtime without it is
58
58
  // handed no `jsExecution` and mounts no Code tool: the same drop-what-you-can't-honour rule as `effort`.
59
59
  execution: "enforced",
60
+ /* turn-plan.ts (honoured) carries this onto every request and guard/turn-gate.ts DERIVES the gate's shape
61
+ * from it: "none" gets no consult and a permanently-set taint bit, "refuse-only" cannot park so a hold
62
+ * refuses, the other two park on a card. So a row that lies about itself changes how its turns behave.
63
+ * It also decides whether Codex is asked to raise approvals at all (codex-agent.ts threadOptions). */
64
+ rulebook: "enforced",
60
65
 
61
66
  /* DESCRIPTIVE: true of the runtime, and nothing consults them. Each describes behaviour that is emergent
62
67
  * rather than gated: an agent that never emits `question` frames simply never asks, one that publishes no
@@ -68,6 +73,14 @@ const LEDGER: Record<keyof AgentCapabilities, Backing> = {
68
73
  commands: "descriptive",
69
74
  terminals: "descriptive",
70
75
  recovery: "descriptive",
76
+ /* DESCRIPTIVE, and the one entry here whose gap cannot be closed by wiring. Masking a stored credential in
77
+ * what the model READS needs a seam that edits a tool result before the model sees it, and only the Claude
78
+ * Code loop has one (a PostToolUse hook). Every other runtime runs its tools inside the VENDOR'S loop: the
79
+ * model has read the result before the daemon sees any frame about it. So nothing consults this field, and
80
+ * nothing can: it exists so limitationsOf can say out loud what is true, instead of letting an owner who
81
+ * stored a credential assume it is hidden everywhere. Moving it to ENFORCED would take a runtime that
82
+ * publishes a result-rewriting seam, not a change here. */
83
+ secrets: "descriptive",
71
84
  };
72
85
 
73
86
  // The full ceiling: every ability real, so `limitationsOf` has nothing to say about it. Read from the catalog
@@ -92,10 +105,16 @@ describe("a descriptive claim reaches the user", () => {
92
105
  expect(limitationsOf(CEILING)).toEqual([]);
93
106
  });
94
107
 
108
+ /* What "lacking this axis" MEANS for a field that is not a boolean. A boolean diminishes to false; anything
109
+ * else has to name its own floor here, because only the axis knows which of its values is the weak one.
110
+ * Adding a non-boolean descriptive field without an entry fails below rather than silently testing nothing. */
111
+ const DIMINISHED: Partial<Record<keyof AgentCapabilities, unknown>> = { secrets: "none" };
112
+
95
113
  it.each(fieldsWhere("descriptive"))("%s puts its own sentence in the picker when the runtime lacks it", (field) => {
96
- expect(typeof CEILING[field], `${field} is descriptive but not a boolean teach the flip below how to diminish it`).toBe("boolean");
114
+ const floor = typeof CEILING[field] === "boolean" ? false : DIMINISHED[field];
115
+ expect(floor, `${field} is descriptive and not a boolean: add its weakest value to DIMINISHED above`).toBeDefined();
97
116
 
98
- const lacking: AgentCapabilities = { ...CEILING, [field]: false };
117
+ const lacking: AgentCapabilities = { ...CEILING, [field]: floor };
99
118
 
100
119
  expect(limitationsOf(lacking)).toHaveLength(1);
101
120
  });
@@ -4,6 +4,22 @@ import { ActivityListSchema, ActivityQuerySchema, ActivityStatusSchema } from ".
4
4
  // The activity audit feed (provider-agnostic; Discord is the first source). Read-only by design,
5
5
  // events are appended daemon-side only, so the log stays a trustworthy record of what the agent did.
6
6
  export const activityContract = {
7
- list: oc.route({ method: "GET", path: "/activity" }).input(ActivityQuerySchema).output(ActivityListSchema),
8
- status: oc.route({ method: "GET", path: "/activity/status" }).output(ActivityStatusSchema),
7
+ list: oc
8
+ .route({
9
+ method: "GET",
10
+ path: "/activity",
11
+ summary: "What the agent has done out in the world",
12
+ description:
13
+ "The audit trail of actions taken on outside services. Read-only on purpose: entries are written by the sandbox alone, which is what makes it a record worth trusting.",
14
+ })
15
+ .input(ActivityQuerySchema)
16
+ .output(ActivityListSchema),
17
+ status: oc
18
+ .route({
19
+ method: "GET",
20
+ path: "/activity/status",
21
+ summary: "Whether the audit trail is being kept",
22
+ description: "Which sources are feeding the record and whether each is working.",
23
+ })
24
+ .output(ActivityStatusSchema),
9
25
  };
@@ -19,20 +19,90 @@ import {
19
19
  // turn waiting on any interactive card (plan approval, clarifying questions, a per-tool permission prompt);
20
20
  // steer injects a user message into the running turn; stop hard-cancels it daemon-side.
21
21
  export const agentContract = {
22
- run: oc.route({ method: "POST", path: "/agent" }).input(AgentTurnSchema).output(StartedTurnSchema),
23
- attach: oc.route({ method: "POST", path: "/agent/attach" }).input(AttachTurnSchema).output(eventIterator(AttachFrameSchema)),
24
- reply: oc.route({ method: "POST", path: "/agent/reply" }).input(AgentReplySchema).output(OkSchema),
25
- steer: oc.route({ method: "POST", path: "/agent/steer" }).input(SteerSchema).output(OkSchema),
26
- stop: oc.route({ method: "POST", path: "/agent/stop" }).input(StopTurnSchema).output(OkSchema),
22
+ run: oc
23
+ .route({
24
+ method: "POST",
25
+ path: "/agent",
26
+ summary: "Say something to an agent",
27
+ description:
28
+ "Starts a turn and answers immediately with its id; the work runs inside the sandbox whether or not anybody stays connected. Watch it by attaching. Naming a conversation that does not exist yet opens it.",
29
+ })
30
+ .input(AgentTurnSchema)
31
+ .output(StartedTurnSchema),
32
+ attach: oc
33
+ .route({
34
+ method: "POST",
35
+ path: "/agent/attach",
36
+ summary: "Watch a turn happen",
37
+ description:
38
+ "Streams everything the agent does: its words, the tools it reaches for, and the answers it gets. Give it the point you have already seen and it replays from there before going live, so a reload loses nothing. The window that started the turn holds no special claim, and any number of watchers on any number of devices see the same thing.",
39
+ })
40
+ .input(AttachTurnSchema)
41
+ .output(eventIterator(AttachFrameSchema)),
42
+ reply: oc
43
+ .route({
44
+ method: "POST",
45
+ path: "/agent/reply",
46
+ summary: "Answer a question the agent asked",
47
+ description:
48
+ "Un-parks a turn that is waiting on you: approving a plan, choosing between options, or permitting a tool. The turn picks up where it stopped.",
49
+ })
50
+ .input(AgentReplySchema)
51
+ .output(OkSchema),
52
+ steer: oc
53
+ .route({
54
+ method: "POST",
55
+ path: "/agent/steer",
56
+ summary: "Interrupt a running turn",
57
+ description:
58
+ "Slips a message into a turn already under way, without stopping it. This is how you redirect an agent mid-thought rather than waiting for it to finish being wrong.",
59
+ })
60
+ .input(SteerSchema)
61
+ .output(OkSchema),
62
+ stop: oc
63
+ .route({
64
+ method: "POST",
65
+ path: "/agent/stop",
66
+ summary: "Stop a turn now",
67
+ description: "Cancels the running turn inside the sandbox. Whatever it had already written to disk stays written.",
68
+ })
69
+ .input(StopTurnSchema)
70
+ .output(OkSchema),
27
71
  // Go back to a message: restore the workspace to that turn's checkpoint, drop the messages after it, and
28
72
  // forget the provider session. CONFLICT while a turn is running, a restore cannot overwrite files an
29
73
  // agent is editing. NOT_FOUND when that message has no checkpoint to go back to.
30
- rewind: oc.route({ method: "POST", path: "/agent/rewind" }).input(RewindTurnSchema).output(RewindResultSchema),
74
+ rewind: oc
75
+ .route({
76
+ method: "POST",
77
+ path: "/agent/rewind",
78
+ summary: "Go back to an earlier message",
79
+ description:
80
+ "Puts the files back as they stood at that point, drops every message after it, and forgets what the model remembered, so the next thing you say starts from there cleanly. Refused while a turn is running, because a restore cannot overwrite files an agent is editing, and refused for a message with no saved state to return to.",
81
+ })
82
+ .input(RewindTurnSchema)
83
+ .output(RewindResultSchema),
31
84
  // The provider's slash commands as last published by one of its turns, so a conversation's `/` popover is
32
85
  // populated before it has run one. The live `commands` frame stays authoritative for a running turn.
33
- commands: oc.route({ method: "GET", path: "/agent/commands" }).input(AgentCommandsQuerySchema).output(AgentCommandsSchema),
86
+ commands: oc
87
+ .route({
88
+ method: "GET",
89
+ path: "/agent/commands",
90
+ summary: "Shortcut commands the agent knows",
91
+ description:
92
+ "The commands a provider published the last time one of its turns ran, so a composer can offer them before this conversation has run anything. A running turn's own list wins over this one.",
93
+ })
94
+ .input(AgentCommandsQuerySchema)
95
+ .output(AgentCommandsSchema),
34
96
  // The last refusal per provider, as reported by whichever turn was refused (ProviderRefusalSchema). Read
35
97
  // alongside the account listings by the surfaces that draw plan limits: the snapshot on an account row says
36
98
  // how full its pools were when last polled, and this says whether one of them has since said no.
37
- refusals: oc.route({ method: "GET", path: "/agent/refusals" }).output(ProviderRefusalsSchema),
99
+ refusals: oc
100
+ .route({
101
+ method: "GET",
102
+ path: "/agent/refusals",
103
+ summary: "The last time each provider said no",
104
+ description:
105
+ "What each model provider most recently refused and why. Read this alongside an account's usage: the usage says how full it was when last checked, this says whether it has since started turning work away.",
106
+ })
107
+ .output(ProviderRefusalsSchema),
38
108
  };
@@ -47,27 +47,96 @@ import {
47
47
  // own agent, so there should be none) and answers with what it actually deleted: a teardown that fails on one
48
48
  // agent's repo leaves that one behind rather than taking the batch down with it.
49
49
  export const agentsContract = {
50
- list: oc.route({ method: "GET", path: "/agents" }).output(AgentsListSchema),
51
- archived: oc.route({ method: "GET", path: "/agents/archived" }).output(AgentsListSchema),
50
+ list: oc
51
+ .route({
52
+ method: "GET",
53
+ path: "/agents",
54
+ summary: "Every live conversation",
55
+ description:
56
+ "The fleet as the board draws it: each conversation with its title, what it is doing, when it last moved and whether anybody has read it since. Archived conversations are not in here.",
57
+ })
58
+ .output(AgentsListSchema),
59
+ archived: oc
60
+ .route({
61
+ method: "GET",
62
+ path: "/agents/archived",
63
+ summary: "Conversations put away",
64
+ description:
65
+ "The same shape as the live fleet, for the conversations somebody has decided are finished. Their work is kept, and any one of them can be brought back.",
66
+ })
67
+ .output(AgentsListSchema),
52
68
  // The board's filter. Answers over BOTH halves of the fleet, the live roster and the archive, because
53
69
  // the board hides by design (the Finished lane windows to a handful, archived agents are off the roster
54
70
  // entirely), and a filter that reports "no matches" while the agent sits one click away is a lie. The
55
71
  // never-carded conversations that are neither are `sessions.list`'s query, which matches by the same rule.
56
- search: oc.route({ method: "GET", path: "/agents/search" }).input(AgentSearchQuerySchema).output(AgentSearchResultSchema),
57
- get: oc.route({ method: "GET", path: "/agents/{id}" }).input(AgentIdSchema).output(AgentSummarySchema),
58
- transcript: oc.route({ method: "GET", path: "/agents/{id}/transcript" }).input(AgentIdSchema).output(AgentTranscriptSchema),
72
+ search: oc
73
+ .route({
74
+ method: "GET",
75
+ path: "/agents/search",
76
+ summary: "Find a conversation",
77
+ description:
78
+ "Searches the live fleet and the archive together. Both halves on purpose: the board hides finished work by design, and a filter that says it found nothing while the answer sits one click away is simply wrong.",
79
+ })
80
+ .input(AgentSearchQuerySchema)
81
+ .output(AgentSearchResultSchema),
82
+ get: oc
83
+ .route({
84
+ method: "GET",
85
+ path: "/agents/{id}",
86
+ summary: "One conversation's card",
87
+ description: "Everything the board shows for a single conversation: its title, state, working branch, unread marker and timestamps.",
88
+ })
89
+ .input(AgentIdSchema)
90
+ .output(AgentSummarySchema),
91
+ transcript: oc
92
+ .route({
93
+ method: "GET",
94
+ path: "/agents/{id}/transcript",
95
+ summary: "The full conversation record",
96
+ description:
97
+ "Every message in one conversation, in order, including the tool calls and their results. This is the record the chat replays and the next turn is seeded from.",
98
+ })
99
+ .input(AgentIdSchema)
100
+ .output(AgentTranscriptSchema),
59
101
  /* SPEAK AS THE AGENT, append the user's words to the conversation's record as an assistant row, with no
60
102
  * turn behind them and no reply. The row is marked `placed` for human readers (RestoredMessageSchema); the
61
103
  * provider session is FORGOTTEN in the same breath, rewind-style, so the next real turn opens a fresh
62
104
  * runtime session seeded from the record, where the placed line reads as the agent's own words, because
63
105
  * the handoff renders every assistant row identically. A running turn is CONFLICT: the illusion can only be
64
106
  * established between turns, and a concurrent turn would resume the very session this exists to retire. */
65
- place: oc.route({ method: "POST", path: "/agents/{id}/place" }).input(AgentPlaceSchema).output(OkSchema),
66
- rename: oc.route({ method: "POST", path: "/agents/{id}/rename" }).input(AgentRenameSchema).output(AgentSummarySchema),
107
+ place: oc
108
+ .route({
109
+ method: "POST",
110
+ path: "/agents/{id}/place",
111
+ summary: "Put words in the agent's mouth",
112
+ description:
113
+ "Writes a line into the record as though the agent had said it, with no turn behind it and no reply. Human readers see it marked as placed. The next real turn starts fresh from the record, where the line reads as the agent's own. Refused while a turn is running.",
114
+ })
115
+ .input(AgentPlaceSchema)
116
+ .output(OkSchema),
117
+ rename: oc
118
+ .route({
119
+ method: "POST",
120
+ path: "/agents/{id}/rename",
121
+ summary: "Retitle a conversation",
122
+ description:
123
+ "Sets the title a person chose, replacing the one that was generated. Allowed while the conversation is working, and it does not count as activity.",
124
+ })
125
+ .input(AgentRenameSchema)
126
+ .output(AgentSummarySchema),
67
127
  // This agent's own land-at-completion posture, an override of the sandbox-wide `autoLand` setting; null
68
128
  // clears it back to "inherit". Legal mid-turn on purpose: the setting is read at turn COMPLETION, so
69
129
  // flipping it while the agent works is exactly "hold THIS turn's work for review", the press that matters.
70
- autoLand: oc.route({ method: "POST", path: "/agents/{id}/auto-land" }).input(AgentAutoLandSchema).output(AgentSummarySchema),
130
+ autoLand: oc
131
+ .route({
132
+ method: "POST",
133
+ path: "/agents/{id}/auto-land",
134
+ summary: "Whether this conversation merges its work automatically",
135
+ description:
136
+ "Overrides the sandbox-wide setting for one conversation; clear it to go back to following the default. Deliberately allowed mid-turn, because the setting is read when the turn finishes, so flipping it while the agent works means exactly hold this piece of work for review.",
137
+ })
138
+ .input(AgentAutoLandSchema)
139
+ .output(AgentSummarySchema),
71
140
  /* THIS conversation's answer to a provider outage, an override of the sandbox-wide `resumeAfterOutage`
72
141
  * setting; null clears it back to "inherit". The chat's offer at the moment a turn dies writes this and
73
142
  * never the global: the press happens inside one conversation and means "finish this piece of work", so
@@ -76,20 +145,112 @@ export const agentsContract = {
76
145
  * Legal mid-turn, and unlike autoLand it is legal for a WORKSPACE conversation too, an outage kills a
77
146
  * main-tree chat exactly as readily as an isolated one, and there is no branch involved either way. */
78
147
  resumeAfterOutage: oc
79
- .route({ method: "POST", path: "/agents/{id}/resume-after-outage" })
148
+ .route({
149
+ method: "POST",
150
+ path: "/agents/{id}/resume-after-outage",
151
+ summary: "Whether this conversation retries after a provider outage",
152
+ description:
153
+ "Overrides the sandbox-wide setting for one conversation; clear it to follow the default again. This is what the offer shown when a turn dies writes, because the press happens inside one conversation and honestly means finish this piece of work.",
154
+ })
80
155
  .input(AgentResumeAfterOutageSchema)
81
156
  .output(AgentSummarySchema),
82
- seen: oc.route({ method: "POST", path: "/agents/{id}/seen" }).input(AgentIdSchema).output(AgentSummarySchema),
83
- seenAll: oc.route({ method: "POST", path: "/agents/seen" }).output(AgentsListSchema),
84
- diff: oc.route({ method: "GET", path: "/agents/{id}/diff" }).input(AgentIdSchema).output(AgentChangesSchema),
85
- fileDiff: oc.route({ method: "GET", path: "/agents/{id}/{repo}/file-diff" }).input(AgentFileDiffQuerySchema).output(FileDiffSchema),
86
- land: oc.route({ method: "POST", path: "/agents/{id}/land" }).input(AgentLandSchema).output(LandResultSchema),
157
+ seen: oc
158
+ .route({
159
+ method: "POST",
160
+ path: "/agents/{id}/seen",
161
+ summary: "Mark a conversation read",
162
+ description:
163
+ "Stamps the read marker behind the unread badge on one card. Allowed while the conversation is working, and reading never counts as activity.",
164
+ })
165
+ .input(AgentIdSchema)
166
+ .output(AgentSummarySchema),
167
+ seenAll: oc
168
+ .route({
169
+ method: "POST",
170
+ path: "/agents/seen",
171
+ summary: "Mark every conversation read",
172
+ description: "Clears the unread badge across the whole fleet at once, and hands the refreshed list back.",
173
+ })
174
+ .output(AgentsListSchema),
175
+ diff: oc
176
+ .route({
177
+ method: "GET",
178
+ path: "/agents/{id}/diff",
179
+ summary: "Everything a conversation has changed",
180
+ description:
181
+ "One flat set of changed files per repo, measured against where each repo stood when the conversation started, with every file flagged as already merged or not. Not the staged-and-unstaged shape a working copy has, because nobody ever checks this branch out to stage into it.",
182
+ })
183
+ .input(AgentIdSchema)
184
+ .output(AgentChangesSchema),
185
+ fileDiff: oc
186
+ .route({
187
+ method: "GET",
188
+ path: "/agents/{id}/{repo}/file-diff",
189
+ summary: "One file's before and after in a conversation's work",
190
+ description: "Both sides of a single file: what it held when the conversation started and what it holds on its branch now.",
191
+ })
192
+ .input(AgentFileDiffQuerySchema)
193
+ .output(FileDiffSchema),
194
+ land: oc
195
+ .route({
196
+ method: "POST",
197
+ path: "/agents/{id}/land",
198
+ summary: "Merge a conversation's work into the workspace",
199
+ description:
200
+ "Brings the conversation's branches into the main tree, one repo at a time. A conflict is reported rather than raised and nothing is lost when it fails. Refused while a turn is running, and refused for a conversation that works directly in the shared tree, which has nothing to merge.",
201
+ })
202
+ .input(AgentLandSchema)
203
+ .output(LandResultSchema),
87
204
  // A collaborator's ask for the land they may not perform themselves (role floors put `land`/`discard` at
88
205
  // maintainer). Stamps AgentSummarySchema.landRequested with the caller's identity and re-frames the fleet,
89
206
  // so every maintainer's board carries the request; the land or discard that answers it clears the stamp.
90
- requestLand: oc.route({ method: "POST", path: "/agents/{id}/request-land" }).input(AgentIdSchema).output(AgentSummarySchema),
91
- discard: oc.route({ method: "POST", path: "/agents/{id}/discard" }).input(AgentIdSchema).output(OkSchema),
92
- archive: oc.route({ method: "POST", path: "/agents/archive" }).input(AgentArchiveSchema).output(AgentsMovedSchema),
93
- unarchive: oc.route({ method: "POST", path: "/agents/unarchive" }).input(AgentIdsSchema).output(AgentsMovedSchema),
94
- purge: oc.route({ method: "POST", path: "/agents/purge" }).output(AgentsRemovedSchema),
207
+ requestLand: oc
208
+ .route({
209
+ method: "POST",
210
+ path: "/agents/{id}/request-land",
211
+ summary: "Ask a maintainer to merge this work",
212
+ description:
213
+ "For a collaborator who is not allowed to merge: marks the conversation as waiting for review, with who asked. The request shows on every maintainer's board and clears when somebody merges or discards it.",
214
+ })
215
+ .input(AgentIdSchema)
216
+ .output(AgentSummarySchema),
217
+ discard: oc
218
+ .route({
219
+ method: "POST",
220
+ path: "/agents/{id}/discard",
221
+ summary: "Throw a conversation's work away",
222
+ description:
223
+ "Deletes the conversation's working copies, its branches and its entry. Nothing is kept. Refused while a turn is running, and refused for a conversation working in the shared tree.",
224
+ })
225
+ .input(AgentIdSchema)
226
+ .output(OkSchema),
227
+ archive: oc
228
+ .route({
229
+ method: "POST",
230
+ path: "/agents/archive",
231
+ summary: "Put conversations away",
232
+ description:
233
+ "The gentle counterpart to discarding. Commits whatever the conversation still has in progress onto its own branch, releases its working copy, and keeps the entry and the record. It leaves the live fleet and joins the archive. Refused for a conversation that is running.",
234
+ })
235
+ .input(AgentArchiveSchema)
236
+ .output(AgentsMovedSchema),
237
+ unarchive: oc
238
+ .route({
239
+ method: "POST",
240
+ path: "/agents/unarchive",
241
+ summary: "Bring conversations back",
242
+ description:
243
+ "Returns archived conversations to the live fleet. The next turn picks up a fresh working copy from the branch that was kept.",
244
+ })
245
+ .input(AgentIdsSchema)
246
+ .output(AgentsMovedSchema),
247
+ purge: oc
248
+ .route({
249
+ method: "POST",
250
+ path: "/agents/purge",
251
+ summary: "Empty the archive for good",
252
+ description:
253
+ "Discards every conversation already in the archive: working copies, branches and entries. The whole archive rather than a chosen few, because the archive is the pile somebody has already decided is over. A teardown that fails on one conversation leaves that one behind instead of taking the rest down with it.",
254
+ })
255
+ .output(AgentsRemovedSchema),
95
256
  };