@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.
Files changed (65) hide show
  1. package/dist/chunk-2PO56VAL.js +3478 -0
  2. package/dist/chunk-2PO56VAL.js.map +1 -0
  3. package/dist/index.d.ts +912 -0
  4. package/dist/index.js +3663 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/sandbox/index.d.ts +1738 -0
  7. package/dist/sandbox/index.js +187 -0
  8. package/dist/sandbox/index.js.map +1 -0
  9. package/package.json +49 -0
  10. package/src/bundled_hashicorp_terraform_skills/LICENSE +373 -0
  11. package/src/bundled_hashicorp_terraform_skills/README.md +18 -0
  12. package/src/bundled_hashicorp_terraform_skills/UPSTREAM_GIT_SHA +1 -0
  13. package/src/bundled_hashicorp_terraform_skills/azure-verified-modules/SKILL.md +613 -0
  14. package/src/bundled_hashicorp_terraform_skills/checkov/SKILL.md +43 -0
  15. package/src/bundled_hashicorp_terraform_skills/refactor-module/SKILL.md +538 -0
  16. package/src/bundled_hashicorp_terraform_skills/social-media-marketing/SKILL.md +35 -0
  17. package/src/bundled_hashicorp_terraform_skills/terraform-search-import/SKILL.md +372 -0
  18. package/src/bundled_hashicorp_terraform_skills/terraform-search-import/references/MANUAL-IMPORT.md +113 -0
  19. package/src/bundled_hashicorp_terraform_skills/terraform-search-import/scripts/list_resources.sh +38 -0
  20. package/src/bundled_hashicorp_terraform_skills/terraform-stacks/SKILL.md +480 -0
  21. package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/api-monitoring.md +543 -0
  22. package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/component-blocks.md +476 -0
  23. package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/deployment-blocks.md +391 -0
  24. package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/examples.md +1529 -0
  25. package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/linked-stacks.md +187 -0
  26. package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/troubleshooting.md +671 -0
  27. package/src/bundled_hashicorp_terraform_skills/terraform-style-guide/SKILL.md +353 -0
  28. package/src/bundled_hashicorp_terraform_skills/terraform-test/SKILL.md +451 -0
  29. package/src/bundled_hashicorp_terraform_skills/terraform-test/references/CI_CD.md +80 -0
  30. package/src/bundled_hashicorp_terraform_skills/terraform-test/references/EXAMPLES.md +314 -0
  31. package/src/bundled_hashicorp_terraform_skills/terraform-test/references/MOCK_PROVIDERS.md +171 -0
  32. package/src/codex-tool-search.ts +267 -0
  33. package/src/context-compaction.ts +538 -0
  34. package/src/history-sanitizer.ts +719 -0
  35. package/src/index.ts +3299 -0
  36. package/src/sandbox/capabilities.ts +69 -0
  37. package/src/sandbox/channel-a.ts +1031 -0
  38. package/src/sandbox/display-stack.ts +231 -0
  39. package/src/sandbox/errors.ts +34 -0
  40. package/src/sandbox/index.ts +832 -0
  41. package/src/sandbox/providers/blaxel.ts +35 -0
  42. package/src/sandbox/providers/cloudflare.ts +24 -0
  43. package/src/sandbox/providers/daytona.ts +34 -0
  44. package/src/sandbox/providers/docker.ts +17 -0
  45. package/src/sandbox/providers/e2b.ts +36 -0
  46. package/src/sandbox/providers/index.ts +107 -0
  47. package/src/sandbox/providers/local.ts +13 -0
  48. package/src/sandbox/providers/modal.ts +55 -0
  49. package/src/sandbox/providers/none.ts +13 -0
  50. package/src/sandbox/providers/runloop.ts +32 -0
  51. package/src/sandbox/providers/selfhosted.ts +96 -0
  52. package/src/sandbox/providers/types.ts +38 -0
  53. package/src/sandbox/providers/vercel.ts +29 -0
  54. package/src/sandbox/recording.ts +286 -0
  55. package/src/sandbox/routing/backend-resolver.ts +189 -0
  56. package/src/sandbox/routing/routing-session.ts +455 -0
  57. package/src/sandbox/select.ts +371 -0
  58. package/src/sandbox/selfhosted/capabilities.ts +255 -0
  59. package/src/sandbox/selfhosted/control-rpc.ts +351 -0
  60. package/src/sandbox/selfhosted/session.ts +930 -0
  61. package/src/sandbox/selfhosted/testing.ts +230 -0
  62. package/src/sandbox/stream-port.ts +185 -0
  63. package/src/sandbox/stream-token.ts +90 -0
  64. package/src/sandbox/terminal-server.ts +203 -0
  65. 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
+ }