@intentic/sandbox-contract 1.311.0 → 1.312.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 (247) hide show
  1. package/README.md +9 -4
  2. package/dist/chores/chores.js +6 -6
  3. package/dist/chores/chores.js.map +1 -1
  4. package/dist/chores/fix-deps.d.ts.map +1 -1
  5. package/dist/chores/fix-deps.js +3 -2
  6. package/dist/chores/fix-deps.js.map +1 -1
  7. package/dist/chores/prompt.d.ts.map +1 -1
  8. package/dist/chores/prompt.js +3 -2
  9. package/dist/chores/prompt.js.map +1 -1
  10. package/dist/contracts/accounts.contract.d.ts +12 -0
  11. package/dist/contracts/accounts.contract.d.ts.map +1 -1
  12. package/dist/contracts/agents.contract.d.ts +601 -0
  13. package/dist/contracts/agents.contract.d.ts.map +1 -1
  14. package/dist/contracts/agents.contract.js +30 -2
  15. package/dist/contracts/agents.contract.js.map +1 -1
  16. package/dist/contracts/device.contract.d.ts +1 -0
  17. package/dist/contracts/device.contract.d.ts.map +1 -1
  18. package/dist/contracts/extensions.contract.d.ts +2 -0
  19. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  20. package/dist/contracts/offload.contract.d.ts +107 -0
  21. package/dist/contracts/offload.contract.d.ts.map +1 -0
  22. package/dist/contracts/offload.contract.js +53 -0
  23. package/dist/contracts/offload.contract.js.map +1 -0
  24. package/dist/contracts/ports.contract.d.ts +5 -0
  25. package/dist/contracts/ports.contract.d.ts.map +1 -1
  26. package/dist/contracts/runner.contract.d.ts +66 -0
  27. package/dist/contracts/runner.contract.d.ts.map +1 -1
  28. package/dist/contracts/runner.contract.js +3 -1
  29. package/dist/contracts/runner.contract.js.map +1 -1
  30. package/dist/contracts/settings.contract.d.ts +8 -0
  31. package/dist/contracts/settings.contract.d.ts.map +1 -1
  32. package/dist/contracts/system.contract.d.ts +95 -0
  33. package/dist/contracts/system.contract.d.ts.map +1 -1
  34. package/dist/contracts/translator.contract.d.ts +16 -0
  35. package/dist/contracts/translator.contract.d.ts.map +1 -1
  36. package/dist/contracts/workspace.contract.d.ts +146 -0
  37. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  38. package/dist/contracts/workspace.contract.js +40 -3
  39. package/dist/contracts/workspace.contract.js.map +1 -1
  40. package/dist/documents/conversions.d.ts +81 -0
  41. package/dist/documents/conversions.d.ts.map +1 -0
  42. package/dist/documents/conversions.js +213 -0
  43. package/dist/documents/conversions.js.map +1 -0
  44. package/dist/documents/index.d.ts +3 -0
  45. package/dist/documents/index.d.ts.map +1 -0
  46. package/dist/documents/index.js +3 -0
  47. package/dist/documents/index.js.map +1 -0
  48. package/dist/documents/passthrough.d.ts +13 -0
  49. package/dist/documents/passthrough.d.ts.map +1 -0
  50. package/dist/documents/passthrough.js +101 -0
  51. package/dist/documents/passthrough.js.map +1 -0
  52. package/dist/events/land-breakage.d.ts +3 -0
  53. package/dist/events/land-breakage.d.ts.map +1 -1
  54. package/dist/events/land-breakage.js +3 -0
  55. package/dist/events/land-breakage.js.map +1 -1
  56. package/dist/events/land-conflict.d.ts +3 -0
  57. package/dist/events/land-conflict.d.ts.map +1 -0
  58. package/dist/events/land-conflict.js +4 -0
  59. package/dist/events/land-conflict.js.map +1 -0
  60. package/dist/events/system-events.d.ts +66 -0
  61. package/dist/events/system-events.d.ts.map +1 -1
  62. package/dist/events/system-events.js +3 -1
  63. package/dist/events/system-events.js.map +1 -1
  64. package/dist/events/verify-nudge.d.ts.map +1 -1
  65. package/dist/events/verify-nudge.js.map +1 -1
  66. package/dist/front/generated/wire.d.ts +63 -0
  67. package/dist/front/generated/wire.d.ts.map +1 -1
  68. package/dist/front/terminal-frames.d.ts +17 -0
  69. package/dist/front/terminal-frames.d.ts.map +1 -0
  70. package/dist/front/terminal-frames.js +79 -0
  71. package/dist/front/terminal-frames.js.map +1 -0
  72. package/dist/ids/conversation-ids.d.ts +2 -0
  73. package/dist/ids/conversation-ids.d.ts.map +1 -1
  74. package/dist/ids/conversation-ids.js +2 -0
  75. package/dist/ids/conversation-ids.js.map +1 -1
  76. package/dist/index.d.ts +1002 -8
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +7 -1
  79. package/dist/index.js.map +1 -1
  80. package/dist/protocol/container-requirements.d.ts +0 -1
  81. package/dist/protocol/container-requirements.d.ts.map +1 -1
  82. package/dist/protocol/container-requirements.js +1 -2
  83. package/dist/protocol/container-requirements.js.map +1 -1
  84. package/dist/protocol/ingress-contract.d.ts +0 -11
  85. package/dist/protocol/ingress-contract.d.ts.map +1 -1
  86. package/dist/protocol/raw-routes.d.ts +36 -4
  87. package/dist/protocol/raw-routes.d.ts.map +1 -1
  88. package/dist/protocol/raw-routes.js +18 -13
  89. package/dist/protocol/raw-routes.js.map +1 -1
  90. package/dist/protocol/route-meta.d.ts +2 -0
  91. package/dist/protocol/route-meta.d.ts.map +1 -1
  92. package/dist/protocol/route-meta.js.map +1 -1
  93. package/dist/protocol/runner-protocol.d.ts +34 -0
  94. package/dist/protocol/runner-protocol.d.ts.map +1 -1
  95. package/dist/protocol/runner-protocol.js +33 -0
  96. package/dist/protocol/runner-protocol.js.map +1 -1
  97. package/dist/protocol/tunnel-lanes.d.ts +10 -0
  98. package/dist/protocol/tunnel-lanes.d.ts.map +1 -0
  99. package/dist/protocol/tunnel-lanes.js +16 -0
  100. package/dist/protocol/tunnel-lanes.js.map +1 -0
  101. package/dist/schemas/activity.d.ts.map +1 -1
  102. package/dist/schemas/activity.js.map +1 -1
  103. package/dist/schemas/agents.d.ts +73 -0
  104. package/dist/schemas/agents.d.ts.map +1 -1
  105. package/dist/schemas/agents.js +33 -2
  106. package/dist/schemas/agents.js.map +1 -1
  107. package/dist/schemas/automations.d.ts +19 -0
  108. package/dist/schemas/automations.d.ts.map +1 -1
  109. package/dist/schemas/devices.d.ts +2 -0
  110. package/dist/schemas/devices.d.ts.map +1 -1
  111. package/dist/schemas/extension-updates.d.ts +4 -0
  112. package/dist/schemas/extension-updates.d.ts.map +1 -1
  113. package/dist/schemas/git/git.d.ts +16 -0
  114. package/dist/schemas/git/git.d.ts.map +1 -1
  115. package/dist/schemas/git/git.js +1 -0
  116. package/dist/schemas/git/git.js.map +1 -1
  117. package/dist/schemas/hosts.d.ts +8 -0
  118. package/dist/schemas/hosts.d.ts.map +1 -1
  119. package/dist/schemas/hosts.js +3 -0
  120. package/dist/schemas/hosts.js.map +1 -1
  121. package/dist/schemas/metrics.d.ts +7 -0
  122. package/dist/schemas/metrics.d.ts.map +1 -1
  123. package/dist/schemas/metrics.js +1 -0
  124. package/dist/schemas/metrics.js.map +1 -1
  125. package/dist/schemas/offload.d.ts +62 -0
  126. package/dist/schemas/offload.d.ts.map +1 -0
  127. package/dist/schemas/offload.js +23 -0
  128. package/dist/schemas/offload.js.map +1 -0
  129. package/dist/schemas/personas.js +3 -3
  130. package/dist/schemas/personas.js.map +1 -1
  131. package/dist/schemas/ports.d.ts +10 -0
  132. package/dist/schemas/ports.d.ts.map +1 -1
  133. package/dist/schemas/ports.js +8 -0
  134. package/dist/schemas/ports.js.map +1 -1
  135. package/dist/schemas/providers/plan-limits.d.ts +29 -0
  136. package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
  137. package/dist/schemas/providers/plan-limits.js +7 -0
  138. package/dist/schemas/providers/plan-limits.js.map +1 -1
  139. package/dist/schemas/providers/provider-oauth.d.ts +12 -0
  140. package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
  141. package/dist/schemas/settings-history.d.ts +28 -0
  142. package/dist/schemas/settings-history.d.ts.map +1 -0
  143. package/dist/schemas/settings-history.js +39 -0
  144. package/dist/schemas/settings-history.js.map +1 -0
  145. package/dist/schemas/settings.d.ts +4 -0
  146. package/dist/schemas/settings.d.ts.map +1 -1
  147. package/dist/schemas/settings.js +10 -3
  148. package/dist/schemas/settings.js.map +1 -1
  149. package/dist/schemas/system.d.ts +24 -0
  150. package/dist/schemas/system.d.ts.map +1 -1
  151. package/dist/schemas/system.js +15 -0
  152. package/dist/schemas/system.js.map +1 -1
  153. package/dist/schemas/workspace/mainline.d.ts +371 -0
  154. package/dist/schemas/workspace/mainline.d.ts.map +1 -0
  155. package/dist/schemas/workspace/mainline.js +134 -0
  156. package/dist/schemas/workspace/mainline.js.map +1 -0
  157. package/dist/schemas/workspace/workspace-tree.d.ts +23 -0
  158. package/dist/schemas/workspace/workspace-tree.d.ts.map +1 -1
  159. package/dist/schemas/workspace/workspace-tree.js +34 -0
  160. package/dist/schemas/workspace/workspace-tree.js.map +1 -1
  161. package/dist/state/definition.d.ts +20 -4
  162. package/dist/state/definition.d.ts.map +1 -1
  163. package/dist/state/definition.js.map +1 -1
  164. package/dist/state/history-state.d.ts.map +1 -1
  165. package/dist/state/history-state.js +5 -0
  166. package/dist/state/history-state.js.map +1 -1
  167. package/dist/state/runtime-state.d.ts +3 -0
  168. package/dist/state/runtime-state.d.ts.map +1 -1
  169. package/dist/state/runtime-state.js +1 -0
  170. package/dist/state/runtime-state.js.map +1 -1
  171. package/dist/state/workspace-state.d.ts +22 -1
  172. package/dist/state/workspace-state.d.ts.map +1 -1
  173. package/dist/state/workspace-state.js +27 -1
  174. package/dist/state/workspace-state.js.map +1 -1
  175. package/dist/text/transcript-fold.d.ts +1 -0
  176. package/dist/text/transcript-fold.d.ts.map +1 -1
  177. package/dist/text/transcript-fold.js +10 -1
  178. package/dist/text/transcript-fold.js.map +1 -1
  179. package/package.json +28 -27
  180. package/src/chores/chores.ts +6 -6
  181. package/src/chores/fix-deps.ts +3 -2
  182. package/src/chores/prompt.ts +3 -2
  183. package/src/contracts/agents.contract.ts +36 -1
  184. package/src/contracts/offload.contract.ts +60 -0
  185. package/src/contracts/runner.contract.ts +7 -1
  186. package/src/contracts/workspace.contract.ts +54 -2
  187. package/src/documents/conversions.ts +402 -0
  188. package/src/documents/index.ts +5 -0
  189. package/src/documents/passthrough.ts +139 -0
  190. package/src/events/land-breakage.ts +12 -0
  191. package/src/events/land-conflict.ts +12 -0
  192. package/src/events/system-events.ts +9 -3
  193. package/src/events/verify-nudge.ts +4 -3
  194. package/src/front/generated/wire.ts +35 -4
  195. package/src/front/terminal-frames.fixture.json +55 -0
  196. package/src/front/terminal-frames.test.ts +63 -0
  197. package/src/front/terminal-frames.ts +102 -0
  198. package/src/ids/conversation-ids.ts +7 -0
  199. package/src/index.ts +7 -1
  200. package/src/protocol/container-requirements.test.ts +2 -3
  201. package/src/protocol/container-requirements.ts +1 -7
  202. package/src/protocol/ingress-contract.fixture.json +77 -0
  203. package/src/protocol/ingress-contract.test.ts +18 -26
  204. package/src/protocol/ingress-contract.ts +3 -22
  205. package/src/protocol/raw-routes.ts +21 -14
  206. package/src/protocol/route-meta.ts +4 -0
  207. package/src/protocol/runner-protocol.ts +53 -0
  208. package/src/protocol/tunnel-lanes.json +136 -0
  209. package/src/protocol/tunnel-lanes.test.ts +30 -0
  210. package/src/protocol/tunnel-lanes.ts +32 -0
  211. package/src/schemas/activity.ts +3 -1
  212. package/src/schemas/agents.ts +52 -3
  213. package/src/schemas/git/git.ts +5 -0
  214. package/src/schemas/hosts.ts +8 -0
  215. package/src/schemas/metrics.ts +2 -0
  216. package/src/schemas/offload.ts +44 -0
  217. package/src/schemas/personas.ts +3 -3
  218. package/src/schemas/ports.ts +10 -0
  219. package/src/schemas/providers/plan-limits.ts +13 -0
  220. package/src/schemas/settings-history.ts +61 -0
  221. package/src/schemas/settings.ts +40 -19
  222. package/src/schemas/system.ts +17 -0
  223. package/src/schemas/workspace/mainline.ts +201 -0
  224. package/src/schemas/workspace/workspace-tree.ts +47 -0
  225. package/src/state/definition.ts +3 -2
  226. package/src/state/history-state.ts +15 -0
  227. package/src/state/runtime-state.ts +4 -0
  228. package/src/state/workspace-state.ts +35 -1
  229. package/src/text/transcript-fold.test.ts +33 -0
  230. package/src/text/transcript-fold.ts +14 -1
  231. package/dist/protocol/ingress-protocol.d.ts +0 -30
  232. package/dist/protocol/ingress-protocol.d.ts.map +0 -1
  233. package/dist/protocol/ingress-protocol.js +0 -300
  234. package/dist/protocol/ingress-protocol.js.map +0 -1
  235. package/dist/protocol/terminal-protocol.d.ts +0 -18
  236. package/dist/protocol/terminal-protocol.d.ts.map +0 -1
  237. package/dist/protocol/terminal-protocol.js +0 -2
  238. package/dist/protocol/terminal-protocol.js.map +0 -1
  239. package/dist/protocol/tunnel-heartbeat.d.ts +0 -19
  240. package/dist/protocol/tunnel-heartbeat.d.ts.map +0 -1
  241. package/dist/protocol/tunnel-heartbeat.js +0 -41
  242. package/dist/protocol/tunnel-heartbeat.js.map +0 -1
  243. package/src/protocol/ingress-protocol.test.ts +0 -579
  244. package/src/protocol/ingress-protocol.ts +0 -469
  245. package/src/protocol/terminal-protocol.ts +0 -13
  246. package/src/protocol/tunnel-heartbeat.test.ts +0 -75
  247. package/src/protocol/tunnel-heartbeat.ts +0 -67
@@ -8,26 +8,26 @@ import { runnerTranslatorPath } from "./runner-protocol.js";
8
8
  export const RAW_ROUTES = {
9
9
  // The "is a daemon there" probe every flow uses; it names the sandbox and says whether it is still converging.
10
10
  "GET /health": { auth: "door", beforeBoot: true },
11
- "GET /diff/raw": {},
11
+ "GET /diff/raw": { lane: "bulk" },
12
12
  // Arming the dictation model is a read any tier makes; dictating is writing a message, the collaborator's grant.
13
13
  "GET /speech/status": { guest: true },
14
14
  "POST /speech/transcribe": { floor: "collaborator", guest: true },
15
15
  // The bytes behind the workspace reads; each applies the caller's fence itself.
16
- "GET /workspace/raw": { guest: true },
16
+ "GET /workspace/raw": { guest: true, lane: "bulk" },
17
17
  // No door, unlike media: the page fetches a thumbnail itself, so the request carries the header.
18
18
  "GET /workspace/thumb": { guest: true },
19
19
  // Fetched by a <video>/<audio> tag, which sends no header: its scoped ticket is checked in the handler.
20
- "GET /workspace/media": { auth: "door", guest: true },
20
+ "GET /workspace/media": { auth: "door", guest: true, lane: "bulk" },
21
21
  // An attachment is part of the message it rides with; any other target edits the shared tree.
22
- "POST /workspace/upload": { floor: "writer", attachmentFloor: "collaborator" },
22
+ "POST /workspace/upload": { floor: "writer", attachmentFloor: "collaborator", lane: "bulk" },
23
23
  "POST /workspace/upload-diff": {},
24
- "POST /workspace/upload-archive": {},
24
+ "POST /workspace/upload-archive": { lane: "bulk" },
25
25
  // Minting is cheap: each WebSocket upgrade floors its own redemption.
26
26
  "POST /system/ws-ticket": { beforeBoot: true, floor: "collaborator", control: "never" },
27
27
  // A WebSocket upgrade carries no Authorization header; the terminal and both browser wires check a query ticket.
28
- "GET /system/terminal": { auth: "door", beforeBoot: true, control: "never" },
28
+ "GET /system/terminal": { auth: "door", beforeBoot: true, control: "never", front: true },
29
29
  // Desktop sync's byte pipe, guarded again by sshd's own key check against the same enrollment.
30
- "GET /system/sync/ssh": { sync: "pipe", control: "never" },
30
+ "GET /system/sync/ssh": { sync: "pipe", control: "never", lane: "bulk" },
31
31
  "GET /system/browser-profile": { auth: "door", beforeBoot: true, control: "never" },
32
32
  "GET /system/browser-view": { auth: "door", beforeBoot: true, control: "never" },
33
33
  // Deploy-target enrollment, gated by the connect token.
@@ -68,7 +68,7 @@ export const RAW_ROUTES = {
68
68
  "DELETE /bundles": { control: "never" },
69
69
  "POST /bundles/ticket": { control: "never" },
70
70
  // Navigated to by the browser, so no header: its ticket is checked in the handler.
71
- "GET /bundles/download": { auth: "door", control: "never" },
71
+ "GET /bundles/download": { auth: "door", control: "never", lane: "bulk" },
72
72
  "GET /definition": {},
73
73
  "POST /definition/diff": {},
74
74
  "GET /definition/workspace": {},
@@ -78,9 +78,14 @@ export const RAW_ROUTES = {
78
78
  "POST /arrivals/scan": {},
79
79
  "POST /arrivals/apply": {},
80
80
  "DELETE /arrivals": {},
81
- "GET /extensions/{id}/bundle": {},
81
+ "GET /extensions/{id}/bundle": { lane: "bulk" },
82
82
  // An extension backend's own namespace, proxied verbatim.
83
83
  "ALL /x/*": {},
84
+ // An extension card's MCP endpoint (its manifest's `mcp`), forwarded to the backend host; the per-boot token its
85
+ // handler checks is the one the turn's tool config carries.
86
+ "POST /mcp/extensions/{id}": { auth: "door" },
87
+ "GET /mcp/extensions/{id}": { auth: "door" },
88
+ "DELETE /mcp/extensions/{id}": { auth: "door" },
84
89
  // The `capabilities` CLI: discovery by name, and the ask that parks on an owner-decided card.
85
90
  "GET /capabilities/connectable": { floor: "maintainer", agent: true, control: "never" },
86
91
  "POST /capabilities/ask": { floor: "maintainer", agent: true, control: "never" },
@@ -135,16 +140,18 @@ export const RAW_ROUTES = {
135
140
  "GET /system/runners": { control: "never" },
136
141
  "DELETE /system/runners/{id}": { control: "never" },
137
142
  "GET /system/runners/connect": { auth: "door", beforeBoot: true, control: "never" },
138
- // A turn's own browser router, carrying the per-boot browser bridge token.
139
- "POST /system/browser/prepare": { auth: "door", control: "never" },
143
+ // A turn's browser routers, hosted in the daemon; each carries its own per-turn bearer, checked by the handler.
144
+ "POST /mcp/browser/{id}": { auth: "door", control: "never" },
145
+ "GET /mcp/browser/{id}": { auth: "door", control: "never" },
146
+ "DELETE /mcp/browser/{id}": { auth: "door", control: "never" },
140
147
  // A connected browser's credential doors, on its durable token: a sign-in moves in (`session`) or out (`lend`).
141
148
  "POST /system/webext/session": { auth: "door", control: "never" },
142
149
  "POST /system/webext/lend": { auth: "door", control: "never" },
143
150
  "POST /system/runners/{id}/definition/sync": { control: "never" },
144
151
  // A runner's git door and credential doors, each on the runner's own bearer.
145
- "GET /system/runners/git/{repo}/info/refs": { auth: "door", control: "never" },
146
- "POST /system/runners/git/{repo}/git-upload-pack": { auth: "door", control: "never" },
147
- "POST /system/runners/git/{repo}/git-receive-pack": { auth: "door", control: "never" },
152
+ "GET /system/runners/git/{repo}/info/refs": { auth: "door", control: "never", lane: "bulk" },
153
+ "POST /system/runners/git/{repo}/git-upload-pack": { auth: "door", control: "never", lane: "bulk" },
154
+ "POST /system/runners/git/{repo}/git-receive-pack": { auth: "door", control: "never", lane: "bulk" },
148
155
  "POST /system/runners/credentials": { auth: "door", control: "never" },
149
156
  "POST /system/runners/credentials/refresh": { auth: "door", control: "never" },
150
157
  [`ALL ${runnerTranslatorPath}/*` as const]: { auth: "door", control: "never" },
@@ -33,6 +33,10 @@ export interface RouteMeta {
33
33
  // Withheld from the panel token, which reaches every other route: it puts a stored credential in motion.
34
34
  readonly panel?: false;
35
35
  readonly control?: ControlReach;
36
+ // Rides the tunnel's bulk lane: a transfer, kept off the connection every keystroke and call waits on.
37
+ readonly lane?: "bulk";
38
+ // Served by intentic-front itself (_sandbox/front, term/): the daemon only answers the front's question about it.
39
+ readonly front?: true;
36
40
  }
37
41
 
38
42
  // The builder every sandbox procedure starts from, so each carries a RouteMeta and whatever it leaves out defaults.
@@ -188,6 +188,59 @@ export const RunnerTurnSchema = z.object({
188
188
  });
189
189
  export type RunnerTurn = z.infer<typeof RunnerTurnSchema>;
190
190
 
191
+ // OFFLOADED COMMANDS: one shell line the parent hands a runner instead of running it itself (settings `offload`). The
192
+ // code travels as a snapshot commit of the parent's working tree, uncommitted and untracked work included, parked under
193
+ // this ref in the repo's git dir for the runner to fetch through the git door, and deleted once the run ends.
194
+ export const OFFLOAD_REF_PREFIX = "refs/intentic-offload/";
195
+ export const offloadRef = (runId: string): string => `${OFFLOAD_REF_PREFIX}${runId}`;
196
+ const OffloadRunIdSchema = z.string().regex(/^[a-z0-9][a-z0-9-]{7,63}$/u);
197
+ const EnvNameSchema = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/u);
198
+
199
+ export const RunnerCommandSchema = z.object({
200
+ runId: OffloadRunIdSchema,
201
+ // The repo id the git door serves (a /history/gits entry: `root` for the workspace itself).
202
+ repo: z.string().min(1),
203
+ // The snapshot commit's ref in that repo, always under OFFLOAD_REF_PREFIX.
204
+ ref: z.string().startsWith(OFFLOAD_REF_PREFIX),
205
+ // Where in the repo the line runs, repo-relative; "" is the repo root.
206
+ cwd: z.string().refine((path) => !path.startsWith("/") && !path.split("/").includes(".."), "must stay inside the repo"),
207
+ command: z.string().min(1).max(64_000),
208
+ // Values set for the line, beside the runner's own environment. Never a secret: a line that needs one runs here.
209
+ env: z.record(EnvNameSchema, z.string()).default({}),
210
+ // Variables the line writes a file to (a report path): the runner points each at a scratch file and hands the file
211
+ // back with the exit.
212
+ exports: z.array(EnvNameSchema).max(8).default([]),
213
+ // The heavy-command rule it matched here, named in the runner's own output and queue.
214
+ label: z.string().min(1).max(80),
215
+ timeoutMs: z
216
+ .number()
217
+ .int()
218
+ .positive()
219
+ .max(6 * 60 * 60_000)
220
+ .optional(),
221
+ });
222
+ export type RunnerCommand = z.infer<typeof RunnerCommandSchema>;
223
+
224
+ // What a running command streams back: narration of the runner's own steps (fetch, install, queue), the line's output
225
+ // as it comes, and last its end: the exit, every file it changed as a binary git patch against the snapshot, and the
226
+ // exported files, base64.
227
+ export const RunnerCommandFrameSchema = z.union([
228
+ z.object({ kind: z.literal("status"), text: z.string() }),
229
+ z.object({ kind: z.literal("output"), stream: z.enum(["stdout", "stderr"]), text: z.string() }),
230
+ z.object({
231
+ kind: z.literal("exit"),
232
+ code: z.number().int(),
233
+ signal: z.string().optional(),
234
+ // Why the run did not end on its own: stopped, timed out, or never started (a fetch or install that failed).
235
+ failure: z.string().optional(),
236
+ // False when the line never started there, which sends it back to run on the parent.
237
+ ran: z.boolean().default(true),
238
+ patchBase64: z.string().optional(),
239
+ files: z.record(EnvNameSchema, z.string()).default({}),
240
+ }),
241
+ ]);
242
+ export type RunnerCommandFrame = z.infer<typeof RunnerCommandFrameSchema>;
243
+
191
244
  // Where a conversation runs, fixed on its first turn like `isolated` placement: later turns follow the registry, not
192
245
  // the sender. `runner` implies isolation since a remote conversation is branch-anchored; absent means local.
193
246
  export const AgentPlacementSchema = z.union([z.object({ kind: z.literal("local") }), z.object({ kind: z.literal("runner"), id: z.string().min(1) })]);
@@ -0,0 +1,136 @@
1
+ [
2
+ {"method":"GET","path":"/health","lane":"interactive"},
3
+ {"method":"GET","path":"/diff/raw","lane":"bulk"},
4
+ {"method":"GET","path":"/speech/status","lane":"interactive"},
5
+ {"method":"POST","path":"/speech/transcribe","lane":"interactive"},
6
+ {"method":"GET","path":"/workspace/raw","lane":"bulk"},
7
+ {"method":"GET","path":"/workspace/thumb","lane":"interactive"},
8
+ {"method":"GET","path":"/workspace/media","lane":"bulk"},
9
+ {"method":"POST","path":"/workspace/upload","lane":"bulk"},
10
+ {"method":"POST","path":"/workspace/upload-diff","lane":"interactive"},
11
+ {"method":"POST","path":"/workspace/upload-archive","lane":"bulk"},
12
+ {"method":"POST","path":"/system/ws-ticket","lane":"interactive"},
13
+ {"method":"GET","path":"/system/terminal","lane":"interactive"},
14
+ {"method":"GET","path":"/system/sync/ssh","lane":"bulk"},
15
+ {"method":"GET","path":"/system/browser-profile","lane":"interactive"},
16
+ {"method":"GET","path":"/system/browser-view","lane":"interactive"},
17
+ {"method":"POST","path":"/enroll","lane":"interactive"},
18
+ {"method":"POST","path":"/automations/{id}/fire","lane":"interactive"},
19
+ {"method":"POST","path":"/workflows/{id}/gate","lane":"interactive"},
20
+ {"method":"GET","path":"/webchat/widget.js","lane":"interactive"},
21
+ {"method":"GET","path":"/webchat/{id}/config","lane":"interactive"},
22
+ {"method":"GET","path":"/webchat/{id}/challenge","lane":"interactive"},
23
+ {"method":"POST","path":"/webchat/{id}/message","lane":"interactive"},
24
+ {"method":"GET","path":"/webchat/{id}/messages","lane":"interactive"},
25
+ {"method":"GET","path":"/webchat/{id}/installs","lane":"interactive"},
26
+ {"method":"GET","path":"/intake/sdk.js","lane":"interactive"},
27
+ {"method":"GET","path":"/intake/{id}/config","lane":"interactive"},
28
+ {"method":"GET","path":"/intake/{id}/challenge","lane":"interactive"},
29
+ {"method":"POST","path":"/intake/{id}/report","lane":"interactive"},
30
+ {"method":"GET","path":"/members","lane":"interactive"},
31
+ {"method":"POST","path":"/members","lane":"interactive"},
32
+ {"method":"DELETE","path":"/members","lane":"interactive"},
33
+ {"method":"DELETE","path":"/members/self","lane":"interactive"},
34
+ {"method":"GET","path":"/environment","lane":"interactive"},
35
+ {"method":"GET","path":"/environment/contents","lane":"interactive"},
36
+ {"method":"POST","path":"/environment/approve","lane":"interactive"},
37
+ {"method":"POST","path":"/environment/reject","lane":"interactive"},
38
+ {"method":"POST","path":"/environment/runtime-install","lane":"interactive"},
39
+ {"method":"GET","path":"/engines","lane":"interactive"},
40
+ {"method":"POST","path":"/engines/channel","lane":"interactive"},
41
+ {"method":"POST","path":"/engines/update","lane":"interactive"},
42
+ {"method":"POST","path":"/engines/revert","lane":"interactive"},
43
+ {"method":"GET","path":"/bundles","lane":"interactive"},
44
+ {"method":"POST","path":"/bundles","lane":"interactive"},
45
+ {"method":"DELETE","path":"/bundles","lane":"interactive"},
46
+ {"method":"POST","path":"/bundles/ticket","lane":"interactive"},
47
+ {"method":"GET","path":"/bundles/download","lane":"bulk"},
48
+ {"method":"GET","path":"/definition","lane":"interactive"},
49
+ {"method":"POST","path":"/definition/diff","lane":"interactive"},
50
+ {"method":"GET","path":"/definition/workspace","lane":"interactive"},
51
+ {"method":"POST","path":"/definition/workspace/publish","lane":"interactive"},
52
+ {"method":"POST","path":"/arrivals/plan","lane":"interactive"},
53
+ {"method":"GET","path":"/arrivals/hosts","lane":"interactive"},
54
+ {"method":"POST","path":"/arrivals/scan","lane":"interactive"},
55
+ {"method":"POST","path":"/arrivals/apply","lane":"interactive"},
56
+ {"method":"DELETE","path":"/arrivals","lane":"interactive"},
57
+ {"method":"GET","path":"/extensions/{id}/bundle","lane":"bulk"},
58
+ {"method":"ALL","path":"/x/*","lane":"interactive"},
59
+ {"method":"POST","path":"/mcp/extensions/{id}","lane":"interactive"},
60
+ {"method":"GET","path":"/mcp/extensions/{id}","lane":"interactive"},
61
+ {"method":"DELETE","path":"/mcp/extensions/{id}","lane":"interactive"},
62
+ {"method":"GET","path":"/capabilities/connectable","lane":"interactive"},
63
+ {"method":"POST","path":"/capabilities/ask","lane":"interactive"},
64
+ {"method":"GET","path":"/sandboxes","lane":"interactive"},
65
+ {"method":"POST","path":"/sandboxes","lane":"interactive"},
66
+ {"method":"GET","path":"/wallet/status","lane":"interactive"},
67
+ {"method":"POST","path":"/wallet/fetch","lane":"interactive"},
68
+ {"method":"GET","path":"/wallet/history","lane":"interactive"},
69
+ {"method":"POST","path":"/children/spawn","lane":"interactive"},
70
+ {"method":"GET","path":"/children/providers","lane":"interactive"},
71
+ {"method":"POST","path":"/children/wait","lane":"interactive"},
72
+ {"method":"POST","path":"/children/send","lane":"interactive"},
73
+ {"method":"POST","path":"/children/answer","lane":"interactive"},
74
+ {"method":"GET","path":"/children","lane":"interactive"},
75
+ {"method":"GET","path":"/fleet","lane":"interactive"},
76
+ {"method":"POST","path":"/fleet/message","lane":"interactive"},
77
+ {"method":"GET","path":"/fleet/{handle}","lane":"interactive"},
78
+ {"method":"GET","path":"/listeners/{provider}/state","lane":"interactive"},
79
+ {"method":"POST","path":"/listeners/{provider}/dispatch","lane":"interactive"},
80
+ {"method":"POST","path":"/listeners/{provider}/failure","lane":"interactive"},
81
+ {"method":"POST","path":"/listeners/{provider}/status","lane":"interactive"},
82
+ {"method":"POST","path":"/ci/webhook/{host}","lane":"interactive"},
83
+ {"method":"POST","path":"/system/sync/pair","lane":"interactive"},
84
+ {"method":"POST","path":"/system/hosts/pair","lane":"interactive"},
85
+ {"method":"POST","path":"/system/hosts/enroll","lane":"interactive"},
86
+ {"method":"GET","path":"/system/hosts","lane":"interactive"},
87
+ {"method":"DELETE","path":"/system/hosts/{id}","lane":"interactive"},
88
+ {"method":"GET","path":"/system/hosts/connect","lane":"interactive"},
89
+ {"method":"POST","path":"/mcp/hosts/{id}","lane":"interactive"},
90
+ {"method":"GET","path":"/mcp/hosts/{id}","lane":"interactive"},
91
+ {"method":"DELETE","path":"/mcp/hosts/{id}","lane":"interactive"},
92
+ {"method":"POST","path":"/system/webext/pair","lane":"interactive"},
93
+ {"method":"POST","path":"/system/webext/enroll","lane":"interactive"},
94
+ {"method":"GET","path":"/system/webext","lane":"interactive"},
95
+ {"method":"DELETE","path":"/system/webext/{id}","lane":"interactive"},
96
+ {"method":"GET","path":"/system/webext/connect","lane":"interactive"},
97
+ {"method":"POST","path":"/mcp/webext/{id}","lane":"interactive"},
98
+ {"method":"GET","path":"/mcp/webext/{id}","lane":"interactive"},
99
+ {"method":"DELETE","path":"/mcp/webext/{id}","lane":"interactive"},
100
+ {"method":"POST","path":"/system/runners/pair","lane":"interactive"},
101
+ {"method":"POST","path":"/system/runners/enroll","lane":"interactive"},
102
+ {"method":"GET","path":"/system/runners","lane":"interactive"},
103
+ {"method":"DELETE","path":"/system/runners/{id}","lane":"interactive"},
104
+ {"method":"GET","path":"/system/runners/connect","lane":"interactive"},
105
+ {"method":"POST","path":"/mcp/browser/{id}","lane":"interactive"},
106
+ {"method":"GET","path":"/mcp/browser/{id}","lane":"interactive"},
107
+ {"method":"DELETE","path":"/mcp/browser/{id}","lane":"interactive"},
108
+ {"method":"POST","path":"/system/webext/session","lane":"interactive"},
109
+ {"method":"POST","path":"/system/webext/lend","lane":"interactive"},
110
+ {"method":"POST","path":"/system/runners/{id}/definition/sync","lane":"interactive"},
111
+ {"method":"GET","path":"/system/runners/git/{repo}/info/refs","lane":"bulk"},
112
+ {"method":"POST","path":"/system/runners/git/{repo}/git-upload-pack","lane":"bulk"},
113
+ {"method":"POST","path":"/system/runners/git/{repo}/git-receive-pack","lane":"bulk"},
114
+ {"method":"POST","path":"/system/runners/credentials","lane":"interactive"},
115
+ {"method":"POST","path":"/system/runners/credentials/refresh","lane":"interactive"},
116
+ {"method":"ALL","path":"/system/runners/translator/*","lane":"interactive"},
117
+ {"method":"POST","path":"/system/control/tokens","lane":"interactive"},
118
+ {"method":"GET","path":"/system/control/tokens","lane":"interactive"},
119
+ {"method":"DELETE","path":"/system/control/tokens/{id}","lane":"interactive"},
120
+ {"method":"GET","path":"/system/passkeys","lane":"interactive"},
121
+ {"method":"POST","path":"/system/passkeys/register/options","lane":"interactive"},
122
+ {"method":"POST","path":"/system/passkeys/register","lane":"interactive"},
123
+ {"method":"POST","path":"/system/passkeys/assert/options","lane":"interactive"},
124
+ {"method":"POST","path":"/system/passkeys/assert","lane":"interactive"},
125
+ {"method":"POST","path":"/system/passkeys/policy","lane":"interactive"},
126
+ {"method":"POST","path":"/system/passkeys/recovery","lane":"interactive"},
127
+ {"method":"DELETE","path":"/system/passkeys/{id}","lane":"interactive"},
128
+ {"method":"POST","path":"/system/session/recover","lane":"interactive"},
129
+ {"method":"POST","path":"/system/sessions/revoke","lane":"interactive"},
130
+ {"method":"POST","path":"/system/access/disable","lane":"interactive"},
131
+ {"method":"POST","path":"/system/authorized-key","lane":"interactive"},
132
+ {"method":"GET","path":"/system/sync","lane":"interactive"},
133
+ {"method":"POST","path":"/system/sync/report","lane":"interactive"},
134
+ {"method":"DELETE","path":"/system/authorized-key","lane":"interactive"},
135
+ {"method":"DELETE","path":"/system/authorized-key/{machine}","lane":"interactive"}
136
+ ]
@@ -0,0 +1,30 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { type TunnelLane, tunnelLaneNamed, tunnelLaneOf, tunnelLaneTable } from "./tunnel-lanes.js";
3
+
4
+ interface LaneCase {
5
+ readonly host: string;
6
+ readonly method: string;
7
+ readonly path: string;
8
+ readonly lane: TunnelLane;
9
+ }
10
+
11
+ const read = (name: string): unknown => JSON.parse(readFileSync(new URL(name, import.meta.url), "utf8"));
12
+ const CASES = (read("./ingress-contract.fixture.json") as { lanes: LaneCase[] }).lanes;
13
+
14
+ test("the table the Rust edge embeds is the raw routes' lanes as they stand (pnpm --filter @intentic/sandbox-contract lanes)", () => {
15
+ expect(read("./tunnel-lanes.json")).toEqual(tunnelLaneTable());
16
+ });
17
+
18
+ test("every request the shared fixture names rides the lane it states", () => {
19
+ expect(CASES.length).toBeGreaterThan(20);
20
+ for (const { host, method, path, lane } of CASES) {
21
+ expect({ host, method, path, lane: tunnelLaneOf(host, method, path) }).toEqual({ host, method, path, lane });
22
+ }
23
+ });
24
+
25
+ test("a tunnel is interactive unless its upgrade names the bulk lane", () => {
26
+ expect(tunnelLaneNamed("bulk")).toBe("bulk");
27
+ expect(tunnelLaneNamed(undefined)).toBe("interactive");
28
+ expect(tunnelLaneNamed("interactive")).toBe("interactive");
29
+ expect(tunnelLaneNamed(["bulk", "bulk"])).toBe("interactive");
30
+ });
@@ -0,0 +1,32 @@
1
+ import { sandboxSubdomain } from "../ids/hostnames.js";
2
+ import { hostOwnerId } from "./ingress-contract.js";
3
+ import { RAW_ROUTE_LIST } from "./raw-routes.js";
4
+ import { servedRoute } from "./routes.js";
5
+
6
+ // Each sandbox dials two tunnels, and each browser request rides one, so a transfer never queues a keystroke, a call or
7
+ // an event frame behind it on one TCP connection. The front dials both (_sandbox/front tunnel.rs); the edge
8
+ // (_platform/ingress lanes.rs) picks by the table this file emits, and falls back to the other lane while the one picked
9
+ // is not held.
10
+
11
+ export type TunnelLane = "interactive" | "bulk";
12
+
13
+ // Names a tunnel's lane on its upgrade; a tunnel that names none is the interactive one.
14
+ export const INGRESS_LANE_HEADER = "x-intentic-lane";
15
+
16
+ export const tunnelLaneNamed = (header: string | string[] | undefined): TunnelLane => (header === "bulk" ? "bulk" : "interactive");
17
+
18
+ // A preview's dev server (every label but the daemon's own) is bulk: a page load is hundreds of requests and its assets
19
+ // can be large. The daemon's routes are interactive unless their policy says otherwise (RouteMeta `lane`).
20
+ export const tunnelLaneOf = (host: string, method: string, path: string): TunnelLane => {
21
+ const id = hostOwnerId(host);
22
+ const label = host.split(":")[0]?.split(".")[0] ?? "";
23
+ if (id === undefined || label !== sandboxSubdomain(id)) {
24
+ return "bulk";
25
+ }
26
+ return servedRoute(RAW_ROUTE_LIST, [], method, path)?.meta.lane ?? "interactive";
27
+ };
28
+
29
+ // The raw routes in registration order with the lane each rides: what the Rust edge matches a daemon request against,
30
+ // written to tunnel-lanes.json by scripts/write-lanes.mjs since the edge cannot run this file.
31
+ export const tunnelLaneTable = (): { readonly method: string; readonly path: string; readonly lane: TunnelLane }[] =>
32
+ RAW_ROUTE_LIST.map(({ method, path, meta }) => ({ method, path, lane: meta.lane ?? "interactive" }));
@@ -20,7 +20,9 @@ export const ActivityEventSchema = z.object({
20
20
  // in: message.received, voice_utterance.received, voice_transcript.received
21
21
  // out: message.send, reaction.add, messages.read, api.call (unclassified endpoint)
22
22
  // system: gateway.login_failed, dispatch.failed, voice.session_started, voice.session_ended, automation.run,
23
- // turn.started, turn.plan, turn.error, turn.completed, rule.blocked_push, rule.held_work, rule.continued_turn
23
+ // turn.started, turn.plan, turn.error, turn.completed, rule.held_work, deps.verify_green, deps.verify_red,
24
+ // deps.breakage_routed, deps.breakage_fixup, deps.breakage_waiting, deps.breakage_held, deps.breakage_resolved,
25
+ // deps.breakage_spent; rule.blocked_push and rule.continued_turn are retired and only appear in older logs
24
26
  type: z
25
27
  .string()
26
28
  .describe(
@@ -83,10 +83,30 @@ export const UnfinishedWorkSchema = z.object({
83
83
  })
84
84
  .optional()
85
85
  .describe("The agent's own checklist where that turn left it. Absent for a conversation that kept no list."),
86
- // Name of the `turn.ending` check still red when the turn ended, after its two repair rounds ran out.
87
- check: z.string().optional().describe("The end-of-turn check that was still failing when the turn ended, by name."),
86
+ // Name of the `turn.ending` check still red when the turn ended. Retired with that check: nothing writes it now, and
87
+ // it stays so entries written before still read.
88
+ check: z
89
+ .string()
90
+ .optional()
91
+ .describe("The end-of-turn check that was still failing when the turn ended, by name. No longer written: checks run after work lands."),
88
92
  });
89
93
  export type UnfinishedWork = z.infer<typeof UnfinishedWorkSchema>;
94
+ // What the last turn showed of its own work, read off its tool calls and never asked of the model: the record a card
95
+ // badges instead of sending the turn back to prove anything. Checks run after the work lands (mainline.ts).
96
+ export const TurnProofSchema = z.object({
97
+ at: z.number().describe("When the turn that left this ended, in milliseconds."),
98
+ verification: z
99
+ .enum(["verified", "unproven", "failing", "no-code"])
100
+ .describe(
101
+ "Verified: a check it ran passed after its last edit to code. Failing: the last one it ran failed. Unproven: it changed code and ran nothing that checked it. No-code: it changed nothing a check could speak to.",
102
+ ),
103
+ check: z.string().optional().describe("The command that spoke, when one did, so a targeted test is never read as the whole suite."),
104
+ unviewed: z
105
+ .number()
106
+ .optional()
107
+ .describe("How many rendered files it changed (pages, components, styles) without looking at the result afterwards. Absent when none."),
108
+ });
109
+ export type TurnProof = z.infer<typeof TurnProofSchema>;
90
110
  // A landing's commit message: subject plus the two trailer sentences a changelog repo gets. One schema, not three
91
111
  // duplicated fields, since both the live roster and the review carrier must read the same shape and can't disagree.
92
112
  export const LandedMessageSchema = z.object({
@@ -396,7 +416,11 @@ export const AgentSummarySchema = z.object({
396
416
  // Beside `attention` since a reader asks both at a glance, but opposite in shape: a turn gone, not one still parked
397
417
  // and waiting.
398
418
  unfinished: UnfinishedWorkSchema.optional().describe(
399
- "What its last turn left open: steps it never completed, a check still failing. Absent for a turn that finished what it started.",
419
+ "What its last turn left open: steps it never completed. Absent for a turn that finished what it started.",
420
+ ),
421
+ // Written by the turn's own settle from its tool calls; nothing is asked of the model to earn or clear it.
422
+ proof: TurnProofSchema.optional().describe(
423
+ "What its last turn showed of its work: whether a check it ran passed after its last edit, and whether it looked at interface files it changed. Absent until a turn that edited or checked anything has ended.",
400
424
  ),
401
425
  // Completed turns and lifetime tool calls, the card's msgs/tools counters.
402
426
  turns: z.number().optional().describe("Turns it has finished."),
@@ -484,6 +508,27 @@ export const AgentSummarySchema = z.object({
484
508
  startedAt: z.number().describe("When it started, in milliseconds."),
485
509
  endedAt: z.number().optional().describe("When the command exited, in milliseconds. Absent while it runs."),
486
510
  exitCode: z.number().int().optional().describe("The code it exited with. Absent while it runs, or when its exit left none."),
511
+ // What the turn that left it running decided about it (agent/tools/job-fates.ts): one of these, or none
512
+ // while its turn is still going.
513
+ watch: z
514
+ .string()
515
+ .optional()
516
+ .describe(
517
+ "The watch its exit wakes this conversation through. Present once the turn that left it running has ended and the conversation is waiting on it.",
518
+ ),
519
+ handed: z
520
+ .boolean()
521
+ .optional()
522
+ .describe(
523
+ "Left running for the person: the turn's reply gave its address, so it outlives the turn, wakes nothing and is theirs to stop.",
524
+ ),
525
+ ports: z.array(z.number().int()).optional().describe("The ports it was listening on when its turn ended."),
526
+ stoppedBy: z
527
+ .enum(["turn", "person", "agent"])
528
+ .optional()
529
+ .describe(
530
+ "Who stopped it: the sandbox, when the turn that used it ended without handing it over; a person; or the agent itself. Present from the moment the stop is asked. Absent for a job that exited by itself.",
531
+ ),
487
532
  }),
488
533
  )
489
534
  .optional()
@@ -514,6 +559,10 @@ export const AgentStopWatchingSchema = AgentIdSchema.extend({
514
559
  watchId: z.string().optional().describe("Which watch to disarm. Absent disarms every watch this conversation is parked on."),
515
560
  });
516
561
 
562
+ export const AgentStopJobSchema = AgentIdSchema.extend({
563
+ jobId: z.string().min(1).describe("Which of its background jobs, by the id its card and transcript row carry."),
564
+ });
565
+
517
566
  // Pages a transcript: a read returns its most recent turns and where they start (`from`); handing that back as `before`
518
567
  // asks for the page above. A stale cursor (a rewind, a fork) is clamped, never refused.
519
568
  export const AgentTranscriptQuerySchema = AgentIdSchema.extend({
@@ -450,6 +450,11 @@ export const AgentChangesSchema = z.object({
450
450
  });
451
451
  export type AgentChanges = z.infer<typeof AgentChangesSchema>;
452
452
 
453
+ // The review's `conflicts` alone, re-derived the same way. The whole review also takes a code-only line count per
454
+ // file, and on a branch of hundreds of files that count, not the conflict check, is what takes seconds.
455
+ export const AgentConflictsSchema = AgentChangesSchema.pick({ conflicts: true });
456
+ export type AgentConflicts = z.infer<typeof AgentConflictsSchema>;
457
+
453
458
  // Names scratch exactly as `AgentChanges.scratch` lists it; anything else is refused, so a stale list cannot reach a
454
459
  // file that was never scratch.
455
460
  export const AgentScratchSchema = z.object({
@@ -35,9 +35,17 @@ export const DeviceFactsSchema = z.object({
35
35
  // the rest are other sandboxes' addresses, and a sandbox has no business learning its siblings'. Absent from an
36
36
  // agent older than this field, which is why "none unreachable" is not the same value as "did not say".
37
37
  links: z.object({ total: z.number(), unreachable: z.number(), unreachableSince: z.number().optional() }).optional(),
38
+ // What this agent understands beyond the ops every agent has, so a caller can refuse up front rather than send a
39
+ // field an older agent drops without a word. Absent from an agent older than this field: it supports none of them.
40
+ features: z.array(z.string()).optional(),
38
41
  });
39
42
  export type DeviceFacts = z.infer<typeof DeviceFactsSchema>;
40
43
 
44
+ // `reshape` with `later`: save the change for the next restart. An agent without it strips `later` from the request and
45
+ // reshapes NOW, restarting the sandbox and interrupting everyone in it, which is why a later-reshape is never sent to one.
46
+ export const DEVICE_FEATURE_RESHAPE_LATER = "reshape-later";
47
+ export const deviceSupports = (facts: Pick<DeviceFacts, "features"> | undefined, feature: string): boolean => facts?.features?.includes(feature) === true;
48
+
41
49
  // Docker Desktop's own distros: `wsl -l -q` lists them like any other, and none ever runs an agent or holds a checkout.
42
50
  export const WSL_SYSTEM_DISTROS: ReadonlySet<string> = new Set(["docker-desktop", "docker-desktop-data"]);
43
51
 
@@ -140,6 +140,8 @@ export const STORAGE_CLEANABILITY = {
140
140
  other: "none",
141
141
  // Git data and checkouts of repositories deleted from the workspace; nothing reads it again, but it may hold commits never pushed.
142
142
  trash: "confirm",
143
+ // Transcripts as they were before the sandbox converted them to its current format; nothing reads them again.
144
+ backups: "confirm",
143
145
  // Copies of the sandbox packed for download.
144
146
  exports: "confirm",
145
147
  // Files agents produced: generated images, reports, harness output.
@@ -0,0 +1,44 @@
1
+ import { z } from "zod";
2
+ import { RunnerCommandFrameSchema, RunnerCommandSchema } from "../protocol/runner-protocol.js";
3
+
4
+ // OFFLOADED WORK, as the in-sandbox `offload-run` command and the editor see it (settings `offload`): a heavy line this
5
+ // sandbox hands a runner on one of the owner's machines instead of running it itself.
6
+
7
+ // Whether a runner can take a line right now, asked before the tree is snapshotted, so a machine that is asleep costs
8
+ // nothing. `name` is the machine the runner sits on, the word the owner picked it by.
9
+ export const OffloadTargetSchema = z.object({
10
+ runner: z.string().min(1),
11
+ name: z.string().min(1),
12
+ ready: z.boolean(),
13
+ // Why not, in the words the command prints before it runs the line here instead.
14
+ why: z.string().optional(),
15
+ });
16
+ export type OffloadTarget = z.infer<typeof OffloadTargetSchema>;
17
+
18
+ export const OffloadRunSchema = RunnerCommandSchema.extend({ runner: z.string().min(1) });
19
+ export type OffloadRun = z.infer<typeof OffloadRunSchema>;
20
+
21
+ // What a run streams back: the runner's own frames, or one refusal when the runner could not take it after all (it went
22
+ // offline between the question and the run, or it predates offloading), which sends the line back here.
23
+ export const OffloadFrameSchema = z.union([...RunnerCommandFrameSchema.options, z.object({ kind: z.literal("refused"), why: z.string() })]);
24
+ export type OffloadFrame = z.infer<typeof OffloadFrameSchema>;
25
+
26
+ // One offloaded run as the runner card lists it: the recent ones, kept in memory, newest first.
27
+ export const OffloadRecordSchema = z.object({
28
+ runId: z.string(),
29
+ runner: z.string(),
30
+ name: z.string(),
31
+ label: z.string(),
32
+ // The line, cut to a readable length.
33
+ command: z.string(),
34
+ startedAt: z.number(),
35
+ endedAt: z.number().optional(),
36
+ code: z.number().int().optional(),
37
+ failure: z.string().optional(),
38
+ });
39
+ export type OffloadRecord = z.infer<typeof OffloadRecordSchema>;
40
+
41
+ // One kind of heavy work the owner can send elsewhere: a heavy-command rule this sandbox queues (its id, as settings
42
+ // `offload.commands` keys it) and the pattern that sorts a line into it.
43
+ export const OffloadKindSchema = z.object({ id: z.string().min(1), pattern: z.string() });
44
+ export type OffloadKind = z.infer<typeof OffloadKindSchema>;
@@ -125,9 +125,9 @@ export const TURN_BRIEFING_NOTES: readonly TurnBriefingNote[] = [
125
125
  },
126
126
  {
127
127
  id: "checks",
128
- label: "Automatic end-of-turn checks",
129
- when: "First message, and again after a compaction, when you have rules that run at the end of a turn.",
130
- cost: "Your checks still run; the agent just does not know they are coming, so a failure arrives as a surprise it has to re-read the turn to understand.",
128
+ label: "Checks after landing",
129
+ when: "First message and after a compaction, and again whenever the main tree's own check turns red or back to green: what runs after its work lands, and which failures the main tree already has.",
130
+ cost: "The agent does not know which failures are the main tree's own and not its doing, so it may spend a turn chasing one somebody else is already fixing.",
131
131
  },
132
132
  {
133
133
  id: "dependencies",
@@ -45,6 +45,16 @@ export const PortSummarySchema = z.object({
45
45
  .describe(
46
46
  'The terminal it came from, to watch it in or stop it from. Absent when nothing in its ancestry is one, which is the honest "you cannot reach this from here".',
47
47
  ),
48
+ // A job an agent's turn left running for the person (AgentSummary.jobs[].handed); ports listed anywhere else stay
49
+ // unclaimed by any conversation.
50
+ job: z
51
+ .object({
52
+ conversationId: z.string().describe("The conversation whose turn left it running."),
53
+ jobId: z.string().describe("The job, as that conversation's card names it; stopping it stops this port."),
54
+ label: z.string().describe("What the job is, in the agent's own words when it gave any."),
55
+ })
56
+ .optional()
57
+ .describe("The background job an agent left running for you on this port, when that is what answers here."),
48
58
  forwarded: z.boolean().describe("Whether it is currently reachable from outside."),
49
59
  // `https://port-<slot>-<sandboxId>.<zone>`.
50
60
  previewUrl: z.string().optional().describe("Where to open it. Present only while forwarded, and only on a sandbox that has an outside address."),
@@ -24,11 +24,24 @@ export const UsageWindowSchema = z.object({
24
24
  gates: WindowGatesSchema,
25
25
  });
26
26
  export type UsageWindow = z.infer<typeof UsageWindowSchema>;
27
+ // A failed re-read, whatever the cause: a 429 park, a 403 asking for verification, a 5xx, a timeout. Recorded as the fact
28
+ // "the last attempt failed", never inferred from a list of known failure kinds, so a surface that dates readings by
29
+ // age can leave out every reading that cannot move, including one whose failure nobody has seen before.
30
+ export const UsageUnreadSchema = z.object({
31
+ since: z.number().describe("When re-reading this account first failed, in milliseconds. It has failed on every attempt since."),
32
+ reason: z
33
+ .string()
34
+ .describe("Why, in the provider's own words where it gave some (\"Verify your account to continue.\"). Short enough to print; never a pasted response body."),
35
+ });
36
+ export type UsageUnread = z.infer<typeof UsageUnreadSchema>;
27
37
  // Every window kept, not just the binding one, since which pool binds changes between turns. Utilization only climbs
28
38
  // within a window, so a stale reading is still a valid floor. `measuredAt` is epoch ms — windows are seconds.
29
39
  export const AccountUsageSchema = z.object({
30
40
  windows: z.array(UsageWindowSchema),
31
41
  measuredAt: z.number(),
42
+ unread: UsageUnreadSchema.optional().describe(
43
+ "Present while re-reading this account keeps failing: these windows are the last reading that succeeded, and `measuredAt` will not move until a read succeeds again.",
44
+ ),
32
45
  });
33
46
  export type AccountUsage = z.infer<typeof AccountUsageSchema>;
34
47
  // Anthropic's once-a-week reset of the session window only; the weekly allowance is untouched. The answer is entirely