@intentic/sandbox-contract 1.310.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 (304) hide show
  1. package/README.md +43 -159
  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 +15 -0
  11. package/dist/contracts/accounts.contract.d.ts.map +1 -1
  12. package/dist/contracts/agent.contract.d.ts +15 -0
  13. package/dist/contracts/agent.contract.d.ts.map +1 -1
  14. package/dist/contracts/agents.contract.d.ts +1228 -32
  15. package/dist/contracts/agents.contract.d.ts.map +1 -1
  16. package/dist/contracts/agents.contract.js +42 -3
  17. package/dist/contracts/agents.contract.js.map +1 -1
  18. package/dist/contracts/capabilities.contract.d.ts +2 -0
  19. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  20. package/dist/contracts/device.contract.d.ts +2 -0
  21. package/dist/contracts/device.contract.d.ts.map +1 -1
  22. package/dist/contracts/extensions.contract.d.ts +2 -0
  23. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  24. package/dist/contracts/offload.contract.d.ts +107 -0
  25. package/dist/contracts/offload.contract.d.ts.map +1 -0
  26. package/dist/contracts/offload.contract.js +53 -0
  27. package/dist/contracts/offload.contract.js.map +1 -0
  28. package/dist/contracts/ports.contract.d.ts +5 -0
  29. package/dist/contracts/ports.contract.d.ts.map +1 -1
  30. package/dist/contracts/runner.contract.d.ts +111 -0
  31. package/dist/contracts/runner.contract.d.ts.map +1 -1
  32. package/dist/contracts/runner.contract.js +3 -1
  33. package/dist/contracts/runner.contract.js.map +1 -1
  34. package/dist/contracts/settings.contract.d.ts +16 -0
  35. package/dist/contracts/settings.contract.d.ts.map +1 -1
  36. package/dist/contracts/system.contract.d.ts +154 -0
  37. package/dist/contracts/system.contract.d.ts.map +1 -1
  38. package/dist/contracts/translator.contract.d.ts +16 -0
  39. package/dist/contracts/translator.contract.d.ts.map +1 -1
  40. package/dist/contracts/workspace.contract.d.ts +146 -0
  41. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  42. package/dist/contracts/workspace.contract.js +40 -3
  43. package/dist/contracts/workspace.contract.js.map +1 -1
  44. package/dist/documents/conversions.d.ts +81 -0
  45. package/dist/documents/conversions.d.ts.map +1 -0
  46. package/dist/documents/conversions.js +213 -0
  47. package/dist/documents/conversions.js.map +1 -0
  48. package/dist/documents/index.d.ts +3 -0
  49. package/dist/documents/index.d.ts.map +1 -0
  50. package/dist/documents/index.js +3 -0
  51. package/dist/documents/index.js.map +1 -0
  52. package/dist/documents/passthrough.d.ts +13 -0
  53. package/dist/documents/passthrough.d.ts.map +1 -0
  54. package/dist/documents/passthrough.js +101 -0
  55. package/dist/documents/passthrough.js.map +1 -0
  56. package/dist/events/agent-events.d.ts +20 -0
  57. package/dist/events/agent-events.d.ts.map +1 -1
  58. package/dist/events/agent-events.js +6 -1
  59. package/dist/events/agent-events.js.map +1 -1
  60. package/dist/events/land-breakage.d.ts +3 -0
  61. package/dist/events/land-breakage.d.ts.map +1 -1
  62. package/dist/events/land-breakage.js +3 -0
  63. package/dist/events/land-breakage.js.map +1 -1
  64. package/dist/events/land-conflict.d.ts +3 -0
  65. package/dist/events/land-conflict.d.ts.map +1 -0
  66. package/dist/events/land-conflict.js +4 -0
  67. package/dist/events/land-conflict.js.map +1 -0
  68. package/dist/events/system-events.d.ts +114 -0
  69. package/dist/events/system-events.d.ts.map +1 -1
  70. package/dist/events/system-events.js +3 -1
  71. package/dist/events/system-events.js.map +1 -1
  72. package/dist/events/transcript.d.ts +5 -0
  73. package/dist/events/transcript.d.ts.map +1 -1
  74. package/dist/events/transcript.js +9 -1
  75. package/dist/events/transcript.js.map +1 -1
  76. package/dist/events/verify-nudge.d.ts.map +1 -1
  77. package/dist/events/verify-nudge.js.map +1 -1
  78. package/dist/front/generated/wire.d.ts +137 -0
  79. package/dist/front/generated/wire.d.ts.map +1 -0
  80. package/dist/front/generated/wire.js +2 -0
  81. package/dist/front/generated/wire.js.map +1 -0
  82. package/dist/front/terminal-frames.d.ts +17 -0
  83. package/dist/front/terminal-frames.d.ts.map +1 -0
  84. package/dist/front/terminal-frames.js +79 -0
  85. package/dist/front/terminal-frames.js.map +1 -0
  86. package/dist/ids/conversation-ids.d.ts +2 -0
  87. package/dist/ids/conversation-ids.d.ts.map +1 -1
  88. package/dist/ids/conversation-ids.js +2 -0
  89. package/dist/ids/conversation-ids.js.map +1 -1
  90. package/dist/index.d.ts +1773 -96
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js +8 -1
  93. package/dist/index.js.map +1 -1
  94. package/dist/policy/command-run.d.ts +1 -1
  95. package/dist/policy/command-run.d.ts.map +1 -1
  96. package/dist/policy/command-run.js +7 -4
  97. package/dist/policy/command-run.js.map +1 -1
  98. package/dist/protocol/container-requirements.d.ts +0 -1
  99. package/dist/protocol/container-requirements.d.ts.map +1 -1
  100. package/dist/protocol/container-requirements.js +1 -2
  101. package/dist/protocol/container-requirements.js.map +1 -1
  102. package/dist/protocol/ingress-contract.d.ts +0 -11
  103. package/dist/protocol/ingress-contract.d.ts.map +1 -1
  104. package/dist/protocol/raw-routes.d.ts +36 -4
  105. package/dist/protocol/raw-routes.d.ts.map +1 -1
  106. package/dist/protocol/raw-routes.js +18 -13
  107. package/dist/protocol/raw-routes.js.map +1 -1
  108. package/dist/protocol/route-meta.d.ts +2 -0
  109. package/dist/protocol/route-meta.d.ts.map +1 -1
  110. package/dist/protocol/route-meta.js.map +1 -1
  111. package/dist/protocol/runner-protocol.d.ts +34 -0
  112. package/dist/protocol/runner-protocol.d.ts.map +1 -1
  113. package/dist/protocol/runner-protocol.js +33 -0
  114. package/dist/protocol/runner-protocol.js.map +1 -1
  115. package/dist/protocol/tunnel-lanes.d.ts +10 -0
  116. package/dist/protocol/tunnel-lanes.d.ts.map +1 -0
  117. package/dist/protocol/tunnel-lanes.js +16 -0
  118. package/dist/protocol/tunnel-lanes.js.map +1 -0
  119. package/dist/schemas/activity.d.ts.map +1 -1
  120. package/dist/schemas/activity.js.map +1 -1
  121. package/dist/schemas/agents.d.ts +145 -3
  122. package/dist/schemas/agents.d.ts.map +1 -1
  123. package/dist/schemas/agents.js +43 -9
  124. package/dist/schemas/agents.js.map +1 -1
  125. package/dist/schemas/automations.d.ts +43 -0
  126. package/dist/schemas/automations.d.ts.map +1 -1
  127. package/dist/schemas/capabilities.d.ts +3 -0
  128. package/dist/schemas/capabilities.d.ts.map +1 -1
  129. package/dist/schemas/capabilities.js +4 -0
  130. package/dist/schemas/capabilities.js.map +1 -1
  131. package/dist/schemas/devices.d.ts +34 -6
  132. package/dist/schemas/devices.d.ts.map +1 -1
  133. package/dist/schemas/devices.js +8 -6
  134. package/dist/schemas/devices.js.map +1 -1
  135. package/dist/schemas/extension-updates.d.ts +4 -0
  136. package/dist/schemas/extension-updates.d.ts.map +1 -1
  137. package/dist/schemas/git/git.d.ts +16 -0
  138. package/dist/schemas/git/git.d.ts.map +1 -1
  139. package/dist/schemas/git/git.js +1 -0
  140. package/dist/schemas/git/git.js.map +1 -1
  141. package/dist/schemas/hosts.d.ts +8 -0
  142. package/dist/schemas/hosts.d.ts.map +1 -1
  143. package/dist/schemas/hosts.js +3 -0
  144. package/dist/schemas/hosts.js.map +1 -1
  145. package/dist/schemas/keep-warm.d.ts +68 -0
  146. package/dist/schemas/keep-warm.d.ts.map +1 -0
  147. package/dist/schemas/keep-warm.js +74 -0
  148. package/dist/schemas/keep-warm.js.map +1 -0
  149. package/dist/schemas/metrics.d.ts +7 -0
  150. package/dist/schemas/metrics.d.ts.map +1 -1
  151. package/dist/schemas/metrics.js +1 -0
  152. package/dist/schemas/metrics.js.map +1 -1
  153. package/dist/schemas/offload.d.ts +62 -0
  154. package/dist/schemas/offload.d.ts.map +1 -0
  155. package/dist/schemas/offload.js +23 -0
  156. package/dist/schemas/offload.js.map +1 -0
  157. package/dist/schemas/personas.js +3 -3
  158. package/dist/schemas/personas.js.map +1 -1
  159. package/dist/schemas/ports.d.ts +10 -0
  160. package/dist/schemas/ports.d.ts.map +1 -1
  161. package/dist/schemas/ports.js +8 -0
  162. package/dist/schemas/ports.js.map +1 -1
  163. package/dist/schemas/providers/plan-limits.d.ts +29 -0
  164. package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
  165. package/dist/schemas/providers/plan-limits.js +7 -0
  166. package/dist/schemas/providers/plan-limits.js.map +1 -1
  167. package/dist/schemas/providers/provider-oauth.d.ts +15 -0
  168. package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
  169. package/dist/schemas/providers/provider-oauth.js +4 -0
  170. package/dist/schemas/providers/provider-oauth.js.map +1 -1
  171. package/dist/schemas/providers/usage.d.ts +6 -0
  172. package/dist/schemas/providers/usage.d.ts.map +1 -1
  173. package/dist/schemas/providers/usage.js +4 -0
  174. package/dist/schemas/providers/usage.js.map +1 -1
  175. package/dist/schemas/settings-history.d.ts +28 -0
  176. package/dist/schemas/settings-history.d.ts.map +1 -0
  177. package/dist/schemas/settings-history.js +39 -0
  178. package/dist/schemas/settings-history.js.map +1 -0
  179. package/dist/schemas/settings.d.ts +8 -0
  180. package/dist/schemas/settings.d.ts.map +1 -1
  181. package/dist/schemas/settings.js +33 -3
  182. package/dist/schemas/settings.js.map +1 -1
  183. package/dist/schemas/system.d.ts +24 -0
  184. package/dist/schemas/system.d.ts.map +1 -1
  185. package/dist/schemas/system.js +15 -0
  186. package/dist/schemas/system.js.map +1 -1
  187. package/dist/schemas/workspace/mainline.d.ts +371 -0
  188. package/dist/schemas/workspace/mainline.d.ts.map +1 -0
  189. package/dist/schemas/workspace/mainline.js +134 -0
  190. package/dist/schemas/workspace/mainline.js.map +1 -0
  191. package/dist/schemas/workspace/workspace-tree.d.ts +23 -0
  192. package/dist/schemas/workspace/workspace-tree.d.ts.map +1 -1
  193. package/dist/schemas/workspace/workspace-tree.js +34 -0
  194. package/dist/schemas/workspace/workspace-tree.js.map +1 -1
  195. package/dist/state/definition.d.ts +36 -4
  196. package/dist/state/definition.d.ts.map +1 -1
  197. package/dist/state/definition.js.map +1 -1
  198. package/dist/state/fix-stance.d.ts.map +1 -1
  199. package/dist/state/fix-stance.js +1 -0
  200. package/dist/state/fix-stance.js.map +1 -1
  201. package/dist/state/history-state.d.ts.map +1 -1
  202. package/dist/state/history-state.js +5 -0
  203. package/dist/state/history-state.js.map +1 -1
  204. package/dist/state/runtime-state.d.ts +6 -0
  205. package/dist/state/runtime-state.d.ts.map +1 -1
  206. package/dist/state/runtime-state.js +2 -0
  207. package/dist/state/runtime-state.js.map +1 -1
  208. package/dist/state/workspace-state.d.ts +22 -1
  209. package/dist/state/workspace-state.d.ts.map +1 -1
  210. package/dist/state/workspace-state.js +27 -1
  211. package/dist/state/workspace-state.js.map +1 -1
  212. package/dist/text/transcript-fold.d.ts +3 -0
  213. package/dist/text/transcript-fold.d.ts.map +1 -1
  214. package/dist/text/transcript-fold.js +30 -8
  215. package/dist/text/transcript-fold.js.map +1 -1
  216. package/dist/time/zone.d.ts +1 -0
  217. package/dist/time/zone.d.ts.map +1 -1
  218. package/dist/time/zone.js +5 -0
  219. package/dist/time/zone.js.map +1 -1
  220. package/package.json +34 -22
  221. package/src/chores/chores.ts +6 -6
  222. package/src/chores/fix-deps.ts +3 -2
  223. package/src/chores/prompt.ts +3 -2
  224. package/src/contracts/agents.contract.ts +50 -3
  225. package/src/contracts/offload.contract.ts +60 -0
  226. package/src/contracts/runner.contract.ts +7 -1
  227. package/src/contracts/workspace.contract.ts +54 -2
  228. package/src/documents/conversions.ts +402 -0
  229. package/src/documents/index.ts +5 -0
  230. package/src/documents/passthrough.ts +139 -0
  231. package/src/events/agent-events.ts +9 -1
  232. package/src/events/land-breakage.ts +12 -0
  233. package/src/events/land-conflict.ts +12 -0
  234. package/src/events/system-events.ts +9 -3
  235. package/src/events/transcript.ts +14 -3
  236. package/src/events/verify-nudge.ts +4 -3
  237. package/src/front/generated/wire.ts +104 -0
  238. package/src/front/terminal-frames.fixture.json +55 -0
  239. package/src/front/terminal-frames.test.ts +63 -0
  240. package/src/front/terminal-frames.ts +102 -0
  241. package/src/ids/conversation-ids.ts +7 -0
  242. package/src/ids/hostnames.fixture.json +20 -0
  243. package/src/ids/hostnames.test.ts +20 -0
  244. package/src/index.ts +8 -1
  245. package/src/policy/command-run.test.ts +35 -0
  246. package/src/policy/command-run.ts +10 -5
  247. package/src/protocol/container-requirements.test.ts +2 -3
  248. package/src/protocol/container-requirements.ts +1 -7
  249. package/src/protocol/ingress-contract.fixture.json +77 -0
  250. package/src/protocol/ingress-contract.test.ts +18 -26
  251. package/src/protocol/ingress-contract.ts +3 -22
  252. package/src/protocol/raw-routes.ts +21 -14
  253. package/src/protocol/route-meta.ts +4 -0
  254. package/src/protocol/runner-protocol.ts +53 -0
  255. package/src/protocol/tunnel-lanes.json +136 -0
  256. package/src/protocol/tunnel-lanes.test.ts +30 -0
  257. package/src/protocol/tunnel-lanes.ts +32 -0
  258. package/src/schemas/activity.ts +3 -1
  259. package/src/schemas/agents.ts +66 -10
  260. package/src/schemas/capabilities.ts +6 -0
  261. package/src/schemas/devices.ts +16 -9
  262. package/src/schemas/git/git.ts +5 -0
  263. package/src/schemas/hosts.ts +8 -0
  264. package/src/schemas/keep-warm.test.ts +68 -0
  265. package/src/schemas/keep-warm.ts +124 -0
  266. package/src/schemas/metrics.ts +2 -0
  267. package/src/schemas/offload.ts +44 -0
  268. package/src/schemas/personas.ts +3 -3
  269. package/src/schemas/ports.ts +10 -0
  270. package/src/schemas/providers/plan-limits.ts +13 -0
  271. package/src/schemas/providers/provider-oauth.ts +6 -0
  272. package/src/schemas/providers/usage.ts +7 -0
  273. package/src/schemas/settings-history.ts +61 -0
  274. package/src/schemas/settings.ts +70 -19
  275. package/src/schemas/system.ts +17 -0
  276. package/src/schemas/workspace/mainline.ts +201 -0
  277. package/src/schemas/workspace/workspace-tree.ts +47 -0
  278. package/src/state/definition.ts +3 -2
  279. package/src/state/fix-stance.test.ts +14 -0
  280. package/src/state/fix-stance.ts +2 -0
  281. package/src/state/history-state.ts +15 -0
  282. package/src/state/runtime-state.ts +8 -0
  283. package/src/state/workspace-state.ts +35 -1
  284. package/src/text/transcript-fold.test.ts +53 -1
  285. package/src/text/transcript-fold.ts +53 -10
  286. package/src/time/zone.ts +7 -0
  287. package/src/webext/index.ts +1 -1
  288. package/dist/protocol/ingress-protocol.d.ts +0 -30
  289. package/dist/protocol/ingress-protocol.d.ts.map +0 -1
  290. package/dist/protocol/ingress-protocol.js +0 -285
  291. package/dist/protocol/ingress-protocol.js.map +0 -1
  292. package/dist/protocol/terminal-protocol.d.ts +0 -18
  293. package/dist/protocol/terminal-protocol.d.ts.map +0 -1
  294. package/dist/protocol/terminal-protocol.js +0 -2
  295. package/dist/protocol/terminal-protocol.js.map +0 -1
  296. package/dist/protocol/tunnel-heartbeat.d.ts +0 -19
  297. package/dist/protocol/tunnel-heartbeat.d.ts.map +0 -1
  298. package/dist/protocol/tunnel-heartbeat.js +0 -41
  299. package/dist/protocol/tunnel-heartbeat.js.map +0 -1
  300. package/src/protocol/ingress-protocol.test.ts +0 -535
  301. package/src/protocol/ingress-protocol.ts +0 -448
  302. package/src/protocol/terminal-protocol.ts +0 -13
  303. package/src/protocol/tunnel-heartbeat.test.ts +0 -75
  304. package/src/protocol/tunnel-heartbeat.ts +0 -67
@@ -311,8 +311,9 @@ export const holdsRequest = (row: TranscriptRequests): boolean => REQUEST_FIELDS
311
311
  // Whether a row is holding the turn open on a card nobody has answered.
312
312
  export const isAwaitingDecision = (row: TranscriptRequests): boolean => REQUEST_FIELDS.some((field) => row[field]?.status === "pending");
313
313
 
314
- // One change to a run's rows, so a client keeps rows rather than frames. Prose/thinking append to a row; a `tool` card
315
- // replaces whole by id; everything else replaces its row; `drop` removes a row that opened and never wrote.
314
+ // One change to a run's rows, so a client keeps rows rather than frames. Prose/thinking append to a row or a card; a
315
+ // `tool` card replaces its own fields by id, never its nested calls or thinking, which arrive by their own patches;
316
+ // everything else replaces its row; `drop` removes a row that opened and never wrote.
316
317
  export const TranscriptPatchSchema = z.discriminatedUnion("op", [
317
318
  z.object({ op: z.literal("append").describe("A new row at the end."), row: TranscriptRowSchema }),
318
319
  z.object({ op: z.literal("replace").describe("This row, whole, in place of the one at that index."), index: z.number().int().nonnegative(), row: TranscriptRowSchema }),
@@ -320,7 +321,17 @@ export const TranscriptPatchSchema = z.discriminatedUnion("op", [
320
321
  z.object({ op: z.literal("text").describe("More of the agent's prose, onto that row's text."), index: z.number().int().nonnegative(), text: z.string() }),
321
322
  z.object({ op: z.literal("thinking").describe("More of the agent's reasoning, onto that row's thinking."), index: z.number().int().nonnegative(), text: z.string() }),
322
323
  z.object({
323
- op: z.literal("tool").describe("A tool card, whole: new, or the latest state of one already there, matched by id wherever it nests."),
324
+ op: z.literal("toolThinking").describe("More of a delegated subagent's reasoning, onto the thinking of the card that started it."),
325
+ index: z.number().int().nonnegative(),
326
+ id: z.string().describe("The card's id, matched wherever it nests."),
327
+ text: z.string(),
328
+ }),
329
+ z.object({
330
+ op: z
331
+ .literal("tool")
332
+ .describe(
333
+ "A tool card's own fields: new, or the latest state of one already there, matched by id wherever it nests. Carries no `children` or `thinking`; a card already there keeps its own.",
334
+ ),
324
335
  index: z.number().int().nonnegative(),
325
336
  tool: TranscriptToolSchema,
326
337
  parent: z.string().optional().describe("The card this one nests under, when it is a delegated subagent's own call."),
@@ -1,6 +1,7 @@
1
- // The follow-up the daemon sends when a turn ended with work it could not confirm: the runtimes with no Stop hook get
2
- // their turn.ending asks this way (verify-nudge.ts), as an ordinary prompt. Its opening lives on the wire because the
3
- // chat has to recognise it coming back: a prompt nobody typed must not reach a reader as their own words.
1
+ // The follow-up the daemon USED to send when a turn ended with work it could not confirm. Nothing sends it any more:
2
+ // checks run after work lands, never inside a conversation (schemas/workspace/mainline.ts). It stays on the wire because
3
+ // records already hold it, and the chat has to go on recognising it: a prompt nobody typed must not reach a reader as
4
+ // their own words.
4
5
 
5
6
  // Anchored on by the reader, so it must stay unique and stable across releases — a reworded opening un-recognises every
6
7
  // nudge already in a record, and they read as the user's own typing again.
@@ -0,0 +1,104 @@
1
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
2
+
3
+ export type Answer = { "answer": "preview", route: PreviewRoute, } | { "answer": "terminal", plan: TerminalPlan,
4
+ /**
5
+ * The member whose ticket opened it, lowercased; absent when the daemon runs without auth.
6
+ */
7
+ member?: string, };
8
+
9
+ /**
10
+ * A PEM certificate chain and its PEM private key.
11
+ */
12
+ export type Certificate = { certificate: string, privateKey: string, };
13
+
14
+ export type Endpoint = { host: string, port: number, };
15
+
16
+ /**
17
+ * Everything Node sends the front.
18
+ */
19
+ export type FromNode = { "kind": "hello", build: string, pid: number, } | { "kind": "listen", config: ListenConfig, } | { "kind": "certificate", certificate?: Certificate, } | { "kind": "tunnel", tunnel?: TunnelConfig, } | { "kind": "answer", id: number, answer: Answer, } | { "kind": "refused", id: number, message: string, } | { "kind": "revoke", member?: string, } | { "kind": "watch", checkout: WatchedCheckout, } | { "kind": "unwatch", dir: string, } | { "kind": "sync", id: number, dirs: Array<string>, };
20
+
21
+ /**
22
+ * Request headers only the front sets on what it forwards to Node; it strips them from everything arriving outside.
23
+ * Exported as a literal type, so a Node constant naming one that drifts from this spelling fails to compile.
24
+ */
25
+ export type FrontHeader = "x-intentic-preview" | "x-intentic-preview-unreachable";
26
+
27
+ /**
28
+ * Every port the front binds, as Node's config names them; an absent one is not bound.
29
+ */
30
+ export type ListenConfig = { daemon: Endpoint, preview?: Endpoint, loopback?: Endpoint,
31
+ /**
32
+ * The 12-hex id every public hostname ends in; without one, a label's prefix alone classifies it.
33
+ */
34
+ sandboxId?: string,
35
+ /**
36
+ * Origins that may frame a preview besides the preview itself.
37
+ */
38
+ frameAncestors: Array<string>,
39
+ /**
40
+ * A preview path Node answers whatever its host resolves to.
41
+ */
42
+ previewProbePath: string, };
43
+
44
+ /**
45
+ * Where a preview host's requests go, as Node resolved it.
46
+ */
47
+ export type PreviewRoute = { "to": "upstream", upstream: Upstream, } | { "to": "node" };
48
+
49
+ /**
50
+ * A question the front asks Node; the answer carries the same id.
51
+ */
52
+ export type Question = { "question": "preview", host: string, } | { "question": "terminal", query: string, };
53
+
54
+ export type Scheme = "http" | "https";
55
+
56
+ /**
57
+ * What the browser sends on a terminal socket, each a JSON text frame.
58
+ */
59
+ export type TerminalClientMessage = { "type": "input", data: string, } | { "type": "resize", cols: number, rows: number, } | { "type": "ping" };
60
+
61
+ /**
62
+ * What a terminal socket opens onto, as Node decided from its ticket, session name and working directory.
63
+ */
64
+ export type TerminalPlan = { "plan": "tmux", session: string, argv: Array<string>, } | { "plan": "tail", path: string, } | { "plan": "exit", code: number, reason: string, } | { "plan": "refused", code: number, reason: string, };
65
+
66
+ /**
67
+ * What a terminal socket sends as JSON text; the pane's own bytes travel as binary frames.
68
+ */
69
+ export type TerminalServerMessage = { "type": "exit", code: number, reason?: string, } | { "type": "pong" };
70
+
71
+ /**
72
+ * The Upgrade a terminal opens with where no WebSocket rides the stream, a WebTransport one: its messages then travel
73
+ * as frames (`FrameKind`). Exported as a literal type, as `FrontHeader` is.
74
+ */
75
+ export type TerminalUpgrade = "intentic-terminal";
76
+
77
+ /**
78
+ * Everything the front sends Node.
79
+ */
80
+ export type ToNode = { "kind": "ask", id: number, question: Question, } | { "kind": "tunnel", connected: boolean, } | { "kind": "synced", id: number, generations: Array<number | null>, };
81
+
82
+ /**
83
+ * The ingress tunnel's door and the reachability grant it presents there.
84
+ */
85
+ export type TunnelConfig = { url: string, grant: string, };
86
+
87
+ /**
88
+ * A local upstream a preview host relays to.
89
+ */
90
+ export type Upstream = { host: string, port: number, scheme: Scheme,
91
+ /**
92
+ * Rewrites Host and Origin to `localhost:<port>`, for an app that never agreed to its preview name.
93
+ */
94
+ localhost: boolean,
95
+ /**
96
+ * Framed by the editor: `frame-ancestors` is set and `x-frame-options` dropped.
97
+ */
98
+ frameable: boolean, };
99
+
100
+ /**
101
+ * A checkout whose changes the front counts: its working tree, the git dir holding its index and HEAD, and the common
102
+ * dir holding refs and excludes (the git dir itself for a repository's main checkout).
103
+ */
104
+ export type WatchedCheckout = { dir: string, gitDir: string, commonDir: string, };
@@ -0,0 +1,55 @@
1
+ {
2
+ "session": "/system/transport",
3
+ "upgrade": "intentic-terminal",
4
+ "frames": [
5
+ {
6
+ "encoded": "00000000081b5b33316d68c3a9",
7
+ "frame": {
8
+ "kind": "pane",
9
+ "bytes": "1b5b33316d68c3a9"
10
+ }
11
+ },
12
+ {
13
+ "encoded": "010000001c7b2274797065223a22696e707574222c2264617461223a22c582227d",
14
+ "frame": {
15
+ "kind": "message",
16
+ "text": "{\"type\":\"input\",\"data\":\"ł\"}"
17
+ }
18
+ },
19
+ {
20
+ "encoded": "020000001703f0617574686f72697a6174696f6e207265766f6b6564",
21
+ "frame": {
22
+ "kind": "close",
23
+ "code": 1008,
24
+ "reason": "authorization revoked"
25
+ }
26
+ },
27
+ {
28
+ "encoded": "0200000000",
29
+ "frame": {
30
+ "kind": "close",
31
+ "reason": ""
32
+ }
33
+ },
34
+ {
35
+ "encoded": "0300000000",
36
+ "frame": {
37
+ "kind": "ping",
38
+ "bytes": ""
39
+ }
40
+ },
41
+ {
42
+ "encoded": "040000000101",
43
+ "frame": {
44
+ "kind": "pong",
45
+ "bytes": "01"
46
+ }
47
+ }
48
+ ],
49
+ "unreadable": [
50
+ "0500000000",
51
+ "01ffffffff",
52
+ "020000000103",
53
+ "0100000002c328"
54
+ ]
55
+ }
@@ -0,0 +1,63 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { TERMINAL_UPGRADE, type TerminalFrame, WEBTRANSPORT_PATH, encodeTerminalFrame, terminalFrameReader } from "./terminal-frames.js";
3
+
4
+ interface FixtureFrame {
5
+ readonly kind: TerminalFrame["kind"];
6
+ readonly bytes?: string;
7
+ readonly text?: string;
8
+ readonly code?: number;
9
+ readonly reason?: string;
10
+ }
11
+
12
+ const FIXTURE = JSON.parse(readFileSync(new URL("./terminal-frames.fixture.json", import.meta.url), "utf8")) as {
13
+ session: string;
14
+ upgrade: string;
15
+ frames: { encoded: string; frame: FixtureFrame }[];
16
+ unreadable: string[];
17
+ };
18
+
19
+ const hex = (value: string): Uint8Array => Uint8Array.from(value.match(/../g) ?? [], (pair) => Number.parseInt(pair, 16));
20
+
21
+ const frameOf = ({ kind, bytes, text, code, reason }: FixtureFrame): TerminalFrame => {
22
+ if (kind === "message") {
23
+ return { kind, text: text ?? "" };
24
+ }
25
+ if (kind === "close") {
26
+ return code === undefined ? { kind, reason: reason ?? "" } : { kind, code, reason: reason ?? "" };
27
+ }
28
+ return { kind, bytes: hex(bytes ?? "") };
29
+ };
30
+
31
+ const MAX = 16 * 1024 * 1024;
32
+
33
+ test("the session's path and the upgrade are the ones the edge and the front answer", () => {
34
+ expect(FIXTURE.session).toBe(WEBTRANSPORT_PATH);
35
+ expect(FIXTURE.upgrade).toBe(TERMINAL_UPGRADE);
36
+ });
37
+
38
+ test("every frame the shared fixture names encodes to its bytes and reads back", () => {
39
+ expect(FIXTURE.frames.length).toBeGreaterThan(5);
40
+ for (const { encoded, frame } of FIXTURE.frames) {
41
+ expect(encodeTerminalFrame(frameOf(frame))).toEqual(hex(encoded));
42
+ expect(terminalFrameReader(MAX)(hex(encoded))).toEqual([frameOf(frame)]);
43
+ }
44
+ });
45
+
46
+ test("frames read the same however the stream's chunks cut them", () => {
47
+ const whole = hex(FIXTURE.frames.map(({ encoded }) => encoded).join(""));
48
+ const expected = FIXTURE.frames.map(({ frame }) => frameOf(frame));
49
+ for (const size of [1, 2, 3, 7, whole.length]) {
50
+ const read = terminalFrameReader(MAX);
51
+ const frames: TerminalFrame[] = [];
52
+ for (let at = 0; at < whole.length; at += size) {
53
+ frames.push(...read(whole.slice(at, at + size)));
54
+ }
55
+ expect(frames).toEqual(expected);
56
+ }
57
+ });
58
+
59
+ test("a frame no front sends ends the stream", () => {
60
+ for (const encoded of FIXTURE.unreadable) {
61
+ expect(() => terminalFrameReader(MAX)(hex(encoded))).toThrow();
62
+ }
63
+ });
@@ -0,0 +1,102 @@
1
+ // A terminal's messages on a stream no WebSocket rides (a WebTransport one): a kind byte, a big-endian u32 length, then
2
+ // that many bytes, one WebSocket message's worth each. The front's Rust codec reads the fixture this file's test reads.
3
+
4
+ import type { TerminalUpgrade } from "./generated/wire.js";
5
+
6
+ export const TERMINAL_UPGRADE: TerminalUpgrade = "intentic-terminal";
7
+
8
+ // Where a browser opens its WebTransport session, on the address of the sandbox the session's streams reach; the edge
9
+ // answers it, never the sandbox.
10
+ export const WEBTRANSPORT_PATH = "/system/transport";
11
+
12
+ const HEADER_BYTES = 5;
13
+
14
+ // Indexed by the kind byte.
15
+ const KINDS = ["pane", "message", "close", "ping", "pong"] as const;
16
+
17
+ export type TerminalFrame =
18
+ | { readonly kind: "pane" | "ping" | "pong"; readonly bytes: Uint8Array }
19
+ | { readonly kind: "message"; readonly text: string }
20
+ // No code is a close that names none, as a WebSocket's may.
21
+ | { readonly kind: "close"; readonly code?: number; readonly reason: string };
22
+
23
+ const utf8 = new TextEncoder();
24
+ const strictUtf8 = new TextDecoder("utf-8", { fatal: true });
25
+
26
+ const payloadOf = (frame: TerminalFrame): Uint8Array => {
27
+ switch (frame.kind) {
28
+ case "message":
29
+ return utf8.encode(frame.text);
30
+ case "close": {
31
+ if (frame.code === undefined) {
32
+ return new Uint8Array(0);
33
+ }
34
+ const reason = utf8.encode(frame.reason);
35
+ const payload = new Uint8Array(2 + reason.length);
36
+ new DataView(payload.buffer).setUint16(0, frame.code);
37
+ payload.set(reason, 2);
38
+ return payload;
39
+ }
40
+ default:
41
+ return frame.bytes;
42
+ }
43
+ };
44
+
45
+ export const encodeTerminalFrame = (frame: TerminalFrame): Uint8Array => {
46
+ const payload = payloadOf(frame);
47
+ const encoded = new Uint8Array(HEADER_BYTES + payload.length);
48
+ encoded[0] = KINDS.indexOf(frame.kind);
49
+ new DataView(encoded.buffer).setUint32(1, payload.length);
50
+ encoded.set(payload, HEADER_BYTES);
51
+ return encoded;
52
+ };
53
+
54
+ const frameOf = (kind: (typeof KINDS)[number], payload: Uint8Array): TerminalFrame => {
55
+ switch (kind) {
56
+ case "message":
57
+ return { kind, text: strictUtf8.decode(payload) };
58
+ case "close": {
59
+ if (payload.length === 0) {
60
+ return { kind, reason: "" };
61
+ }
62
+ if (payload.length < 2) {
63
+ throw new Error("a close frame's code is two bytes");
64
+ }
65
+ const code = new DataView(payload.buffer, payload.byteOffset, 2).getUint16(0);
66
+ return { kind, code, reason: strictUtf8.decode(payload.subarray(2)) };
67
+ }
68
+ default:
69
+ return { kind, bytes: payload };
70
+ }
71
+ };
72
+
73
+ // Whole frames out of a stream's chunks however the chunks cut them. Throws on a kind, a length past `maxBytes` or a
74
+ // payload no front sends, since nothing after it on the stream can be framed.
75
+ export const terminalFrameReader = (maxBytes: number): ((chunk: Uint8Array) => TerminalFrame[]) => {
76
+ let pending = new Uint8Array(0);
77
+ return (chunk) => {
78
+ const joined = new Uint8Array(pending.length + chunk.length);
79
+ joined.set(pending);
80
+ joined.set(chunk, pending.length);
81
+ const view = new DataView(joined.buffer);
82
+ const frames: TerminalFrame[] = [];
83
+ let at = 0;
84
+ while (joined.length - at >= HEADER_BYTES) {
85
+ const kind = KINDS[joined[at] ?? KINDS.length];
86
+ if (kind === undefined) {
87
+ throw new Error(`no terminal frame is of kind ${joined[at]}`);
88
+ }
89
+ const length = view.getUint32(at + 1);
90
+ if (length > maxBytes) {
91
+ throw new Error(`a terminal frame of ${length} bytes is past ${maxBytes}`);
92
+ }
93
+ if (joined.length - at - HEADER_BYTES < length) {
94
+ break;
95
+ }
96
+ frames.push(frameOf(kind, joined.subarray(at + HEADER_BYTES, at + HEADER_BYTES + length)));
97
+ at += HEADER_BYTES + length;
98
+ }
99
+ pending = joined.slice(at);
100
+ return frames;
101
+ };
102
+ };
@@ -178,6 +178,13 @@ const digest = (text: string): string => {
178
178
 
179
179
  export const pushFixConversationId = (scope: string, signature: string): string => `${PUSH_FIX_PREFIX}${repoSlug(scope)}-${digest(signature)}`;
180
180
 
181
+ // A red main-line check that no conversation still holding the work could take: keyed by the project and the moment its
182
+ // red streak began, so every attempt at one streak shares a base and a later streak in the same project starts over.
183
+ // Seconds in base36 keep it short; the workspace root is spelled `workspace`, since its project folder is empty.
184
+ export const LAND_FIX_PREFIX = "land-fix-";
185
+ export const landFixConversationId = (project: string, redSince: number): string =>
186
+ `${LAND_FIX_PREFIX}${repoSlug(project === "" ? "workspace" : project)}-${Math.floor(redSince / 1000).toString(36)}`;
187
+
181
188
  // ONE FAILURE, MANY ATTEMPTS, ONE LIVE ANSWER. The derived id above names the FAILURE; an attempt at it is a
182
189
  // conversation of its own, so starting over (another model, a clean worktree) never rewrites a record some other window
183
190
  // is showing, and every attempt keeps its own transcript, cost and URL. Attempt 1 wears the bare id, so every id minted
@@ -0,0 +1,20 @@
1
+ [
2
+ { "host": "preview-web-abcdef012345.sbx.example.test", "sandboxId": "abcdef012345", "panel": "web" },
3
+ { "host": "preview-my-app-abcdef012345.sbx.example.test:443", "sandboxId": "abcdef012345", "panel": "my-app" },
4
+ { "host": "preview-shop--web-abcdef012345.example.com", "sandboxId": "abcdef012345", "panel": "shop--web" },
5
+ { "host": "preview-web-0123456789ab.sbx.example.test", "sandboxId": "abcdef012345" },
6
+ { "host": "preview--abcdef012345.sbx.example.test", "sandboxId": "abcdef012345" },
7
+ { "host": "preview-x-abcdef012345.sbx.example.test", "sandboxId": "abcdef012345", "panel": "x" },
8
+ { "host": "port-1a2b3c4d5e6f-abcdef012345.sbx.example.test:8443", "sandboxId": "abcdef012345", "port": "1a2b3c4d5e6f" },
9
+ { "host": "public-9f8e7d6c5b4a-abcdef012345.sbx.example.test", "sandboxId": "abcdef012345", "public": "9f8e7d6c5b4a" },
10
+ { "host": "sandbox-abcdef012345.sbx.example.test", "sandboxId": "abcdef012345" },
11
+ { "host": "app.example.com", "sandboxId": "abcdef012345" },
12
+ { "host": "preview-web.localhost:8788", "panel": "web" },
13
+ { "host": "port-a.localhost", "port": "a" },
14
+ { "host": "public-z.example.com", "public": "z" },
15
+ { "host": "preview-.localhost" },
16
+ { "host": "127.0.0.1:8788" },
17
+ { "host": "[::1]:8788" },
18
+ { "host": "" },
19
+ { "host": "Preview-web-abcdef012345.sbx.example.test", "sandboxId": "abcdef012345" }
20
+ ]
@@ -1,3 +1,4 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import {
2
3
  labelHostname,
3
4
  panelFromHost,
@@ -8,6 +9,7 @@ import {
8
9
  previewHostname,
9
10
  previewLabel,
10
11
  previewUrl,
12
+ publicSlotFromHost,
11
13
  sandboxHostname,
12
14
  sandboxIdFromUrl,
13
15
  syncFolder,
@@ -15,6 +17,24 @@ import {
15
17
 
16
18
  const ID = "abc123def456";
17
19
 
20
+ // The cases the front's Rust router is held to as well (_sandbox/front, route.rs), read from the one file both run.
21
+ interface HostCase {
22
+ readonly host: string;
23
+ readonly sandboxId?: string;
24
+ readonly panel?: string;
25
+ readonly port?: string;
26
+ readonly public?: string;
27
+ }
28
+ const HOST_CASES = JSON.parse(readFileSync(new URL("./hostnames.fixture.json", import.meta.url), "utf8")) as HostCase[];
29
+
30
+ test.each(HOST_CASES)("the shared host cases parse as the front parses them: $host", (hostCase) => {
31
+ expect({
32
+ panel: panelFromHost(hostCase.host, hostCase.sandboxId),
33
+ port: portSlotFromHost(hostCase.host, hostCase.sandboxId),
34
+ public: publicSlotFromHost(hostCase.host, hostCase.sandboxId),
35
+ }).toEqual({ panel: hostCase.panel, port: hostCase.port, public: hostCase.public });
36
+ });
37
+
18
38
  test("preview and port hostnames round-trip through their Host-header parsers", () => {
19
39
  expect(panelFromHost(`${previewHostname("shop--web", ID, "example.com")}:443`, ID)).toBe("shop--web");
20
40
  expect(portSlotFromHost(`${portHostname("a", ID, "example.com")}:443`, ID)).toBe("a");
package/src/index.ts CHANGED
@@ -40,6 +40,7 @@ import { skillsContract } from "./contracts/skills.contract.js";
40
40
  import { systemContract } from "./contracts/system.contract.js";
41
41
  import { translatorContract } from "./contracts/translator.contract.js";
42
42
  import { usageContract } from "./contracts/usage.contract.js";
43
+ import { offloadContract } from "./contracts/offload.contract.js";
43
44
  import { vpnContract } from "./contracts/vpn.contract.js";
44
45
  import { workflowsContract } from "./contracts/workflows.contract.js";
45
46
  import { workspaceContract } from "./contracts/workspace.contract.js";
@@ -100,6 +101,7 @@ export { systemContract } from "./contracts/system.contract.js";
100
101
  export { translatorContract } from "./contracts/translator.contract.js";
101
102
  export { type PlanLimitsHeld, type PlanLimitsRefreshed, PlanLimitsRefreshedSchema, usageContract } from "./contracts/usage.contract.js";
102
103
  export { netdiskContract } from "./contracts/netdisk.contract.js";
104
+ export { offloadContract } from "./contracts/offload.contract.js";
103
105
  export { vpnContract } from "./contracts/vpn.contract.js";
104
106
  export { workflowsContract } from "./contracts/workflows.contract.js";
105
107
  export { workspaceContract } from "./contracts/workspace.contract.js";
@@ -110,6 +112,7 @@ export * from "./events/resume.js";
110
112
  export * from "./events/system-events.js";
111
113
  export * from "./events/transcript.js";
112
114
  export * from "./events/land-breakage.js";
115
+ export * from "./events/land-conflict.js";
113
116
  export * from "./events/verify-nudge.js";
114
117
  export * from "./events/watch-wake.js";
115
118
  export * from "./policy/request-status.js";
@@ -210,25 +213,28 @@ export * from "./schemas/secrets.js";
210
213
  export * from "./schemas/netdisk.js";
211
214
  export * from "./schemas/sessions.js";
212
215
  export * from "./schemas/settings.js";
216
+ export * from "./schemas/settings-history.js";
213
217
  export * from "./schemas/share.js";
214
218
  export * from "./schemas/shared.js";
215
219
  export * from "./schemas/system-prompt.js";
216
220
  export * from "./schemas/system.js";
217
221
  export * from "./schemas/terminal.js";
218
222
  export * from "./schemas/turn-break.js";
223
+ export * from "./schemas/keep-warm.js";
219
224
  export * from "./schemas/providers/usage.js";
220
225
  export * from "./schemas/vpn.js";
226
+ export * from "./schemas/offload.js";
221
227
  export * from "./schemas/webext.js";
222
228
  export * from "./schemas/workflows.js";
223
229
  export * from "./schemas/workspace/workspace-repos.js";
224
230
  export * from "./schemas/workspace/workspace-search.js";
231
+ export * from "./schemas/workspace/mainline.js";
225
232
  export * from "./schemas/workspace/workspace-setup.js";
226
233
  export * from "./schemas/workspace/workspace-tree.js";
227
234
  export * from "./state/arrival.js";
228
235
  export * from "./state/definition.js";
229
236
  export * from "./policy/search-globs.js";
230
237
  export * from "./state/starter.js";
231
- export * from "./protocol/terminal-protocol.js";
232
238
  export * from "./text/title.js";
233
239
  export * from "./state/versions.js";
234
240
  export * from "./text/model-answer.js";
@@ -277,6 +283,7 @@ export const sandboxContract = {
277
283
  translator: translatorContract,
278
284
  usage: usageContract,
279
285
  vpn: vpnContract,
286
+ offload: offloadContract,
280
287
  exit: exitContract,
281
288
  netdisk: netdiskContract,
282
289
  workflows: workflowsContract,
@@ -0,0 +1,35 @@
1
+ import type { CommandRun } from "../schemas/ci.js";
2
+ import { followCommandRun } from "./command-run.js";
3
+
4
+ const RUNNING: CommandRun = { status: "running", command: "pnpm test", output: "" };
5
+ const PASSED: CommandRun = { status: "passed", command: "pnpm test", output: "ok" };
6
+
7
+ test("a poll that fails is reported and retried, and the run is still followed to its verdict", async () => {
8
+ const answers: Array<CommandRun | Error> = [new Error("daemon restarting"), RUNNING, PASSED];
9
+ const errors: unknown[] = [];
10
+ const settled = await followCommandRun(
11
+ async () => {
12
+ const next = answers.shift();
13
+ if (next instanceof Error) {
14
+ throw next;
15
+ }
16
+ return next as CommandRun;
17
+ },
18
+ { intervalMs: 1, onError: (cause) => errors.push(cause) },
19
+ );
20
+ expect(settled).toEqual(PASSED);
21
+ expect(errors).toEqual([new Error("daemon restarting")]);
22
+ });
23
+
24
+ test("a throw from onState is the caller's bug and reaches the caller, not a poll failure retried forever", async () => {
25
+ const errors: unknown[] = [];
26
+ const following = followCommandRun(async () => RUNNING, {
27
+ intervalMs: 1,
28
+ onState: () => {
29
+ throw new TypeError("state.session is undefined");
30
+ },
31
+ onError: (cause) => errors.push(cause),
32
+ });
33
+ await expect(following).rejects.toThrow("state.session is undefined");
34
+ expect(errors).toEqual([]);
35
+ });
@@ -10,8 +10,9 @@ export interface FollowRunOptions<R extends CommandRun> {
10
10
  readonly signal?: AbortSignal | undefined;
11
11
  // Every state read, settled or not, for a surface that draws progress.
12
12
  readonly onState?: ((run: R) => void) | undefined;
13
- // A poll that failed, not a run that failed: the run is still going, and following continues.
14
- readonly onError?: ((cause: unknown) => void) | undefined;
13
+ // A poll that failed, not a run that failed: the run is still going, and following continues. Required, since a
14
+ // loop that retries a failing read forever with nobody told looks exactly like a run that is still going.
15
+ readonly onError: (cause: unknown) => void;
15
16
  }
16
17
 
17
18
  // Follows a run to its verdict: reads state until it is no longer running. The one shared loop, so a dropped poll isn't
@@ -22,14 +23,18 @@ export const followCommandRun = async <R extends CommandRun>(read: () => Promise
22
23
  if (signal?.aborted === true) {
23
24
  return undefined;
24
25
  }
26
+ // Only the read is a poll that may fail and be retried; a throw from `onState` is the caller's own bug.
27
+ let run: R | undefined;
25
28
  try {
26
- const run = await read();
29
+ run = await read();
30
+ } catch (cause) {
31
+ onError(cause);
32
+ }
33
+ if (run !== undefined) {
27
34
  onState?.(run);
28
35
  if (run.status !== "running") {
29
36
  return run;
30
37
  }
31
- } catch (cause) {
32
- onError?.(cause);
33
38
  }
34
39
  await sleep(intervalMs, { signal });
35
40
  }
@@ -38,9 +38,8 @@ describe(`containerDrift`, () => {
38
38
  expect(containerDrift({ SANDBOX_PUBLIC_URL: `` })).toEqual([]);
39
39
  });
40
40
 
41
- // A hosted Fly microVM has no grant by design, so this reports nothing on it.
42
- it(`stays quiet on a hosted machine, which serves its address without dialling out`, () => {
43
- expect(containerDrift({ SANDBOX_PUBLIC_URL: CURRENT.SANDBOX_PUBLIC_URL, SANDBOX_VM: `1` })).toEqual([]);
41
+ it(`holds a hosted machine to the same grant, since it dials the edge like any sandbox`, () => {
42
+ expect(containerDrift({ SANDBOX_PUBLIC_URL: CURRENT.SANDBOX_PUBLIC_URL, SANDBOX_VM: `1` }).map((gap) => gap.key)).toEqual([`reachability`]);
44
43
  });
45
44
  });
46
45
 
@@ -7,8 +7,6 @@ export interface ContainerRequirement {
7
7
  readonly requires: readonly string[];
8
8
  // Absent `given` evidence means this sandbox never had the capability: not a fault, just unconfigured.
9
9
  readonly given: readonly string[];
10
- // Row stays silent if any `unless` key is present: an alternate mechanism already satisfies the evidence.
11
- readonly unless?: readonly string[];
12
10
  readonly enables: string;
13
11
  readonly lost: string;
14
12
  readonly repair: string;
@@ -25,8 +23,6 @@ export const CONTAINER_REQUIREMENTS: readonly ContainerRequirement[] = [
25
23
  key: "reachability",
26
24
  requires: ["SANDBOX_GRANT", "INGRESS_URL"],
27
25
  given: ["SANDBOX_PUBLIC_URL"],
28
- // A Fly microVM's edge replays directly to it, so it holds no grant by design.
29
- unless: ["SANDBOX_VM"],
30
26
  enables: "reaching this sandbox at its public address, from anywhere",
31
27
  lost:
32
28
  "Its public address answers 502 to everyone, including this browser when it is not on the same machine as the sandbox. " +
@@ -45,9 +41,7 @@ export interface ContainerGap {
45
41
 
46
42
  /** Empty means every requirement is met or not applicable; non-empty always means a setup rerun is needed. */
47
43
  export const containerDrift = (env: ContainerEnv): readonly ContainerGap[] =>
48
- CONTAINER_REQUIREMENTS.filter(
49
- (requirement) => requirement.given.every((name) => present(env, name)) && !(requirement.unless ?? []).some((name) => present(env, name)),
50
- )
44
+ CONTAINER_REQUIREMENTS.filter((requirement) => requirement.given.every((name) => present(env, name)))
51
45
  .map((requirement) => ({
52
46
  key: requirement.key,
53
47
  missing: requirement.requires.filter((name) => !present(env, name)),