@opengeni/runtime 0.2.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/dist/chunk-2PO56VAL.js +3478 -0
- package/dist/chunk-2PO56VAL.js.map +1 -0
- package/dist/index.d.ts +912 -0
- package/dist/index.js +3663 -0
- package/dist/index.js.map +1 -0
- package/dist/sandbox/index.d.ts +1738 -0
- package/dist/sandbox/index.js +187 -0
- package/dist/sandbox/index.js.map +1 -0
- package/package.json +49 -0
- package/src/bundled_hashicorp_terraform_skills/LICENSE +373 -0
- package/src/bundled_hashicorp_terraform_skills/README.md +18 -0
- package/src/bundled_hashicorp_terraform_skills/UPSTREAM_GIT_SHA +1 -0
- package/src/bundled_hashicorp_terraform_skills/azure-verified-modules/SKILL.md +613 -0
- package/src/bundled_hashicorp_terraform_skills/checkov/SKILL.md +43 -0
- package/src/bundled_hashicorp_terraform_skills/refactor-module/SKILL.md +538 -0
- package/src/bundled_hashicorp_terraform_skills/social-media-marketing/SKILL.md +35 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-search-import/SKILL.md +372 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-search-import/references/MANUAL-IMPORT.md +113 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-search-import/scripts/list_resources.sh +38 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/SKILL.md +480 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/api-monitoring.md +543 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/component-blocks.md +476 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/deployment-blocks.md +391 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/examples.md +1529 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/linked-stacks.md +187 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/troubleshooting.md +671 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-style-guide/SKILL.md +353 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-test/SKILL.md +451 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-test/references/CI_CD.md +80 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-test/references/EXAMPLES.md +314 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-test/references/MOCK_PROVIDERS.md +171 -0
- package/src/codex-tool-search.ts +267 -0
- package/src/context-compaction.ts +538 -0
- package/src/history-sanitizer.ts +719 -0
- package/src/index.ts +3299 -0
- package/src/sandbox/capabilities.ts +69 -0
- package/src/sandbox/channel-a.ts +1031 -0
- package/src/sandbox/display-stack.ts +231 -0
- package/src/sandbox/errors.ts +34 -0
- package/src/sandbox/index.ts +832 -0
- package/src/sandbox/providers/blaxel.ts +35 -0
- package/src/sandbox/providers/cloudflare.ts +24 -0
- package/src/sandbox/providers/daytona.ts +34 -0
- package/src/sandbox/providers/docker.ts +17 -0
- package/src/sandbox/providers/e2b.ts +36 -0
- package/src/sandbox/providers/index.ts +107 -0
- package/src/sandbox/providers/local.ts +13 -0
- package/src/sandbox/providers/modal.ts +55 -0
- package/src/sandbox/providers/none.ts +13 -0
- package/src/sandbox/providers/runloop.ts +32 -0
- package/src/sandbox/providers/selfhosted.ts +96 -0
- package/src/sandbox/providers/types.ts +38 -0
- package/src/sandbox/providers/vercel.ts +29 -0
- package/src/sandbox/recording.ts +286 -0
- package/src/sandbox/routing/backend-resolver.ts +189 -0
- package/src/sandbox/routing/routing-session.ts +455 -0
- package/src/sandbox/select.ts +371 -0
- package/src/sandbox/selfhosted/capabilities.ts +255 -0
- package/src/sandbox/selfhosted/control-rpc.ts +351 -0
- package/src/sandbox/selfhosted/session.ts +930 -0
- package/src/sandbox/selfhosted/testing.ts +230 -0
- package/src/sandbox/stream-port.ts +185 -0
- package/src/sandbox/stream-token.ts +90 -0
- package/src/sandbox/terminal-server.ts +203 -0
- package/src/sandbox-computer.ts +835 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// Backend selection + capability negotiation/degradation (module 03 §0, §5).
|
|
2
|
+
//
|
|
3
|
+
// negotiateCapabilities() turns a static CapabilityDescriptor + runtime context
|
|
4
|
+
// (the selected OS, the lease liveness/epoch, and the deployment's desktop
|
|
5
|
+
// policy) into a coherent SessionCapabilities document. The load-bearing rule
|
|
6
|
+
// (master-spine Part D): a capability cell is ALWAYS present — when unavailable
|
|
7
|
+
// it is `available:false` + a typed `reason`, NEVER absent. Degradation is a
|
|
8
|
+
// value, not a silent drop.
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
CAPABILITY_DESCRIPTORS,
|
|
12
|
+
type CapabilityDescriptor,
|
|
13
|
+
type CapabilityUnavailableReason,
|
|
14
|
+
type SandboxBackend,
|
|
15
|
+
type SandboxOs,
|
|
16
|
+
type SessionCapabilities,
|
|
17
|
+
} from "@opengeni/contracts";
|
|
18
|
+
|
|
19
|
+
export interface NegotiationContext {
|
|
20
|
+
sessionId: string;
|
|
21
|
+
backend: SandboxBackend;
|
|
22
|
+
os: SandboxOs;
|
|
23
|
+
/** Current lease liveness; cold means nothing is provisioned yet. */
|
|
24
|
+
liveness: "cold" | "warming" | "warm" | "draining";
|
|
25
|
+
/** The lease epoch echoed on viewer heartbeats (the split-brain fence). */
|
|
26
|
+
leaseEpoch: number;
|
|
27
|
+
/** The deployment desktop toggle (settings.sandboxDesktopEnabled). */
|
|
28
|
+
desktopEnabled: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* The HUMAN take-control toggle (settings.sandboxDesktopInteractive). When true
|
|
31
|
+
* (default) and the desktop cell is available, the negotiated DesktopStream.mode
|
|
32
|
+
* is "interactive" — the noVNC viewer can drive mouse+keyboard into :0 (the box's
|
|
33
|
+
* x11vnc runs without -viewonly). When false the cell reports mode "read-only"
|
|
34
|
+
* and the client disables the "Take control" affordance (a genuinely read-only
|
|
35
|
+
* deployment). Independent of `computerUseReadOnly`, which gates the AGENT
|
|
36
|
+
* driver, not the human viewer plane. Defaults to true so a caller that never
|
|
37
|
+
* threads it (e.g. headless tests) still gets the interactive plane when the
|
|
38
|
+
* desktop is available.
|
|
39
|
+
*/
|
|
40
|
+
desktopInteractive?: boolean;
|
|
41
|
+
/** The deployment computer-use toggle (settings.computerUseEnabled). The agent
|
|
42
|
+
* drives :0 via xdotool/scrot; availability tracks desktop. Defaults to true. */
|
|
43
|
+
computerUseEnabled?: boolean;
|
|
44
|
+
/** Whether the agent computer-use driver is gated to no-op input
|
|
45
|
+
* (settings.computerUseReadOnly). v1 default false (the agent clicks/types). */
|
|
46
|
+
computerUseReadOnly?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Whether a scoped-stream-token secret is resolvable (I8/OD-8). When desktop
|
|
49
|
+
* is enabled but this is false (no streamTokenSecret AND no delegationSecret),
|
|
50
|
+
* the desktop plane GRACEFULLY DEGRADES to transport:null — the deployment
|
|
51
|
+
* boots, but the pixel plane cannot mint scoped tokens. Defaults to true so a
|
|
52
|
+
* caller that never threads it (e.g. headless tests) is unaffected.
|
|
53
|
+
*/
|
|
54
|
+
streamTokenSecretAvailable?: boolean;
|
|
55
|
+
/** Whether the calling principal has acknowledged the un-redacted desktop
|
|
56
|
+
* pixels (and, for a shared box, the shared-exposure disclosure). When the
|
|
57
|
+
* box is shared this must be the SHARED acknowledgment; a bare un-redacted ack
|
|
58
|
+
* does not satisfy a shared box. */
|
|
59
|
+
desktopAcknowledged?: boolean;
|
|
60
|
+
/** True when the box's group has >1 session: watching this desktop also shows
|
|
61
|
+
* the sibling sessions' agents on the one :0 framebuffer (addendum E.1). */
|
|
62
|
+
shared?: boolean;
|
|
63
|
+
/** The OTHER sessions whose agents may appear on the shared desktop — IDS
|
|
64
|
+
* ONLY, never their conversation/metadata (stress g). Empty for a solo box. */
|
|
65
|
+
sharedSessionIds?: string[];
|
|
66
|
+
/**
|
|
67
|
+
* The minted pixel-plane endpoint (P4.2): the direct-to-provider WS URL + the
|
|
68
|
+
* scoped stream token + its expiry + the framebuffer geometry. Threaded by the
|
|
69
|
+
* API-direct handshake AFTER it has resumed the box, ensured the display stack,
|
|
70
|
+
* and resolved the provider tunnel. When ABSENT (the negotiation-only read, a
|
|
71
|
+
* cold lease, or a degraded desktop) the DesktopStream cell reports url/token/
|
|
72
|
+
* expiresAt as null — the capability is advertised, the live address is not yet
|
|
73
|
+
* minted (the caller POSTs to /viewers to mint it). Presence does NOT override
|
|
74
|
+
* the gates: a degraded/cold/unacked desktop still reports transport:null and
|
|
75
|
+
* the minted endpoint is dropped.
|
|
76
|
+
*/
|
|
77
|
+
desktopStream?: {
|
|
78
|
+
url: string;
|
|
79
|
+
token: string;
|
|
80
|
+
expiresAt: string;
|
|
81
|
+
resolution: [number, number];
|
|
82
|
+
};
|
|
83
|
+
/** The deployment terminal toggle (settings.sandboxTerminalEnabled). The REAL
|
|
84
|
+
* PTY (ttyd pty-ws) is gated on this + a real-PTY backend; when off the
|
|
85
|
+
* Terminal cell still advertises the read-only sse-events firehose. Defaults to
|
|
86
|
+
* true so a caller that never threads it is unaffected. */
|
|
87
|
+
terminalEnabled?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* The minted terminal-plane endpoint (P5.t): the direct-to-provider ttyd
|
|
90
|
+
* PTY-over-websocket URL + the scoped stream token + its expiry. Threaded by the
|
|
91
|
+
* API-direct handshake AFTER it has resumed the box, ensured the terminal
|
|
92
|
+
* server, and resolved the provider tunnel (mintTerminalStream) — SYMMETRIC with
|
|
93
|
+
* `desktopStream`. When ABSENT (the negotiation-only read, a cold lease, or a
|
|
94
|
+
* degraded terminal) the Terminal cell reports url/token/expiresAt as null and
|
|
95
|
+
* falls back to transport "sse-events" (the read-only firehose) — the caller
|
|
96
|
+
* POSTs to /viewers to mint the live pty-ws address.
|
|
97
|
+
*/
|
|
98
|
+
terminalStream?: {
|
|
99
|
+
url: string;
|
|
100
|
+
token: string;
|
|
101
|
+
expiresAt: string;
|
|
102
|
+
};
|
|
103
|
+
/** Override the negotiation clock (tests). */
|
|
104
|
+
now?: Date;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Resolve the descriptor for a backend. Throws on an unknown backend rather than
|
|
109
|
+
* returning a half-formed default (the registry is the single source of truth).
|
|
110
|
+
*/
|
|
111
|
+
export function selectBackend(backend: SandboxBackend): CapabilityDescriptor {
|
|
112
|
+
const descriptor = CAPABILITY_DESCRIPTORS[backend];
|
|
113
|
+
if (!descriptor) {
|
|
114
|
+
throw new Error(`Unknown sandbox backend "${backend}"`);
|
|
115
|
+
}
|
|
116
|
+
return descriptor;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** True iff the descriptor lists the requested OS as supported. */
|
|
120
|
+
export function backendSupportsOs(descriptor: CapabilityDescriptor, os: SandboxOs): boolean {
|
|
121
|
+
return descriptor.os.supported.includes(os);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* True iff the backend can serve the Channel-B desktop pixel plane at all — i.e.
|
|
126
|
+
* its static descriptor advertises DesktopStream as available. The gate the
|
|
127
|
+
* worker / API use before launching the display stack (so a headless-only
|
|
128
|
+
* backend like cloudflare/vercel/none never tries). This is the STATIC
|
|
129
|
+
* feasibility only; the runtime `sandboxDesktopEnabled` policy toggle and the
|
|
130
|
+
* stream-token-secret gate are layered on by the caller / negotiateCapabilities.
|
|
131
|
+
*
|
|
132
|
+
* Accepts EITHER the SandboxBackend enum value (e.g. "local") OR the SDK
|
|
133
|
+
* client backendId (e.g. "unix_local") — they diverge for the local backend —
|
|
134
|
+
* so a caller holding only `established.backendId` resolves correctly.
|
|
135
|
+
*/
|
|
136
|
+
export function desktopCapableBackend(backend: SandboxBackend | string): boolean {
|
|
137
|
+
const direct = CAPABILITY_DESCRIPTORS[backend as SandboxBackend];
|
|
138
|
+
if (direct) {
|
|
139
|
+
return direct.capabilities.DesktopStream.available === true;
|
|
140
|
+
}
|
|
141
|
+
// Fall back to a backendId lookup (the SDK client id, which differs from the
|
|
142
|
+
// enum key for `local`/`unix_local`).
|
|
143
|
+
for (const descriptor of Object.values(CAPABILITY_DESCRIPTORS)) {
|
|
144
|
+
if (descriptor.backendId === backend) {
|
|
145
|
+
return descriptor.capabilities.DesktopStream.available === true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Negotiate a coherent SessionCapabilities document for (backend, os). Every
|
|
153
|
+
* capability is reported with availability + a reason-when-unavailable; nothing
|
|
154
|
+
* is ever absent. The reason precedence is: os_unsupported (the OS axis can't be
|
|
155
|
+
* served at all) > the per-capability static feasibility > policy/liveness gates.
|
|
156
|
+
*/
|
|
157
|
+
export function negotiateCapabilities(ctx: NegotiationContext): SessionCapabilities {
|
|
158
|
+
const descriptor = selectBackend(ctx.backend);
|
|
159
|
+
const osSupported = backendSupportsOs(descriptor, ctx.os);
|
|
160
|
+
const negotiatedAt = (ctx.now ?? new Date()).toISOString();
|
|
161
|
+
|
|
162
|
+
// The dominant degrade: an unsupported OS knocks out every capability with a
|
|
163
|
+
// single coherent reason.
|
|
164
|
+
const osReason: CapabilityUnavailableReason | null = osSupported ? null : "os_unsupported";
|
|
165
|
+
|
|
166
|
+
const fileSystem = (() => {
|
|
167
|
+
if (osReason) {
|
|
168
|
+
return { available: false, readOnly: true, root: descriptor.workspaceRoot, pathSep: "/" as const, treeMode: "lazy" as const, reason: osReason };
|
|
169
|
+
}
|
|
170
|
+
const cap = descriptor.capabilities.FileSystem;
|
|
171
|
+
return {
|
|
172
|
+
available: cap.available,
|
|
173
|
+
readOnly: cap.readOnly,
|
|
174
|
+
root: descriptor.workspaceRoot,
|
|
175
|
+
pathSep: "/" as const,
|
|
176
|
+
treeMode: "lazy" as const,
|
|
177
|
+
reason: cap.available ? null : ("backend_unsupported" as const),
|
|
178
|
+
};
|
|
179
|
+
})();
|
|
180
|
+
|
|
181
|
+
const terminal = (() => {
|
|
182
|
+
const cap = descriptor.capabilities.Terminal;
|
|
183
|
+
if (osReason) {
|
|
184
|
+
return { transport: null, ptyCapable: false, shell: "/bin/bash", url: null, token: null, expiresAt: null, reason: osReason };
|
|
185
|
+
}
|
|
186
|
+
if (!cap.available) {
|
|
187
|
+
return { transport: null, ptyCapable: false, shell: "/bin/bash", url: null, token: null, expiresAt: null, reason: "backend_unsupported" as const };
|
|
188
|
+
}
|
|
189
|
+
// The REAL PTY (ttyd pty-ws) rides the SAME tunnel as the desktop, so it is
|
|
190
|
+
// gated identically: a real-PTY backend (cap.pty), the terminal policy toggle
|
|
191
|
+
// ON, and a live box. Until those hold the cell advertises the read-only
|
|
192
|
+
// sse-events firehose (Channel-A command.output still works) with a typed
|
|
193
|
+
// reason — degradation is a value, never an absent capability.
|
|
194
|
+
// - terminal off -> sse-events + disabled_by_policy.
|
|
195
|
+
// - cold lease + NO mint -> sse-events + lease_cold (no live pty-ws address;
|
|
196
|
+
// the caller mints it via mintTerminalStream at
|
|
197
|
+
// viewer attach).
|
|
198
|
+
// - not a real-PTY backend-> sse-events (no reason; the firehose IS the cap).
|
|
199
|
+
// A PRESENT minted pty-ws url (ctx.terminalStream) is ITSELF proof of liveness:
|
|
200
|
+
// the box (Modal-warm OR selfhosted-online) actually served the ttyd port, so a
|
|
201
|
+
// cold MODAL-GROUP lease liveness must NOT degrade it. lease_cold only fires
|
|
202
|
+
// when nothing was minted. A selfhosted-active session has no warm Modal lease
|
|
203
|
+
// (liveness "cold") yet mints a valid RELAY pty-ws cell — honour it.
|
|
204
|
+
const ptyCapable = cap.pty;
|
|
205
|
+
let transport: "pty-ws" | "sse-events" = ptyCapable ? "pty-ws" : "sse-events";
|
|
206
|
+
let reason: CapabilityUnavailableReason | null = null;
|
|
207
|
+
if (ptyCapable && ctx.terminalEnabled === false) {
|
|
208
|
+
transport = "sse-events";
|
|
209
|
+
reason = "disabled_by_policy";
|
|
210
|
+
} else if (ptyCapable && ctx.liveness === "cold" && !ctx.terminalStream) {
|
|
211
|
+
transport = "sse-events";
|
|
212
|
+
reason = "lease_cold";
|
|
213
|
+
}
|
|
214
|
+
// The minted pty-ws endpoint is folded in ONLY when the terminal is actually
|
|
215
|
+
// serving pty-ws (the gates passed). When absent the cell advertises the
|
|
216
|
+
// capability with a null live address — the caller mints it via POST /viewers.
|
|
217
|
+
const minted = transport === "pty-ws" ? ctx.terminalStream : undefined;
|
|
218
|
+
return {
|
|
219
|
+
transport,
|
|
220
|
+
ptyCapable,
|
|
221
|
+
shell: "/bin/bash",
|
|
222
|
+
url: minted?.url ?? null,
|
|
223
|
+
token: minted?.token ?? null,
|
|
224
|
+
expiresAt: minted?.expiresAt ?? null,
|
|
225
|
+
reason,
|
|
226
|
+
};
|
|
227
|
+
})();
|
|
228
|
+
|
|
229
|
+
const git = (() => {
|
|
230
|
+
const cap = descriptor.capabilities.Git;
|
|
231
|
+
if (osReason) {
|
|
232
|
+
return { available: false, repos: [], reason: osReason };
|
|
233
|
+
}
|
|
234
|
+
return { available: cap.available, repos: [], reason: cap.available ? null : ("backend_unsupported" as const) };
|
|
235
|
+
})();
|
|
236
|
+
|
|
237
|
+
const desktop = (() => {
|
|
238
|
+
const cap = descriptor.capabilities.DesktopStream;
|
|
239
|
+
// Reason precedence: OS > backend-tier feasibility > policy disable >
|
|
240
|
+
// stream-token-secret > cold lease WITHOUT a mint.
|
|
241
|
+
let reason: CapabilityUnavailableReason | null = null;
|
|
242
|
+
let available = cap.available;
|
|
243
|
+
if (osReason) {
|
|
244
|
+
available = false;
|
|
245
|
+
reason = osReason;
|
|
246
|
+
} else if (!cap.available) {
|
|
247
|
+
available = false;
|
|
248
|
+
// Headless tiers expose the typed tier_headless reason; dev/none are
|
|
249
|
+
// backend_unsupported for desktop.
|
|
250
|
+
reason = descriptor.tier === "headless" ? "tier_headless" : "backend_unsupported";
|
|
251
|
+
} else if (!ctx.desktopEnabled) {
|
|
252
|
+
available = false;
|
|
253
|
+
reason = "disabled_by_policy";
|
|
254
|
+
} else if (ctx.streamTokenSecretAvailable === false) {
|
|
255
|
+
// Graceful degrade (I8/OD-8): desktop is enabled + backend-capable, but no
|
|
256
|
+
// stream-token secret is resolvable, so no scoped token can be minted. The
|
|
257
|
+
// deployment boots; the desktop cell reports transport:null + a typed
|
|
258
|
+
// reason rather than crashing the API.
|
|
259
|
+
available = false;
|
|
260
|
+
reason = "disabled_by_policy";
|
|
261
|
+
} else if (ctx.liveness === "cold" && !ctx.desktopStream) {
|
|
262
|
+
// A PRESENT minted pixel url (ctx.desktopStream) is ITSELF proof of liveness:
|
|
263
|
+
// the box (Modal-warm OR selfhosted-online) actually served the noVNC port,
|
|
264
|
+
// so a cold MODAL-GROUP lease liveness must NOT degrade it. lease_cold only
|
|
265
|
+
// fires when nothing was minted. A selfhosted-active session has no warm
|
|
266
|
+
// Modal lease (liveness "cold") yet mints a valid RELAY framebuffer cell —
|
|
267
|
+
// honour it (the un-redacted-pixel ack gate below still applies).
|
|
268
|
+
available = false;
|
|
269
|
+
reason = "lease_cold";
|
|
270
|
+
}
|
|
271
|
+
const shared = available ? Boolean(ctx.shared) : false;
|
|
272
|
+
// The minted pixel endpoint is handed out ONLY when the desktop is actually
|
|
273
|
+
// available (the gates passed) AND acknowledged: an unacked/cold/degraded
|
|
274
|
+
// desktop never leaks a live URL (the un-redacted-pixel consent gate). When
|
|
275
|
+
// absent the cell advertises the capability with a null live address — the
|
|
276
|
+
// caller mints it via POST /viewers.
|
|
277
|
+
const acknowledged = available ? Boolean(ctx.desktopAcknowledged) : false;
|
|
278
|
+
const minted = available && acknowledged ? ctx.desktopStream : undefined;
|
|
279
|
+
// Human take-control: the cell is "interactive" when the desktop is actually
|
|
280
|
+
// available AND the deployment's take-control policy is on (default true). The
|
|
281
|
+
// box's x11vnc runs without -viewonly, so a viewer driving input reaches :0;
|
|
282
|
+
// this mode bit is the CLIENT gate (the "Take control" affordance). A
|
|
283
|
+
// deployment that wants a genuinely read-only desktop sets
|
|
284
|
+
// sandboxDesktopInteractive=false → mode "read-only" and the client disables
|
|
285
|
+
// take-control. An unavailable cell is always "read-only" (nothing to drive).
|
|
286
|
+
// Selfhosted desktop is the RELAY framebuffer: PNG-per-frame protobuf datagrams
|
|
287
|
+
// spliced over the relay, rendered by the "frames" canvas client — NOT noVNC/RFB
|
|
288
|
+
// (that x11vnc path exists only for Modal boxes). It is VIEW-ONLY in v1 (the
|
|
289
|
+
// frame client does not forward input yet), so its mode is always read-only
|
|
290
|
+
// regardless of the take-control policy.
|
|
291
|
+
const selfhostedFrames = ctx.backend === "selfhosted";
|
|
292
|
+
const interactive = available && !selfhostedFrames && ctx.desktopInteractive !== false;
|
|
293
|
+
const mode = interactive ? ("interactive" as const) : ("read-only" as const);
|
|
294
|
+
return {
|
|
295
|
+
transport: available ? (selfhostedFrames ? ("relay-frames" as const) : cap.transport) : null,
|
|
296
|
+
client: available ? (selfhostedFrames ? ("frames" as const) : ("novnc" as const)) : null,
|
|
297
|
+
mode,
|
|
298
|
+
url: minted?.url ?? null,
|
|
299
|
+
token: minted?.token ?? null,
|
|
300
|
+
expiresAt: minted?.expiresAt ?? null,
|
|
301
|
+
resolution: minted?.resolution ?? ([1024, 768] as [number, number]),
|
|
302
|
+
// Desktop pixels are ALWAYS un-redacted when present (the literal
|
|
303
|
+
// framebuffer); the acknowledgment gate rests on this.
|
|
304
|
+
unredacted: true,
|
|
305
|
+
requiresAcknowledgment: available,
|
|
306
|
+
acknowledged: available ? Boolean(ctx.desktopAcknowledged) : false,
|
|
307
|
+
// Shared-exposure disclosure (addendum E.1): `shared` when the group has
|
|
308
|
+
// >1 session; `sharedSessionIds` is the OTHER sessions' ids ONLY (never
|
|
309
|
+
// their conversation/metadata). Empty/false for a solo box or when the
|
|
310
|
+
// desktop cell is unavailable.
|
|
311
|
+
shared,
|
|
312
|
+
sharedSessionIds: shared ? (ctx.sharedSessionIds ?? []) : [],
|
|
313
|
+
reason,
|
|
314
|
+
};
|
|
315
|
+
})();
|
|
316
|
+
|
|
317
|
+
const recording = (() => {
|
|
318
|
+
const cap = descriptor.capabilities.Recording;
|
|
319
|
+
if (osReason) {
|
|
320
|
+
return { available: false, modes: [] as ("manual" | "on-turn" | "on-verify")[], codecs: [] as ("h264-mp4" | "vp9-webm")[], reason: osReason };
|
|
321
|
+
}
|
|
322
|
+
if (!cap.available) {
|
|
323
|
+
return { available: false, modes: [] as ("manual" | "on-turn" | "on-verify")[], codecs: [] as ("h264-mp4" | "vp9-webm")[], reason: descriptor.tier === "headless" ? ("tier_headless" as const) : ("backend_unsupported" as const) };
|
|
324
|
+
}
|
|
325
|
+
// Recording feasibility tracks desktop; policy-gate it the same way.
|
|
326
|
+
if (!ctx.desktopEnabled) {
|
|
327
|
+
return { available: false, modes: [] as ("manual" | "on-turn" | "on-verify")[], codecs: [] as ("h264-mp4" | "vp9-webm")[], reason: "disabled_by_policy" as const };
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
available: true,
|
|
331
|
+
modes: ["manual", "on-turn", "on-verify"] as ("manual" | "on-turn" | "on-verify")[],
|
|
332
|
+
codecs: ["h264-mp4", "vp9-webm"] as ("h264-mp4" | "vp9-webm")[],
|
|
333
|
+
reason: null,
|
|
334
|
+
};
|
|
335
|
+
})();
|
|
336
|
+
|
|
337
|
+
const computerUse = (() => {
|
|
338
|
+
// The agent computer-use driver requires the same desktop image (X stack) as
|
|
339
|
+
// the pixel plane: it drives :0 with xdotool/scrot. Availability == desktop-
|
|
340
|
+
// capable backend && desktopEnabled && computerUseEnabled. Degradation is a
|
|
341
|
+
// value, never silent (an unavailable cell carries a reason).
|
|
342
|
+
const desktopCapable = descriptor.capabilities.DesktopStream.available;
|
|
343
|
+
const readOnly = ctx.computerUseReadOnly ?? false;
|
|
344
|
+
if (osReason) {
|
|
345
|
+
return { available: false, readOnly, reason: osReason };
|
|
346
|
+
}
|
|
347
|
+
if (!desktopCapable) {
|
|
348
|
+
return { available: false, readOnly, reason: descriptor.tier === "headless" ? ("tier_headless" as const) : ("backend_unsupported" as const) };
|
|
349
|
+
}
|
|
350
|
+
if (!ctx.desktopEnabled || ctx.computerUseEnabled === false) {
|
|
351
|
+
return { available: false, readOnly, reason: "disabled_by_policy" as const };
|
|
352
|
+
}
|
|
353
|
+
return { available: true, readOnly, reason: null };
|
|
354
|
+
})();
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
sessionId: ctx.sessionId,
|
|
358
|
+
backend: ctx.backend,
|
|
359
|
+
os: ctx.os,
|
|
360
|
+
liveness: ctx.liveness,
|
|
361
|
+
leaseEpoch: ctx.leaseEpoch,
|
|
362
|
+
viewerHeartbeatIntervalMs: 30_000,
|
|
363
|
+
FileSystem: fileSystem,
|
|
364
|
+
Terminal: terminal,
|
|
365
|
+
Git: git,
|
|
366
|
+
DesktopStream: desktop,
|
|
367
|
+
Recording: recording,
|
|
368
|
+
ComputerUse: computerUse,
|
|
369
|
+
negotiatedAt,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// Selfhosted capability negotiation (M3, dossier §10.2 item 4).
|
|
2
|
+
//
|
|
3
|
+
// `negotiateSelfhostedCapabilities` resolves the selfhosted-specific cells from
|
|
4
|
+
// (a) the M2 enrollment row (consent / display / status / lastSeenAt), and
|
|
5
|
+
// (b) a LIVENESS PROBE (a `ControlRpc` Ping, mockable) — "is there a responder
|
|
6
|
+
// on the subject right now?"
|
|
7
|
+
// into the right `SessionCapabilities` cells with the selfhosted reasons:
|
|
8
|
+
// - online → responder + consented: cells available.
|
|
9
|
+
// - offline → no enrollment / revoked / no responder: agent_offline.
|
|
10
|
+
// - reconnecting → a transient blip (a recent lastSeenAt but the probe
|
|
11
|
+
// missed): agent_reconnecting.
|
|
12
|
+
// - consent_required → enrolled but whole-machine / screen-control not acked:
|
|
13
|
+
// consent_required on the desktop/computer-use cells.
|
|
14
|
+
// - display_unavailable → online but the machine has no display (headless, no
|
|
15
|
+
// Xvfb): the desktop/computer-use cells degrade with
|
|
16
|
+
// display_unavailable.
|
|
17
|
+
//
|
|
18
|
+
// It REUSES `negotiateCapabilities` for the descriptor-shaped cells (so the
|
|
19
|
+
// "every cell present, degradation is a value" rule and the FS/Terminal/Git
|
|
20
|
+
// surface stay identical to Modal), then overlays the selfhosted liveness/consent
|
|
21
|
+
// /display reasons. The base function stays pure + synchronous; this is the
|
|
22
|
+
// selfhosted-aware entrypoint the API/worker call.
|
|
23
|
+
|
|
24
|
+
import type {
|
|
25
|
+
CapabilityUnavailableReason,
|
|
26
|
+
SandboxOs,
|
|
27
|
+
SessionCapabilities,
|
|
28
|
+
} from "@opengeni/contracts";
|
|
29
|
+
import { negotiateCapabilities, type NegotiationContext } from "../select";
|
|
30
|
+
import type { SelfhostedSession } from "./session";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The structural slice of the M2 `@opengeni/db` `EnrollmentRecord` the selfhosted
|
|
34
|
+
* negotiation reads. Defined STRUCTURALLY (not imported from `@opengeni/db`) so
|
|
35
|
+
* the agent-loop-free sandbox leaf does not couple to the DB package's graph —
|
|
36
|
+
* the API/worker pass an `EnrollmentRecord`, which satisfies this shape. The
|
|
37
|
+
* fields: `status` (active gates reachability), `exposure` +
|
|
38
|
+
* `allowScreenControl` (whole-machine + screen-control consent),`hasDisplay`
|
|
39
|
+
* (the display plane), `lastSeenAt` (the reconnecting-window disambiguator).
|
|
40
|
+
*/
|
|
41
|
+
export interface SelfhostedEnrollment {
|
|
42
|
+
status: string;
|
|
43
|
+
exposure: string;
|
|
44
|
+
allowScreenControl: boolean;
|
|
45
|
+
hasDisplay: boolean;
|
|
46
|
+
lastSeenAt: string | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The derived liveness state of a selfhosted machine (the online/offline/
|
|
50
|
+
* reconnecting/consent/display matrix). */
|
|
51
|
+
export interface SelfhostedLivenessState {
|
|
52
|
+
/** The dominant machine state. */
|
|
53
|
+
state: "online" | "reconnecting" | "offline";
|
|
54
|
+
/** Whole-machine + screen-control consent acknowledged (gates desktop input). */
|
|
55
|
+
consented: boolean;
|
|
56
|
+
/** A display (real or Xvfb) is present (gates the desktop pixel plane). */
|
|
57
|
+
hasDisplay: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The window after `lastSeenAt` within which a missed liveness probe is read as a
|
|
62
|
+
* transient BLIP (`reconnecting`) rather than a hard `offline`. Mirrors the
|
|
63
|
+
* resiliency model (§10.6: reconnecting after 1 missed window, offline after
|
|
64
|
+
* ~30s). A probe miss with a lastSeenAt inside this window → reconnecting.
|
|
65
|
+
*/
|
|
66
|
+
export const SELFHOSTED_RECONNECT_WINDOW_MS = 30_000;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Derive the selfhosted liveness state from the enrollment row + a liveness probe
|
|
70
|
+
* outcome. The probe is the authoritative "is the agent answering NOW" signal;
|
|
71
|
+
* `lastSeenAt` disambiguates a probe-miss into reconnecting (recent) vs offline
|
|
72
|
+
* (stale / never seen).
|
|
73
|
+
*
|
|
74
|
+
* - no enrollment / revoked → offline (the machine isn't enrolled).
|
|
75
|
+
* - probe responded → online.
|
|
76
|
+
* - probe missed, lastSeenAt recent → reconnecting (a transient blip).
|
|
77
|
+
* - probe missed, lastSeenAt stale → offline.
|
|
78
|
+
*/
|
|
79
|
+
export function selfhostedLiveness(input: {
|
|
80
|
+
enrollment: SelfhostedEnrollment | null;
|
|
81
|
+
/** The ControlRpc Ping outcome: true iff a responder answered. */
|
|
82
|
+
probeResponded: boolean;
|
|
83
|
+
/** Override the clock (tests). */
|
|
84
|
+
now?: Date;
|
|
85
|
+
}): SelfhostedLivenessState {
|
|
86
|
+
const { enrollment } = input;
|
|
87
|
+
if (!enrollment || enrollment.status !== "active") {
|
|
88
|
+
return { state: "offline", consented: false, hasDisplay: false };
|
|
89
|
+
}
|
|
90
|
+
const consented = enrollment.exposure === "whole-machine" && enrollment.allowScreenControl;
|
|
91
|
+
const hasDisplay = enrollment.hasDisplay;
|
|
92
|
+
if (input.probeResponded) {
|
|
93
|
+
return { state: "online", consented, hasDisplay };
|
|
94
|
+
}
|
|
95
|
+
// Probe missed → reconnecting if we saw it recently, else offline.
|
|
96
|
+
const now = (input.now ?? new Date()).getTime();
|
|
97
|
+
const lastSeen = enrollment.lastSeenAt ? new Date(enrollment.lastSeenAt).getTime() : null;
|
|
98
|
+
if (lastSeen !== null && now - lastSeen <= SELFHOSTED_RECONNECT_WINDOW_MS) {
|
|
99
|
+
return { state: "reconnecting", consented, hasDisplay };
|
|
100
|
+
}
|
|
101
|
+
return { state: "offline", consented, hasDisplay };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface SelfhostedNegotiationInput {
|
|
105
|
+
sessionId: string;
|
|
106
|
+
os?: SandboxOs;
|
|
107
|
+
leaseEpoch: number;
|
|
108
|
+
/** The M2 enrollment row for the machine (null → never enrolled → offline). */
|
|
109
|
+
enrollment: SelfhostedEnrollment | null;
|
|
110
|
+
/** A live liveness probe — typically `session.ping()`. When a session is
|
|
111
|
+
* provided this is called; otherwise pass `probeResponded` explicitly. */
|
|
112
|
+
session?: Pick<SelfhostedSession, "ping">;
|
|
113
|
+
/** Explicit probe outcome (when no session is given, e.g. a pure read). */
|
|
114
|
+
probeResponded?: boolean;
|
|
115
|
+
/** The deployment desktop/terminal/computer-use policy toggles (threaded
|
|
116
|
+
* through to the base negotiation). */
|
|
117
|
+
desktopEnabled?: boolean;
|
|
118
|
+
terminalEnabled?: boolean;
|
|
119
|
+
computerUseEnabled?: boolean;
|
|
120
|
+
/** Whether the calling principal acknowledged the un-redacted desktop. */
|
|
121
|
+
desktopAcknowledged?: boolean;
|
|
122
|
+
shared?: boolean;
|
|
123
|
+
sharedSessionIds?: string[];
|
|
124
|
+
/** Override the clock (tests). */
|
|
125
|
+
now?: Date;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Negotiate the full `SessionCapabilities` document for a selfhosted machine,
|
|
130
|
+
* with the online/offline/reconnecting/consent_required/display_unavailable cells
|
|
131
|
+
* correctly decided. Async because it issues the liveness probe.
|
|
132
|
+
*/
|
|
133
|
+
export async function negotiateSelfhostedCapabilities(input: SelfhostedNegotiationInput): Promise<SessionCapabilities> {
|
|
134
|
+
const probeResponded = input.probeResponded ?? (input.session ? await input.session.ping() : false);
|
|
135
|
+
const liveness = selfhostedLiveness({
|
|
136
|
+
enrollment: input.enrollment,
|
|
137
|
+
probeResponded,
|
|
138
|
+
...(input.now ? { now: input.now } : {}),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// The base context: map the machine state onto the lease `liveness` axis so the
|
|
142
|
+
// descriptor-shaped cells (FS/Terminal/Git/Desktop) negotiate as on a warm box
|
|
143
|
+
// when online, and a cold box when not reachable (no live tunnel). The
|
|
144
|
+
// selfhosted overlay below then stamps the selfhosted-specific reasons.
|
|
145
|
+
const baseLiveness: NegotiationContext["liveness"] = liveness.state === "online" ? "warm" : "cold";
|
|
146
|
+
const base: NegotiationContext = {
|
|
147
|
+
sessionId: input.sessionId,
|
|
148
|
+
backend: "selfhosted",
|
|
149
|
+
os: input.os ?? "linux",
|
|
150
|
+
liveness: baseLiveness,
|
|
151
|
+
leaseEpoch: input.leaseEpoch,
|
|
152
|
+
desktopEnabled: input.desktopEnabled ?? true,
|
|
153
|
+
terminalEnabled: input.terminalEnabled ?? true,
|
|
154
|
+
computerUseEnabled: input.computerUseEnabled ?? true,
|
|
155
|
+
...(input.desktopAcknowledged !== undefined ? { desktopAcknowledged: input.desktopAcknowledged } : {}),
|
|
156
|
+
...(input.shared !== undefined ? { shared: input.shared } : {}),
|
|
157
|
+
...(input.sharedSessionIds !== undefined ? { sharedSessionIds: input.sharedSessionIds } : {}),
|
|
158
|
+
...(input.now ? { now: input.now } : {}),
|
|
159
|
+
};
|
|
160
|
+
const caps = negotiateCapabilities(base);
|
|
161
|
+
|
|
162
|
+
// ── Overlay the selfhosted liveness/consent/display reasons ────────────────
|
|
163
|
+
|
|
164
|
+
// When the machine is not online, the Channel-A surface (FS/Terminal/Git) and
|
|
165
|
+
// the desktop plane cannot be reached — stamp the machine-liveness reason. This
|
|
166
|
+
// is the dominant degrade (like os_unsupported): an offline/reconnecting agent
|
|
167
|
+
// knocks out every reachable capability with the single coherent reason.
|
|
168
|
+
if (liveness.state !== "online") {
|
|
169
|
+
const reason: CapabilityUnavailableReason = liveness.state === "offline" ? "agent_offline" : "agent_reconnecting";
|
|
170
|
+
return {
|
|
171
|
+
...caps,
|
|
172
|
+
FileSystem: { ...caps.FileSystem, available: false, readOnly: true, reason },
|
|
173
|
+
Terminal: { ...caps.Terminal, transport: null, url: null, token: null, expiresAt: null, reason },
|
|
174
|
+
Git: { ...caps.Git, available: false, reason },
|
|
175
|
+
DesktopStream: {
|
|
176
|
+
...caps.DesktopStream,
|
|
177
|
+
transport: null,
|
|
178
|
+
client: null,
|
|
179
|
+
mode: "read-only",
|
|
180
|
+
url: null,
|
|
181
|
+
token: null,
|
|
182
|
+
expiresAt: null,
|
|
183
|
+
requiresAcknowledgment: false,
|
|
184
|
+
acknowledged: false,
|
|
185
|
+
shared: false,
|
|
186
|
+
sharedSessionIds: [],
|
|
187
|
+
reason,
|
|
188
|
+
},
|
|
189
|
+
Recording: { ...caps.Recording, available: false, modes: [], codecs: [], reason },
|
|
190
|
+
ComputerUse: { ...caps.ComputerUse, available: false, reason },
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Online: FS/Terminal/Git stay as the base negotiated them (the machine is
|
|
195
|
+
// reachable). The desktop plane splits VIEW from CONTROL:
|
|
196
|
+
// - VIEW (a read-only DesktopStream, Recording) requires a DISPLAY only. The
|
|
197
|
+
// agent already holds whole-machine shell exec (it can `screencapture` the
|
|
198
|
+
// screen itself), so passive viewing is within the exposure the user already
|
|
199
|
+
// consented to; a missing display (headless, no Xvfb / no macOS Screen
|
|
200
|
+
// Recording grant) is the only blocker.
|
|
201
|
+
// - CONTROL — driving input (ComputerUse) or an INTERACTIVE stream —
|
|
202
|
+
// additionally requires the explicit allowScreenControl consent (`consented`).
|
|
203
|
+
// Precedence: a headless machine blocks everything (display_unavailable); a
|
|
204
|
+
// displayed-but-unconsented machine can be VIEWED (read-only) + RECORDED but not
|
|
205
|
+
// CONTROLLED (consent_required). (If the base already degraded a cell for a
|
|
206
|
+
// policy reason — desktop disabled / no stream-token secret — that base reason
|
|
207
|
+
// wins; we only stamp a selfhosted reason on a cell the base left AVAILABLE.)
|
|
208
|
+
if (!liveness.hasDisplay) {
|
|
209
|
+
const reason: CapabilityUnavailableReason = "display_unavailable";
|
|
210
|
+
return {
|
|
211
|
+
...caps,
|
|
212
|
+
DesktopStream: caps.DesktopStream.transport !== null
|
|
213
|
+
? {
|
|
214
|
+
...caps.DesktopStream,
|
|
215
|
+
transport: null,
|
|
216
|
+
client: null,
|
|
217
|
+
mode: "read-only",
|
|
218
|
+
url: null,
|
|
219
|
+
token: null,
|
|
220
|
+
expiresAt: null,
|
|
221
|
+
requiresAcknowledgment: false,
|
|
222
|
+
acknowledged: false,
|
|
223
|
+
shared: false,
|
|
224
|
+
sharedSessionIds: [],
|
|
225
|
+
reason,
|
|
226
|
+
}
|
|
227
|
+
: caps.DesktopStream,
|
|
228
|
+
Recording: caps.Recording.available
|
|
229
|
+
? { ...caps.Recording, available: false, modes: [], codecs: [], reason }
|
|
230
|
+
: caps.Recording,
|
|
231
|
+
ComputerUse: caps.ComputerUse.available
|
|
232
|
+
? { ...caps.ComputerUse, available: false, reason }
|
|
233
|
+
: caps.ComputerUse,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!liveness.consented) {
|
|
238
|
+
// Displayed but no screen-CONTROL consent: VIEW (read-only) + Recording stay
|
|
239
|
+
// available; only CONTROL (input) is withheld. Force the stream to read-only
|
|
240
|
+
// so no input is forwarded even if the base offered an interactive mode.
|
|
241
|
+
return {
|
|
242
|
+
...caps,
|
|
243
|
+
DesktopStream: caps.DesktopStream.transport !== null
|
|
244
|
+
? { ...caps.DesktopStream, mode: "read-only" }
|
|
245
|
+
: caps.DesktopStream,
|
|
246
|
+
ComputerUse: caps.ComputerUse.available
|
|
247
|
+
? { ...caps.ComputerUse, available: false, reason: "consent_required" }
|
|
248
|
+
: caps.ComputerUse,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Fully online + displayed + consented: the base negotiation already produced
|
|
253
|
+
// the correct available cells (desktop vnc-ws, computer-use available, etc.).
|
|
254
|
+
return caps;
|
|
255
|
+
}
|