@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
@@ -1,6 +1,10 @@
1
1
  // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
2
2
 
3
- export type Answer = { "answer": "preview", route: PreviewRoute, };
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, };
4
8
 
5
9
  /**
6
10
  * A PEM certificate chain and its PEM private key.
@@ -12,7 +16,7 @@ export type Endpoint = { host: string, port: number, };
12
16
  /**
13
17
  * Everything Node sends the front.
14
18
  */
15
- 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, };
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>, };
16
20
 
17
21
  /**
18
22
  * Request headers only the front sets on what it forwards to Node; it strips them from everything arriving outside.
@@ -45,14 +49,35 @@ export type PreviewRoute = { "to": "upstream", upstream: Upstream, } | { "to": "
45
49
  /**
46
50
  * A question the front asks Node; the answer carries the same id.
47
51
  */
48
- export type Question = { "question": "preview", host: string, };
52
+ export type Question = { "question": "preview", host: string, } | { "question": "terminal", query: string, };
49
53
 
50
54
  export type Scheme = "http" | "https";
51
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
+
52
77
  /**
53
78
  * Everything the front sends Node.
54
79
  */
55
- export type ToNode = { "kind": "ask", id: number, question: Question, } | { "kind": "tunnel", connected: boolean, };
80
+ export type ToNode = { "kind": "ask", id: number, question: Question, } | { "kind": "tunnel", connected: boolean, } | { "kind": "synced", id: number, generations: Array<number | null>, };
56
81
 
57
82
  /**
58
83
  * The ingress tunnel's door and the reachability grant it presents there.
@@ -71,3 +96,9 @@ localhost: boolean,
71
96
  * Framed by the editor: `frame-ancestors` is set and `x-frame-options` dropped.
72
97
  */
73
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
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,6 +213,7 @@ 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";
@@ -219,17 +223,18 @@ export * from "./schemas/turn-break.js";
219
223
  export * from "./schemas/keep-warm.js";
220
224
  export * from "./schemas/providers/usage.js";
221
225
  export * from "./schemas/vpn.js";
226
+ export * from "./schemas/offload.js";
222
227
  export * from "./schemas/webext.js";
223
228
  export * from "./schemas/workflows.js";
224
229
  export * from "./schemas/workspace/workspace-repos.js";
225
230
  export * from "./schemas/workspace/workspace-search.js";
231
+ export * from "./schemas/workspace/mainline.js";
226
232
  export * from "./schemas/workspace/workspace-setup.js";
227
233
  export * from "./schemas/workspace/workspace-tree.js";
228
234
  export * from "./state/arrival.js";
229
235
  export * from "./state/definition.js";
230
236
  export * from "./policy/search-globs.js";
231
237
  export * from "./state/starter.js";
232
- export * from "./protocol/terminal-protocol.js";
233
238
  export * from "./text/title.js";
234
239
  export * from "./state/versions.js";
235
240
  export * from "./text/model-answer.js";
@@ -278,6 +283,7 @@ export const sandboxContract = {
278
283
  translator: translatorContract,
279
284
  usage: usageContract,
280
285
  vpn: vpnContract,
286
+ offload: offloadContract,
281
287
  exit: exitContract,
282
288
  netdisk: netdiskContract,
283
289
  workflows: workflowsContract,
@@ -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)),
@@ -0,0 +1,77 @@
1
+ {
2
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAfiMlxPB/8tnjh6TYS3rVde63OXB/wY77KWWtJ39XjBg=\n-----END PUBLIC KEY-----\n",
3
+ "grants": [
4
+ { "token": "ig1.eyJzdWIiOiJhYmNkZWYwMTIzNDUiLCJpYXQiOjE3MDAwMDAwMDB9.wdpBpqmMl21mqAZvRSYi4WfDejreTKDxHfOQp_sBKOTQB0_N2trL6iFcRxFUDrGBnGHmYnv-x66FgU_j57ZECA", "claim": {"sandboxId": "abcdef012345", "issuedAt": 1700000000} },
5
+ { "token": "ig1.eyJzdWIiOiIwMTIzNDU2Nzg5YWIiLCJpYXQiOjE3NTcwMDAwMDAsImV4dHJhIjoiaWdub3JlZCJ9.NK2BR2L_ycCACdmnbZgkg7sovw-Nhlt5vIL6ldArAx_jN9XOh-k1wisviROAgU40YGBgUASIgA6Wdn5SyYHHCA", "claim": {"sandboxId": "0123456789ab", "issuedAt": 1757000000} },
6
+ { "token": "ig1.eyJpYXQiOjE3MDAwMDAwMDAsInN1YiI6ImFiY2RlZjAxMjM0NSJ9.AenzYQn3Opfc-Sg_vkBrMEIOnCFZ3a46oSzjABAWyoVe3GP0vXlIM8MFgddHDQc7yow4kdfn1kHcUcsOoKkLCg", "claim": {"sandboxId": "abcdef012345", "issuedAt": 1700000000} },
7
+ { "token": "ig1.eyJzdWIiOiJhYmNkZWYwMTIzNDUiLCJpYXQiOjE3MDAwMDAwMDB9.wNpBpqmMl21mqAZvRSYi4WfDejreTKDxHfOQp_sBKOTQB0_N2trL6iFcRxFUDrGBnGHmYnv-x66FgU_j57ZECA", "claim": null },
8
+ { "token": "ig1.eiJzdWIiOiJhYmNkZWYwMTIzNDUiLCJpYXQiOjE3MDAwMDAwMDB9.wdpBpqmMl21mqAZvRSYi4WfDejreTKDxHfOQp_sBKOTQB0_N2trL6iFcRxFUDrGBnGHmYnv-x66FgU_j57ZECA", "claim": null },
9
+ { "token": "ig1.eyJzdWIiOiJhYmNkZWYwMTIzNDUiLCJpYXQiOjE3MDAwMDAwMDB9.Ac2ofn7IxD3M51pchXEycOPg4TP_kiTzu-aTVowDLTirjVzYrvfJ-ntrzPbQg-3RpBhmc53yP7s7iWyOgGWuAw", "claim": null },
10
+ { "token": "ig2.eyJzdWIiOiJhYmNkZWYwMTIzNDUiLCJpYXQiOjE3MDAwMDAwMDB9.wdpBpqmMl21mqAZvRSYi4WfDejreTKDxHfOQp_sBKOTQB0_N2trL6iFcRxFUDrGBnGHmYnv-x66FgU_j57ZECA", "claim": null },
11
+ { "token": "eyJzdWIiOiJhYmNkZWYwMTIzNDUiLCJpYXQiOjE3MDAwMDAwMDB9.wdpBpqmMl21mqAZvRSYi4WfDejreTKDxHfOQp_sBKOTQB0_N2trL6iFcRxFUDrGBnGHmYnv-x66FgU_j57ZECA", "claim": null },
12
+ { "token": "ig1.eyJzdWIiOiJhYmNkZWYwMTIzNDUiLCJpYXQiOjE3MDAwMDAwMDB9.wdpBpqmMl21mqAZvRSYi4WfDejreTKDxHfOQp_sBKOTQB0_N2trL6iFcRxFUDrGBnGHmYnv-x66FgU_j57ZECA.wdpBpqmMl21mqAZvRSYi4WfDejreTKDxHfOQp_sBKOTQB0_N2trL6iFcRxFUDrGBnGHmYnv-x66FgU_j57ZECA", "claim": null },
13
+ { "token": "ig1.eyJzdWIiOiJhYmNkZWYwMTIzNDUiLCJpYXQiOjE3MDAwMDAwMDB9.", "claim": null },
14
+ { "token": "", "claim": null },
15
+ { "token": "ig1", "claim": null },
16
+ { "token": "ig1..", "claim": null },
17
+ { "token": "ig1.%%%.%%%", "claim": null },
18
+ { "token": "ig1.eyJzdWIiOiJBQkNERUYwMTIzNDUiLCJpYXQiOjE3MDAwMDAwMDB9.CbPEciFki-GXJnUcS5F9_9uXUOw5tWhv3iS69BjuQpSmsA72_xnvYUIjhAMiBPI98XBQ9-zKc9GSPJIFGQqaDA", "claim": null },
19
+ { "token": "ig1.eyJzdWIiOiJhYmNkZWYwMTIzNCIsImlhdCI6MTcwMDAwMDAwMH0.qfJbxpVtbcymaweaCu9fUounkldkMy4HSdllEDlHs5Yq8b11XxiHLguDyvqubXcMGCuVVygCeg2MjU3i0vEuDA", "claim": null },
20
+ { "token": "ig1.eyJzdWIiOiJhYmNkZWYwMTIzNDUiLCJpYXQiOiIxNzAwMDAwMDAwIn0.BI6UT1aTaN01bGZyI9SHc_qIf9S-JlpmNtiBd8by_giZCq8pQIJCBKIutYZW0mmqCF0YHf0NHc9icq1h9Wr7BA", "claim": null },
21
+ { "token": "ig1.eyJzdWIiOiJhYmNkZWYwMTIzNDUifQ.Aa5FeLKazhsr2IvYZ2p5BJmLL1qvQWReXeCKt5gxCq9PqSaAnn8Pa6PJp0jiaXY5FIbc_S9MFrUCeEfQ6vnkBQ", "claim": null },
22
+ { "token": "ig1.eyJzdWIiOjEyMzQ1Njc4OTAxMiwiaWF0IjoxNzAwMDAwMDAwfQ.kb_lMTBvFS4nzTmqLf-T7l9IF8PHaPPkP5HB1T0yd0IRWqYJnIZH2eoKexv7e6aA2PkqCpe-B1FL8Nbu4qPvDg", "claim": null },
23
+ { "token": "ig1.bm90IGpzb24.Ffec27cYSbvKvjlxtSUIFFX7sGo3Zp4BxotW02rFwcJHfJikM51Zfzn3_0Rg8coR4y43iOUA2u-ZOcBJg4mmDA", "claim": null },
24
+ { "token": "ig1.bnVsbA.lFa3FNSkMcf3WCyjdb87YRVdt8h3aSzt4cignx9Rkwq6drwl12m0rlrmzdPkLjsdrQdL1oWZ2E2MGuo2CMeSBw", "claim": null },
25
+ { "token": "ig1.WyJhYmNkZWYwMTIzNDUiLDE3MDAwMDAwMDBd.BWFTD0bHKDBAEWwhYq6F-cWo_HXEC8UvDWkrTmafOycvTZUHvVOjWy_97OYl_pF-svCO1vbeumhF89ybhqv4Dw", "claim": null }
26
+ ],
27
+ "owners": [
28
+ { "host": "sandbox-abcdef012345.sbx.example.test", "owner": "abcdef012345" },
29
+ { "host": "sandbox-abcdef012345.sbx.example.test:443", "owner": "abcdef012345" },
30
+ { "host": "preview-web-app-abcdef012345.sbx.example.test:443", "owner": "abcdef012345" },
31
+ { "host": "port-0f0f0f0f0f0f-abcdef012345.sbx.example.dev", "owner": "abcdef012345" },
32
+ { "host": "public-1a2b3c4d5e6f-abcdef012345.sbx.example.dev", "owner": "abcdef012345" },
33
+ { "host": "SANDBOX-abcdef012345.sbx.example.test", "owner": "abcdef012345" },
34
+ { "host": "-abcdef012345.example.com", "owner": "abcdef012345" },
35
+ { "host": "sandbox-abcdef012345", "owner": "abcdef012345" },
36
+ { "host": "ingress.sbx.example.test", "owner": null },
37
+ { "host": "sbx.example.test", "owner": null },
38
+ { "host": "abcdef012345.local.sbx.example.dev", "owner": null },
39
+ { "host": "sandbox-ABCDEF012345.sbx.example.test", "owner": null },
40
+ { "host": "sandbox-abcdef01234.sbx.example.test", "owner": null },
41
+ { "host": "sandbox-abcdef012345x.sbx.example.test", "owner": null },
42
+ { "host": "sandbox-abcdef012345-.sbx.example.test", "owner": null },
43
+ { "host": "[::1]:8788", "owner": null },
44
+ { "host": "127.0.0.1:8788", "owner": null },
45
+ { "host": "", "owner": null }
46
+ ],
47
+ "lanes": [
48
+ { "host": "sandbox-abcdef012345.sbx.example.test", "method": "GET", "path": "/workspace/media?path=a.mp4", "lane": "bulk" },
49
+ { "host": "sandbox-abcdef012345.sbx.example.test", "method": "POST", "path": "/workspace/upload?path=a", "lane": "bulk" },
50
+ { "host": "sandbox-abcdef012345.sbx.example.test:443", "method": "GET", "path": "/system/runners/git/app/info/refs?service=git-upload-pack", "lane": "bulk" },
51
+ { "host": "sandbox-abcdef012345.sbx.example.test", "method": "POST", "path": "/system/runners/git/app/git-receive-pack", "lane": "bulk" },
52
+ { "host": "sandbox-abcdef012345.sbx.example.test", "method": "GET", "path": "/system/sync/ssh", "lane": "bulk" },
53
+ { "host": "sandbox-abcdef012345.sbx.example.test", "method": "GET", "path": "/extensions/abc/bundle", "lane": "bulk" },
54
+ { "host": "sandbox-abcdef012345.sbx.example.test", "method": "GET", "path": "/diff/raw?a=1?b=2", "lane": "bulk" },
55
+ { "host": "sandbox-abcdef012345.sbx.example.test", "method": "GET", "path": "/bundles/download?ticket=x", "lane": "bulk" },
56
+ { "host": "sandbox-abcdef012345.sbx.example.test", "method": "POST", "path": "/workspace/upload-archive", "lane": "bulk" },
57
+ { "host": "sandbox-abcdef012345.sbx.example.test", "method": "HEAD", "path": "/workspace/raw?path=a", "lane": "bulk" },
58
+ { "host": "sandbox-abcdef012345.sbx.example.test", "method": "OPTIONS", "path": "/workspace/raw", "lane": "bulk" },
59
+ { "host": "sandbox-abcdef012345.sbx.example.test", "method": "OPTIONS", "path": "/system/runners/git/app/git-upload-pack", "lane": "bulk" },
60
+ { "host": "sandbox-abcdef012345.sbx.example.test", "method": "get", "path": "/workspace/raw?path=a", "lane": "bulk" },
61
+ { "host": "sandbox-abcdef012345.sbx.example.test", "method": "GET", "path": "/workspace/thumb?path=a.png", "lane": "interactive" },
62
+ { "host": "sandbox-abcdef012345.sbx.example.test", "method": "POST", "path": "/workspace/upload-diff", "lane": "interactive" },
63
+ { "host": "sandbox-abcdef012345.sbx.example.test", "method": "GET", "path": "/agents", "lane": "interactive" },
64
+ { "host": "sandbox-abcdef012345.sbx.example.test", "method": "GET", "path": "/system/terminal?ticket=t", "lane": "interactive" },
65
+ { "host": "sandbox-abcdef012345.sbx.example.test", "method": "POST", "path": "/workspace/raw", "lane": "interactive" },
66
+ { "host": "sandbox-abcdef012345.sbx.example.test", "method": "GET", "path": "/workspace/raw/", "lane": "interactive" },
67
+ { "host": "sandbox-abcdef012345.sbx.example.test", "method": "GET", "path": "/system/runners/git//info/refs", "lane": "interactive" },
68
+ { "host": "sandbox-abcdef012345.sbx.example.test", "method": "GET", "path": "/x/anything/at/all", "lane": "interactive" },
69
+ { "host": "sandbox-abcdef012345.sbx.example.test", "method": "GET", "path": "/", "lane": "interactive" },
70
+ { "host": "sandbox-abcdef012345.sbx.example.test", "method": "GET", "path": "", "lane": "interactive" },
71
+ { "host": "preview-web-abcdef012345.sbx.example.test", "method": "GET", "path": "/agents", "lane": "bulk" },
72
+ { "host": "port-1a2b3c4d5e6f-abcdef012345.sbx.example.test", "method": "GET", "path": "/", "lane": "bulk" },
73
+ { "host": "Sandbox-abcdef012345.sbx.example.test", "method": "GET", "path": "/agents", "lane": "bulk" },
74
+ { "host": "sbx.example.test", "method": "GET", "path": "/agents", "lane": "bulk" },
75
+ { "host": "", "method": "GET", "path": "/agents", "lane": "bulk" }
76
+ ]
77
+ }
@@ -1,5 +1,14 @@
1
1
  import { generateKeyPairSync } from "node:crypto";
2
- import { hostOwnerId, mintReachabilityGrant, verifyReachabilityGrant } from "./ingress-contract.js";
2
+ import { readFileSync } from "node:fs";
3
+ import { hostOwnerId, type ReachabilityGrant, mintReachabilityGrant, verifyReachabilityGrant } from "./ingress-contract.js";
4
+
5
+ // The edge's rules as both implementations must answer them; the Rust edge reads the same file. Its grants were minted
6
+ // by a throwaway pair whose private half was never kept, so a new grant case takes a new pair and every token again.
7
+ const FIXTURE = JSON.parse(readFileSync(new URL("./ingress-contract.fixture.json", import.meta.url), "utf8")) as {
8
+ readonly publicKey: string;
9
+ readonly grants: readonly { readonly token: string; readonly claim: ReachabilityGrant | null }[];
10
+ readonly owners: readonly { readonly host: string; readonly owner: string | null }[];
11
+ };
3
12
 
4
13
  const pemPair = (): { privateKey: string; publicKey: string } => {
5
14
  const { privateKey, publicKey } = generateKeyPairSync("ed25519");
@@ -31,10 +40,11 @@ describe("reachability grant", () => {
31
40
  expect(verifyReachabilityGrant(keys.publicKey, `${prefix}.${forged}.${signature}`)).toBeUndefined();
32
41
  });
33
42
 
34
- it("answers undefined for garbage rather than throwing", () => {
35
- const keys = pemPair();
36
- for (const junk of ["", "ig1", "ig1..", "nonsense.a.b", "ig2.a.b", "ig1.%%%.%%%"]) {
37
- expect(verifyReachabilityGrant(keys.publicKey, junk)).toBeUndefined();
43
+ it("answers every grant in the shared fixture as it states, and garbage with undefined rather than a throw", () => {
44
+ // Both verdicts, so the loop below can never pass by holding only one kind of case.
45
+ expect(new Set(FIXTURE.grants.map(({ claim }) => (claim === null ? "refused" : "accepted")))).toEqual(new Set(["accepted", "refused"]));
46
+ for (const { token, claim } of FIXTURE.grants) {
47
+ expect({ token, claim: verifyReachabilityGrant(FIXTURE.publicKey, token) ?? null }).toEqual({ token, claim });
38
48
  }
39
49
  });
40
50
 
@@ -44,27 +54,9 @@ describe("reachability grant", () => {
44
54
  });
45
55
 
46
56
  describe("hostOwnerId", () => {
47
- it("owns the daemon's own name and every labelled name", () => {
48
- expect(hostOwnerId(`sandbox-${SANDBOX_ID}.sbx.example.dev`)).toBe(SANDBOX_ID);
49
- expect(hostOwnerId(`preview-operator-${SANDBOX_ID}.sbx.example.dev`)).toBe(SANDBOX_ID);
50
- expect(hostOwnerId(`port-0f0f0f0f0f0f-${SANDBOX_ID}.sbx.example.dev`)).toBe(SANDBOX_ID);
51
- expect(hostOwnerId(`public-1a2b3c4d5e6f-${SANDBOX_ID}.sbx.example.dev`)).toBe(SANDBOX_ID);
52
- });
53
-
54
- it("ignores a port suffix", () => {
55
- expect(hostOwnerId(`sandbox-${SANDBOX_ID}.sbx.example.dev:443`)).toBe(SANDBOX_ID);
56
- });
57
-
58
- it("owns nothing that does not end in a 12-hex tail", () => {
59
- // The ingress's own door, the zone apex, the loopback name's bare-id label, and a near-miss tail.
60
- for (const host of [
61
- "ingress.sbx.example.dev",
62
- "sbx.example.dev",
63
- `${SANDBOX_ID}.local.sbx.example.dev`,
64
- "sandbox-abc123def45.sbx.example.dev",
65
- "",
66
- ]) {
67
- expect(hostOwnerId(host)).toBeUndefined();
57
+ it("owns every host in the shared fixture as it states, port or not", () => {
58
+ for (const { host, owner } of FIXTURE.owners) {
59
+ expect({ host, owner: hostOwnerId(host) ?? null }).toEqual({ host, owner });
68
60
  }
69
61
  });
70
62
  });
@@ -73,31 +73,12 @@ export const INGRESS_GRANT_HEADER = "x-intentic-grant";
73
73
  export const ENV_INGRESS_URL = "INGRESS_URL";
74
74
  export const ENV_SANDBOX_GRANT = "SANDBOX_GRANT";
75
75
 
76
- // The daemon-side surface
77
-
78
- // Daemon-side tunnel behavior this contract requires: dial, register, forward to the loopback listener, and reconnect
79
- // forever with backoff, never giving up. `close()` is for shutdown and tests.
80
- export interface IngressTunnelOptions {
81
- // e.g. https://ingress.<zone>; the daemon derives the wss:// door itself via INGRESS_TUNNEL_PATH.
82
- readonly url: string;
83
- readonly grant: string;
84
- // The daemon's own loopback listener; every h2 stream lands there as a plain HTTP/1.1 request or upgrade.
85
- readonly targetPort: number;
86
- readonly log: (message: string, error?: unknown) => void;
87
- }
88
-
89
- export interface IngressTunnelHandle {
90
- readonly close: () => Promise<void>;
91
- // For /health and the boot log: whether the tunnel currently holds a registered session.
92
- readonly connected: () => boolean;
93
- }
94
-
95
- export type StartIngressTunnel = (options: IngressTunnelOptions) => IngressTunnelHandle;
96
-
97
- // What the ingress side must also honor, enforced by ingress-protocol tests:
76
+ // What the edge (_platform/ingress, Rust) must also honor, held by its suites and by ingress-contract.fixture.json:
98
77
  // - Register: verify the grant offline; if PLATFORM_URL is set, check the sandbox exists (GET /api/reachability/<id>)
99
78
  // and cache the answer, failing open if the platform doesn't respond. A 404 refuses the tunnel.
100
79
  // - Displacement: a new tunnel for an id closes the old session (code 4001) and takes the registration.
80
+ // - Lanes: a tunnel naming the bulk lane (tunnel-lanes.ts) is held beside the interactive one and displaces only its
81
+ // own kind; only the interactive lane is the sandbox's home in the cluster.
101
82
  // - Liveness: WebSocket ping every 15s; a peer silent for 45s is unregistered.
102
83
  // - Routing: host maps via hostOwnerId to a registered tunnel; no tunnel answers 502 naming the sandbox label, with
103
84
  // the verdict header and CORS of edge-verdict.ts, and admits the preflight of the request it is about to refuse.