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