@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.
- package/README.md +9 -4
- package/dist/chores/chores.js +6 -6
- package/dist/chores/chores.js.map +1 -1
- package/dist/chores/fix-deps.d.ts.map +1 -1
- package/dist/chores/fix-deps.js +3 -2
- package/dist/chores/fix-deps.js.map +1 -1
- package/dist/chores/prompt.d.ts.map +1 -1
- package/dist/chores/prompt.js +3 -2
- package/dist/chores/prompt.js.map +1 -1
- package/dist/contracts/accounts.contract.d.ts +12 -0
- package/dist/contracts/accounts.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.d.ts +601 -0
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +30 -2
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/device.contract.d.ts +1 -0
- package/dist/contracts/device.contract.d.ts.map +1 -1
- package/dist/contracts/extensions.contract.d.ts +2 -0
- package/dist/contracts/extensions.contract.d.ts.map +1 -1
- package/dist/contracts/offload.contract.d.ts +107 -0
- package/dist/contracts/offload.contract.d.ts.map +1 -0
- package/dist/contracts/offload.contract.js +53 -0
- package/dist/contracts/offload.contract.js.map +1 -0
- package/dist/contracts/ports.contract.d.ts +5 -0
- package/dist/contracts/ports.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.d.ts +66 -0
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.js +3 -1
- package/dist/contracts/runner.contract.js.map +1 -1
- package/dist/contracts/settings.contract.d.ts +8 -0
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +95 -0
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/translator.contract.d.ts +16 -0
- package/dist/contracts/translator.contract.d.ts.map +1 -1
- package/dist/contracts/workspace.contract.d.ts +146 -0
- package/dist/contracts/workspace.contract.d.ts.map +1 -1
- package/dist/contracts/workspace.contract.js +40 -3
- package/dist/contracts/workspace.contract.js.map +1 -1
- package/dist/documents/conversions.d.ts +81 -0
- package/dist/documents/conversions.d.ts.map +1 -0
- package/dist/documents/conversions.js +213 -0
- package/dist/documents/conversions.js.map +1 -0
- package/dist/documents/index.d.ts +3 -0
- package/dist/documents/index.d.ts.map +1 -0
- package/dist/documents/index.js +3 -0
- package/dist/documents/index.js.map +1 -0
- package/dist/documents/passthrough.d.ts +13 -0
- package/dist/documents/passthrough.d.ts.map +1 -0
- package/dist/documents/passthrough.js +101 -0
- package/dist/documents/passthrough.js.map +1 -0
- package/dist/events/land-breakage.d.ts +3 -0
- package/dist/events/land-breakage.d.ts.map +1 -1
- package/dist/events/land-breakage.js +3 -0
- package/dist/events/land-breakage.js.map +1 -1
- package/dist/events/land-conflict.d.ts +3 -0
- package/dist/events/land-conflict.d.ts.map +1 -0
- package/dist/events/land-conflict.js +4 -0
- package/dist/events/land-conflict.js.map +1 -0
- package/dist/events/system-events.d.ts +66 -0
- package/dist/events/system-events.d.ts.map +1 -1
- package/dist/events/system-events.js +3 -1
- package/dist/events/system-events.js.map +1 -1
- package/dist/events/verify-nudge.d.ts.map +1 -1
- package/dist/events/verify-nudge.js.map +1 -1
- package/dist/front/generated/wire.d.ts +63 -0
- package/dist/front/generated/wire.d.ts.map +1 -1
- package/dist/front/terminal-frames.d.ts +17 -0
- package/dist/front/terminal-frames.d.ts.map +1 -0
- package/dist/front/terminal-frames.js +79 -0
- package/dist/front/terminal-frames.js.map +1 -0
- package/dist/ids/conversation-ids.d.ts +2 -0
- package/dist/ids/conversation-ids.d.ts.map +1 -1
- package/dist/ids/conversation-ids.js +2 -0
- package/dist/ids/conversation-ids.js.map +1 -1
- package/dist/index.d.ts +1002 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/protocol/container-requirements.d.ts +0 -1
- package/dist/protocol/container-requirements.d.ts.map +1 -1
- package/dist/protocol/container-requirements.js +1 -2
- package/dist/protocol/container-requirements.js.map +1 -1
- package/dist/protocol/ingress-contract.d.ts +0 -11
- package/dist/protocol/ingress-contract.d.ts.map +1 -1
- package/dist/protocol/raw-routes.d.ts +36 -4
- package/dist/protocol/raw-routes.d.ts.map +1 -1
- package/dist/protocol/raw-routes.js +18 -13
- package/dist/protocol/raw-routes.js.map +1 -1
- package/dist/protocol/route-meta.d.ts +2 -0
- package/dist/protocol/route-meta.d.ts.map +1 -1
- package/dist/protocol/route-meta.js.map +1 -1
- package/dist/protocol/runner-protocol.d.ts +34 -0
- package/dist/protocol/runner-protocol.d.ts.map +1 -1
- package/dist/protocol/runner-protocol.js +33 -0
- package/dist/protocol/runner-protocol.js.map +1 -1
- package/dist/protocol/tunnel-lanes.d.ts +10 -0
- package/dist/protocol/tunnel-lanes.d.ts.map +1 -0
- package/dist/protocol/tunnel-lanes.js +16 -0
- package/dist/protocol/tunnel-lanes.js.map +1 -0
- package/dist/schemas/activity.d.ts.map +1 -1
- package/dist/schemas/activity.js.map +1 -1
- package/dist/schemas/agents.d.ts +73 -0
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +33 -2
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/automations.d.ts +19 -0
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/devices.d.ts +2 -0
- package/dist/schemas/devices.d.ts.map +1 -1
- package/dist/schemas/extension-updates.d.ts +4 -0
- package/dist/schemas/extension-updates.d.ts.map +1 -1
- package/dist/schemas/git/git.d.ts +16 -0
- package/dist/schemas/git/git.d.ts.map +1 -1
- package/dist/schemas/git/git.js +1 -0
- package/dist/schemas/git/git.js.map +1 -1
- package/dist/schemas/hosts.d.ts +8 -0
- package/dist/schemas/hosts.d.ts.map +1 -1
- package/dist/schemas/hosts.js +3 -0
- package/dist/schemas/hosts.js.map +1 -1
- package/dist/schemas/metrics.d.ts +7 -0
- package/dist/schemas/metrics.d.ts.map +1 -1
- package/dist/schemas/metrics.js +1 -0
- package/dist/schemas/metrics.js.map +1 -1
- package/dist/schemas/offload.d.ts +62 -0
- package/dist/schemas/offload.d.ts.map +1 -0
- package/dist/schemas/offload.js +23 -0
- package/dist/schemas/offload.js.map +1 -0
- package/dist/schemas/personas.js +3 -3
- package/dist/schemas/personas.js.map +1 -1
- package/dist/schemas/ports.d.ts +10 -0
- package/dist/schemas/ports.d.ts.map +1 -1
- package/dist/schemas/ports.js +8 -0
- package/dist/schemas/ports.js.map +1 -1
- package/dist/schemas/providers/plan-limits.d.ts +29 -0
- package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
- package/dist/schemas/providers/plan-limits.js +7 -0
- package/dist/schemas/providers/plan-limits.js.map +1 -1
- package/dist/schemas/providers/provider-oauth.d.ts +12 -0
- package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
- package/dist/schemas/settings-history.d.ts +28 -0
- package/dist/schemas/settings-history.d.ts.map +1 -0
- package/dist/schemas/settings-history.js +39 -0
- package/dist/schemas/settings-history.js.map +1 -0
- package/dist/schemas/settings.d.ts +4 -0
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +10 -3
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/system.d.ts +24 -0
- package/dist/schemas/system.d.ts.map +1 -1
- package/dist/schemas/system.js +15 -0
- package/dist/schemas/system.js.map +1 -1
- package/dist/schemas/workspace/mainline.d.ts +371 -0
- package/dist/schemas/workspace/mainline.d.ts.map +1 -0
- package/dist/schemas/workspace/mainline.js +134 -0
- package/dist/schemas/workspace/mainline.js.map +1 -0
- package/dist/schemas/workspace/workspace-tree.d.ts +23 -0
- package/dist/schemas/workspace/workspace-tree.d.ts.map +1 -1
- package/dist/schemas/workspace/workspace-tree.js +34 -0
- package/dist/schemas/workspace/workspace-tree.js.map +1 -1
- package/dist/state/definition.d.ts +20 -4
- package/dist/state/definition.d.ts.map +1 -1
- package/dist/state/definition.js.map +1 -1
- package/dist/state/history-state.d.ts.map +1 -1
- package/dist/state/history-state.js +5 -0
- package/dist/state/history-state.js.map +1 -1
- package/dist/state/runtime-state.d.ts +3 -0
- package/dist/state/runtime-state.d.ts.map +1 -1
- package/dist/state/runtime-state.js +1 -0
- package/dist/state/runtime-state.js.map +1 -1
- package/dist/state/workspace-state.d.ts +22 -1
- package/dist/state/workspace-state.d.ts.map +1 -1
- package/dist/state/workspace-state.js +27 -1
- package/dist/state/workspace-state.js.map +1 -1
- package/dist/text/transcript-fold.d.ts +1 -0
- package/dist/text/transcript-fold.d.ts.map +1 -1
- package/dist/text/transcript-fold.js +10 -1
- package/dist/text/transcript-fold.js.map +1 -1
- package/package.json +28 -27
- package/src/chores/chores.ts +6 -6
- package/src/chores/fix-deps.ts +3 -2
- package/src/chores/prompt.ts +3 -2
- package/src/contracts/agents.contract.ts +36 -1
- package/src/contracts/offload.contract.ts +60 -0
- package/src/contracts/runner.contract.ts +7 -1
- package/src/contracts/workspace.contract.ts +54 -2
- package/src/documents/conversions.ts +402 -0
- package/src/documents/index.ts +5 -0
- package/src/documents/passthrough.ts +139 -0
- package/src/events/land-breakage.ts +12 -0
- package/src/events/land-conflict.ts +12 -0
- package/src/events/system-events.ts +9 -3
- package/src/events/verify-nudge.ts +4 -3
- package/src/front/generated/wire.ts +35 -4
- package/src/front/terminal-frames.fixture.json +55 -0
- package/src/front/terminal-frames.test.ts +63 -0
- package/src/front/terminal-frames.ts +102 -0
- package/src/ids/conversation-ids.ts +7 -0
- package/src/index.ts +7 -1
- package/src/protocol/container-requirements.test.ts +2 -3
- package/src/protocol/container-requirements.ts +1 -7
- package/src/protocol/ingress-contract.fixture.json +77 -0
- package/src/protocol/ingress-contract.test.ts +18 -26
- package/src/protocol/ingress-contract.ts +3 -22
- package/src/protocol/raw-routes.ts +21 -14
- package/src/protocol/route-meta.ts +4 -0
- package/src/protocol/runner-protocol.ts +53 -0
- package/src/protocol/tunnel-lanes.json +136 -0
- package/src/protocol/tunnel-lanes.test.ts +30 -0
- package/src/protocol/tunnel-lanes.ts +32 -0
- package/src/schemas/activity.ts +3 -1
- package/src/schemas/agents.ts +52 -3
- package/src/schemas/git/git.ts +5 -0
- package/src/schemas/hosts.ts +8 -0
- package/src/schemas/metrics.ts +2 -0
- package/src/schemas/offload.ts +44 -0
- package/src/schemas/personas.ts +3 -3
- package/src/schemas/ports.ts +10 -0
- package/src/schemas/providers/plan-limits.ts +13 -0
- package/src/schemas/settings-history.ts +61 -0
- package/src/schemas/settings.ts +40 -19
- package/src/schemas/system.ts +17 -0
- package/src/schemas/workspace/mainline.ts +201 -0
- package/src/schemas/workspace/workspace-tree.ts +47 -0
- package/src/state/definition.ts +3 -2
- package/src/state/history-state.ts +15 -0
- package/src/state/runtime-state.ts +4 -0
- package/src/state/workspace-state.ts +35 -1
- package/src/text/transcript-fold.test.ts +33 -0
- package/src/text/transcript-fold.ts +14 -1
- package/dist/protocol/ingress-protocol.d.ts +0 -30
- package/dist/protocol/ingress-protocol.d.ts.map +0 -1
- package/dist/protocol/ingress-protocol.js +0 -300
- package/dist/protocol/ingress-protocol.js.map +0 -1
- package/dist/protocol/terminal-protocol.d.ts +0 -18
- package/dist/protocol/terminal-protocol.d.ts.map +0 -1
- package/dist/protocol/terminal-protocol.js +0 -2
- package/dist/protocol/terminal-protocol.js.map +0 -1
- package/dist/protocol/tunnel-heartbeat.d.ts +0 -19
- package/dist/protocol/tunnel-heartbeat.d.ts.map +0 -1
- package/dist/protocol/tunnel-heartbeat.js +0 -41
- package/dist/protocol/tunnel-heartbeat.js.map +0 -1
- package/src/protocol/ingress-protocol.test.ts +0 -579
- package/src/protocol/ingress-protocol.ts +0 -469
- package/src/protocol/terminal-protocol.ts +0 -13
- package/src/protocol/tunnel-heartbeat.test.ts +0 -75
- 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
|
-
|
|
42
|
-
|
|
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 {
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
//
|
|
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.
|