import type { Settings } from "@opengeni/config"; import { type Session } from "@opengeni/contracts"; import { type Database, type LeaseSnapshot, type SandboxRecord } from "@opengeni/db"; import { type EventBus } from "@opengeni/events"; import type { ObjectStorage } from "@opengeni/storage"; import { establishSandboxSessionFromEnvelope, type EstablishedSandboxSession } from "@opengeni/runtime/sandbox"; /** The minimal services a viewer op needs: the DB + settings (lease cadence + * the sandbox client construction the leaf reads from settings). The bus is * optional — only the rotation path (emitting stream.url.rotated to OTHER * viewers) needs it. */ export type ViewerServices = { db: Database; settings: Settings; bus?: EventBus; objectStorage?: ObjectStorage | null; /** Provider-establish dependency used by API-direct readiness and stream * operations. Production uses the runtime leaf; isolated tests may supply a * deterministic provider without replacing a process-global module. */ establishSandboxSession?: typeof establishSandboxSessionFromEnvelope; }; /** A coherent snapshot the routes echo back: the holder id (the viewer's fence- * carrying handle), the lease liveness/epoch, and the recorded data-plane URL * (null until P4 mints the desktop tunnel). */ export type ViewerAttachResult = { viewerId: string; liveness: LeaseSnapshot["liveness"]; leaseEpoch: number; workspaceGeneration: number | null; archiveGeneration: number | null; archiveComplete: boolean; sandboxGroupId: string; viewerHeartbeatIntervalMs: number; dataPlaneUrl: string | null; }; /** * The STABLE run-scoped sandbox environment a COLD box must be created with so * that — whether the box is first warmed by an API-direct ATTACH (here) or by the * worker TURN — its manifest environment matches the environment the agent later * declares for a turn. Without this, an attach-warmed box was created with the * BASE allowlist env only (establishSandboxSessionFromEnvelope's * collectSandboxEnvironment default), so the next turn's fuller env (git identity * + workspace environment + HOME) introduced a delta and the SDK's * `validateNoEnvironmentDelta` threw "Live sandbox sessions cannot change manifest * environment variables" — the BLOCKING error this fixes. * * Mirrors the worker turn's STABLE env (config.stableSandboxEnvironmentForRun + * the session's attached, decrypted workspace environment + — for a repo-attached * session — the stable git-auth POINTERS the turn declares since the token-broker: * GIT_ASKPASS / GIT_TERMINAL_PROMPT / bot identity). The pointers carry NO rotating * value (the token lives in the box FILE the clone hook seeds), so they are * attach-reproducible; omitting them cold-created a box whose env lacked keys the * next repo turn's manifest declares → the SDK guard threw "Live sandbox sessions * cannot change manifest environment variables" whenever a viewer attach (an open * session page) won the cold-create race against the first turn. */ export declare function sessionAttachEnvironment(services: ViewerServices, workspaceId: string, session: Session, /** The authenticated route subject driving this attach (0282); recorded on * the materialization audit fact. Null records the legacy service sentinel. */ attachSubjectId: string | null): Promise>; /** * Acquire a `viewer` holder on the group lease, spinning up the box IN-PROCESS * when cold. Mirrors the worker's resumeBoxForTurn spawner/attached branches, * but with kind:'viewer' and run by the API process — no Temporal, no worker. * * `viewerId` is the unique-per-connection holder id (a uuid the client carries * through heartbeats + detach); generated when absent. */ export declare function attachViewer(services: ViewerServices, input: { accountId: string; workspaceId: string; session: Session; viewerId?: string; /** The authenticated subject attaching this viewer (0281): recorded on * the holder row together with the live session authority epoch. */ viewerSubjectId?: string; /** Cancel lifecycle waiting when the originating HTTP request disconnects. */ waitSignal?: AbortSignal; }): Promise; export type SessionGroupReadinessHold = { lease: LeaseSnapshot; /** Idempotently release the viewer holder after route publication settles. */ release: () => Promise; }; /** Readiness callback for fleet attach/swap. It acquires one disposable viewer * holder through the same provider-verifying path as the UI and RETURNS that * holder to the swap owner. The holder must remain live until route publication * settles, otherwise the reaper can drain the just-verified target between the * readiness probe and the route CAS. A first 409 may have fenced a missing warm * provider; one bounded retry lets the normal cold->warming election rematerialize * it. */ export declare function ensureSessionGroupReady(services: ViewerServices, input: { accountId: string; workspaceId: string; session: Session; /** The authenticated subject driving the fleet attach/swap (0282): recorded * on the viewer holder and the materialization audit fact. Omitting it * records the explicit service sentinel, so forward the route subject. */ subjectId?: string | null; }): Promise; /** * Refresh a viewer holder's TTL (the app-level viewer heartbeat). Epoch-fenced: * a stale-epoch heartbeat (a box re-established under a newer epoch) returns * false and the client must re-attach. Returns whether the holder is still live. */ export declare function heartbeatViewer(services: ViewerServices, input: { accountId: string; workspaceId: string; sandboxGroupId: string; viewerId: string; expectedEpoch: number; }): Promise; /** * Release a viewer holder (the client disconnected). Idempotent: a double * detach (or a detach after the reaper already TTL-reaped the holder) is a * no-op. The box drains/stops only when no turn AND no viewer holds it. */ export declare function detachViewer(services: ViewerServices, input: { accountId: string; workspaceId: string; sandboxGroupId: string; viewerId: string; }): Promise<{ liveness: LeaseSnapshot["liveness"]; refcount: number; } | null>; /** Non-locking lease snapshot for the capability-negotiation read. */ export declare function readGroupLease(services: ViewerServices, input: { workspaceId: string; sandboxGroupId: string; }): Promise; export declare function viewerHeartbeatIntervalMs(settings: Settings): number; /** * The desktop WIRE transport for the session's ACTIVE sandbox — the single * invariant that keeps the advertised transport in lockstep with where * mintDesktopStream routed the pixels. It MUST be derived from the ACTIVE sandbox * (not the session's HOME backend that negotiateCapabilities keys on): a selfhosted * machine serves the RELAY framebuffer (PNG-per-frame → "relay-frames"/"frames", * view-only in v1), and a Modal group box serves noVNC/RFB over the 6080 tunnel * (→ "vnc-ws"/"novnc", take-control unless the deployment disabled it). Advertising * relay-frames for a Modal box (the swap-away case) hands the client a dead relay * socket → "desktop stream closed before it opened"; the reverse hands a machine's * relay URL to the noVNC renderer. `selfhostedActive` == (activeSandboxId set AND the * active sandbox kind is "selfhosted") — EXACTLY mintDesktopStream's routing predicate. */ export declare function resolveActiveDesktopTransport(selfhostedActive: boolean, interactive: boolean): { transport: "relay-frames" | "vnc-ws"; client: "frames" | "novnc"; mode: "read-only" | "interactive"; }; /** The minted pixel cell the handshake/attach folds into the DesktopStream * capability. Null when degraded (no secret, headless backend, display-stack * failure, provider tunnel failure) — degradation is a value, never a throw. */ export type DesktopStreamMint = { url: string; token: string; expiresAt: string; resolution: [number, number]; leaseEpoch: number; }; export type MintDesktopStreamInput = { accountId: string; workspaceId: string; resourceSubjectId?: string; /** True when the route grant is a signed delegated token (metadata.delegated): * its authority is the token, not membership rows, so the mint-time * live-authority recheck does not apply. */ resourceSubjectDelegated?: boolean; session: Session; /** The viewer holder id the scoped token is minted for. */ viewerId: string; /** The live lease (must be holder-confirmed warm). A selfhosted-active * session may have no Modal group lease; omit and the selfhosted branch handles it. */ lease?: LeaseSnapshot; /** The epoch the CALLER last observed the URL minted under. When the live * lease epoch is greater, the box rolled over → emit stream.url.rotated to the * other viewers. Omit on a first mint (no prior URL to rotate from). */ previousEpoch?: number; /** Test seam: override how the box is re-established by id. Defaults to the * real leaf `establishSandboxSessionFromEnvelope`. Production NEVER passes * this; it exists so a real-lease integration test can inject a fake provider * session carrying `resolveExposedPort` without a live cloud box. */ establish?: (envelope: Record | null) => Promise; /** Test seam: inject a fake relay-resolving session for the selfhosted-active * branch. Production NEVER passes this. */ resolveSelfhostedSession?: (sandbox: SandboxRecord) => Promise<{ resolveExposedPort?: (port: number) => Promise; }>; }; /** * Mint (or re-mint) the desktop pixel cell for a viewer against a WARM box, * IN-PROCESS. Returns the minted cell, or null when the desktop tier degrades * (no resolvable stream-token secret, a headless backend, a display-stack * failure, or a provider-tunnel failure) — the caller surfaces transport:null, * never an exception to the user. * * Idempotent display-stack + resolveExposedPort are safe to call N times. The * resolved URL is recorded on the lease (data_plane_url) under the epoch fence; a * stale-epoch write (the box re-established under a newer epoch mid-call) is a * no-op and the mint returns null rather than disclosing a capability for the * superseded provider epoch. */ export declare function mintDesktopStream(services: ViewerServices, input: MintDesktopStreamInput): Promise; /** The minted terminal cell the handshake/attach folds into the Terminal * capability (pty-ws). Null when degraded — the caller surfaces transport * "sse-events" (the read-only firehose), never an exception. */ export type TerminalStreamMint = { url: string; token: string; expiresAt: string; leaseEpoch: number; transport: "pty-ws" | "relay-pty"; }; type SelfhostedStreamMint = Omit; export type MintTerminalStreamInput = { accountId: string; workspaceId: string; resourceSubjectId?: string; /** See MintDesktopStreamInput.resourceSubjectDelegated. */ resourceSubjectDelegated?: boolean; session: Session; /** The viewer holder / principal id the scoped token is minted for. */ viewerId: string; /** The live lease (must be holder-confirmed warm). A selfhosted-active * session may have no Modal group lease; omit and the selfhosted branch handles it. */ lease?: LeaseSnapshot; /** Test seam: override how the box is re-established by id (see * MintDesktopStreamInput.establish). Production NEVER passes this. */ establish?: (envelope: Record | null) => Promise; /** Test seam: inject a fake relay-resolving session for the selfhosted-active * branch. Production NEVER passes this. */ resolveSelfhostedSession?: (sandbox: SandboxRecord) => Promise<{ resolveExposedPort?: (port: number) => Promise; }>; }; /** * Mint (or re-mint) the REAL PTY (ttyd pty-ws) terminal cell for a viewer against * a WARM box, IN-PROCESS. Returns the minted cell, or null when the terminal tier * degrades (terminal off, no resolvable stream-token secret, a headless backend, * a ttyd-launch failure, or a provider-tunnel failure) — the caller surfaces the * sse-events firehose, never an exception to the user. Mirrors mintDesktopStream. */ export declare function mintTerminalStream(services: ViewerServices, input: MintTerminalStreamInput): Promise; /** The structural slice of a selfhosted session the relay stream mint needs. */ type RelayResolvableSession = { resolveExposedPort?: (port: number) => Promise; }; export type MintSelfhostedStreamInput = { workspaceId: string; sessionId: string; /** The viewer holder / principal id the scoped token is minted for. */ viewerId: string; /** The swap fence: the session's `active_epoch`. The minted `ogs_` token carries * THIS as its leaseEpoch claim so the relay rejects a stale-epoch (swapped-away) * viewer. */ activeEpoch: number; /** 0281 viewer authority claims stamped into the relay stream token. */ viewerAuthority?: { subjectId?: string; authorityEpoch: number; }; /** The exposed stream port (6080 desktop / 7681 terminal). */ port: number; /** The resolvable selfhosted session (the routing proxy resolves the active * selfhosted backend; its `resolveExposedPort` returns the relay endpoint). */ session: RelayResolvableSession; }; /** * Mint the selfhosted relay stream cell for a viewer against the session's ACTIVE * selfhosted machine, IN-PROCESS. Resolves the relay endpoint via the selfhosted * session's `resolveExposedPort` and mints the scoped `ogs_` token FENCED BY THE * SWAP `active_epoch`. Returns null when the stream tier degrades (no stream-token * secret, the agent is offline / cannot ensure a channel) — the caller surfaces * transport:null, never an exception. * * The token is RECORDED against the viewer holder by the caller and is NEVER a URL * query param (the relay validates the in-band token); the relay's stale-viewer * fence uses the token's leaseEpoch claim (== activeEpoch here). */ export declare function mintSelfhostedStream(services: ViewerServices, input: MintSelfhostedStreamInput): Promise; export {};