@opengeni/runtime 0.2.3 → 0.3.1

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.
@@ -1,1740 +1,6 @@
1
- import { Settings } from '@opengeni/config';
2
1
  export { collectSandboxEnvironment, parseExposedPorts } from '@opengeni/config';
3
- import { SandboxBackend, CapabilityDescriptor, SandboxOs, SessionCapabilities, StreamTokenPayload, SessionEventType, SessionStructuredCapabilities, FsListRequest, FsListResponse, FsReadRequest, FsReadResponse, FsWriteRequest, FsWriteResponse, FsDeleteRequest, FsDeleteResponse, FsMoveRequest, FsMoveResponse, FsMkdirRequest, FsMkdirResponse, GitStatusRequest, GitStatusResponse, GitDiffRequest, GitDiffResponse, GitLogRequest, GitLogResponse, GitShowRequest, GitShowResponse, TerminalExecRequest, TerminalExecResponse, PtyOpenRequest, PtyOpenResponse, PtyWriteRequest, PtyResizeRequest, PtyCloseRequest, GitChangedPayload, GitDiffHunk, GitFileStatusCode, CapabilityUnavailableReason } from '@opengeni/contracts';
4
2
  export { CAPABILITY_DESCRIPTORS, CapabilityDescriptor, DESKTOP_STREAM_PORT, StreamTokenPayload, StreamTokenPayload as StreamTokenPayloadType, TERMINAL_STREAM_PORT } from '@opengeni/contracts';
5
- import { Manifest, SandboxClient, SandboxSessionState } from '@openai/agents/sandbox';
6
- import { ControlRequest, ControlResponse, ErrorCode, AgentError, DesktopInputRequest, ExecRequest, ExecResponse } from '@opengeni/agent-proto';
7
-
8
- /**
9
- * Descriptor-table invariants, asserted once at registry build (and from a unit
10
- * test). This is the guardrail that keeps the static matrix internally coherent.
11
- * It validates the descriptor data only; the descriptor.backendId === SDK
12
- * client.backendId assertion (the deferred-from-P0.1 check) lives in
13
- * providers/index.ts because it must construct the real SDK clients.
14
- */
15
- declare function assertDescriptorRegistryInvariants(): void;
16
-
17
- declare class SandboxConfigError extends Error {
18
- readonly backend: SandboxBackend | string;
19
- constructor(backend: SandboxBackend | string, message: string);
20
- }
21
- declare class SandboxProviderUnavailableError extends Error {
22
- readonly backend: SandboxBackend | string;
23
- constructor(backend: SandboxBackend | string);
24
- }
25
-
26
- interface ProviderConstructionContext {
27
- settings: Settings;
28
- /** The env map for the box (collectSandboxEnvironment / per-run environment). */
29
- environment: Record<string, string>;
30
- /**
31
- * Parsed exposed ports (config string -> number[]); already includes the
32
- * desktop stream port (6080) when this is a desktop tier with desktop enabled
33
- * and the provider cannot expose ports on demand (the merge happens in
34
- * createSandboxClient before build()).
35
- */
36
- exposedPorts: number[];
37
- }
38
- interface ProviderRegistration {
39
- backend: SandboxBackend;
40
- descriptor: CapabilityDescriptor;
41
- /**
42
- * Validate that the settings carry the credentials/config this provider
43
- * REQUIRES. Throw SandboxConfigError on any missing/contradictory field.
44
- * Pure — no network. Called by both the factory and a deploy-time preflight.
45
- * The factory calls this before build(), so build() may assume valid settings.
46
- */
47
- validateCredentials(settings: Settings): void;
48
- /**
49
- * Build the raw SDK SandboxClient. Returns undefined ONLY for "none".
50
- * The factory calls validateCredentials() first, so build() can assume valid.
51
- */
52
- build(ctx: ProviderConstructionContext): unknown;
53
- }
54
-
55
- declare const PROVIDER_REGISTRY: Record<SandboxBackend, ProviderRegistration>;
56
- /**
57
- * Assert the descriptor table AND that each registered provider's SDK client
58
- * reports the backendId its descriptor claims. The latter is the
59
- * deferred-from-P0.1 invariant — it can only run here because it constructs the
60
- * real clients. Called once at registry build (and from a unit test).
61
- */
62
- declare function assertProviderRegistryInvariants(): void;
63
-
64
- interface NegotiationContext {
65
- sessionId: string;
66
- backend: SandboxBackend;
67
- os: SandboxOs;
68
- /** Current lease liveness; cold means nothing is provisioned yet. */
69
- liveness: "cold" | "warming" | "warm" | "draining";
70
- /** The lease epoch echoed on viewer heartbeats (the split-brain fence). */
71
- leaseEpoch: number;
72
- /** The deployment desktop toggle (settings.sandboxDesktopEnabled). */
73
- desktopEnabled: boolean;
74
- /**
75
- * The HUMAN take-control toggle (settings.sandboxDesktopInteractive). When true
76
- * (default) and the desktop cell is available, the negotiated DesktopStream.mode
77
- * is "interactive" — the noVNC viewer can drive mouse+keyboard into :0 (the box's
78
- * x11vnc runs without -viewonly). When false the cell reports mode "read-only"
79
- * and the client disables the "Take control" affordance (a genuinely read-only
80
- * deployment). Independent of `computerUseReadOnly`, which gates the AGENT
81
- * driver, not the human viewer plane. Defaults to true so a caller that never
82
- * threads it (e.g. headless tests) still gets the interactive plane when the
83
- * desktop is available.
84
- */
85
- desktopInteractive?: boolean;
86
- /** The deployment computer-use toggle (settings.computerUseEnabled). The agent
87
- * drives :0 via xdotool/scrot; availability tracks desktop. Defaults to true. */
88
- computerUseEnabled?: boolean;
89
- /** Whether the agent computer-use driver is gated to no-op input
90
- * (settings.computerUseReadOnly). v1 default false (the agent clicks/types). */
91
- computerUseReadOnly?: boolean;
92
- /**
93
- * Whether a scoped-stream-token secret is resolvable (I8/OD-8). When desktop
94
- * is enabled but this is false (no streamTokenSecret AND no delegationSecret),
95
- * the desktop plane GRACEFULLY DEGRADES to transport:null — the deployment
96
- * boots, but the pixel plane cannot mint scoped tokens. Defaults to true so a
97
- * caller that never threads it (e.g. headless tests) is unaffected.
98
- */
99
- streamTokenSecretAvailable?: boolean;
100
- /** Whether the calling principal has acknowledged the un-redacted desktop
101
- * pixels (and, for a shared box, the shared-exposure disclosure). When the
102
- * box is shared this must be the SHARED acknowledgment; a bare un-redacted ack
103
- * does not satisfy a shared box. */
104
- desktopAcknowledged?: boolean;
105
- /** True when the box's group has >1 session: watching this desktop also shows
106
- * the sibling sessions' agents on the one :0 framebuffer (addendum E.1). */
107
- shared?: boolean;
108
- /** The OTHER sessions whose agents may appear on the shared desktop — IDS
109
- * ONLY, never their conversation/metadata (stress g). Empty for a solo box. */
110
- sharedSessionIds?: string[];
111
- /**
112
- * The minted pixel-plane endpoint (P4.2): the direct-to-provider WS URL + the
113
- * scoped stream token + its expiry + the framebuffer geometry. Threaded by the
114
- * API-direct handshake AFTER it has resumed the box, ensured the display stack,
115
- * and resolved the provider tunnel. When ABSENT (the negotiation-only read, a
116
- * cold lease, or a degraded desktop) the DesktopStream cell reports url/token/
117
- * expiresAt as null — the capability is advertised, the live address is not yet
118
- * minted (the caller POSTs to /viewers to mint it). Presence does NOT override
119
- * the gates: a degraded/cold/unacked desktop still reports transport:null and
120
- * the minted endpoint is dropped.
121
- */
122
- desktopStream?: {
123
- url: string;
124
- token: string;
125
- expiresAt: string;
126
- resolution: [number, number];
127
- };
128
- /** The deployment terminal toggle (settings.sandboxTerminalEnabled). The REAL
129
- * PTY (ttyd pty-ws) is gated on this + a real-PTY backend; when off the
130
- * Terminal cell still advertises the read-only sse-events firehose. Defaults to
131
- * true so a caller that never threads it is unaffected. */
132
- terminalEnabled?: boolean;
133
- /**
134
- * The minted terminal-plane endpoint (P5.t): the direct-to-provider ttyd
135
- * PTY-over-websocket URL + the scoped stream token + its expiry. Threaded by the
136
- * API-direct handshake AFTER it has resumed the box, ensured the terminal
137
- * server, and resolved the provider tunnel (mintTerminalStream) — SYMMETRIC with
138
- * `desktopStream`. When ABSENT (the negotiation-only read, a cold lease, or a
139
- * degraded terminal) the Terminal cell reports url/token/expiresAt as null and
140
- * falls back to transport "sse-events" (the read-only firehose) — the caller
141
- * POSTs to /viewers to mint the live pty-ws address.
142
- */
143
- terminalStream?: {
144
- url: string;
145
- token: string;
146
- expiresAt: string;
147
- };
148
- /** Override the negotiation clock (tests). */
149
- now?: Date;
150
- }
151
- /**
152
- * Resolve the descriptor for a backend. Throws on an unknown backend rather than
153
- * returning a half-formed default (the registry is the single source of truth).
154
- */
155
- declare function selectBackend(backend: SandboxBackend): CapabilityDescriptor;
156
- /** True iff the descriptor lists the requested OS as supported. */
157
- declare function backendSupportsOs(descriptor: CapabilityDescriptor, os: SandboxOs): boolean;
158
- /**
159
- * True iff the backend can serve the Channel-B desktop pixel plane at all — i.e.
160
- * its static descriptor advertises DesktopStream as available. The gate the
161
- * worker / API use before launching the display stack (so a headless-only
162
- * backend like cloudflare/vercel/none never tries). This is the STATIC
163
- * feasibility only; the runtime `sandboxDesktopEnabled` policy toggle and the
164
- * stream-token-secret gate are layered on by the caller / negotiateCapabilities.
165
- *
166
- * Accepts EITHER the SandboxBackend enum value (e.g. "local") OR the SDK
167
- * client backendId (e.g. "unix_local") — they diverge for the local backend —
168
- * so a caller holding only `established.backendId` resolves correctly.
169
- */
170
- declare function desktopCapableBackend(backend: SandboxBackend | string): boolean;
171
- /**
172
- * Negotiate a coherent SessionCapabilities document for (backend, os). Every
173
- * capability is reported with availability + a reason-when-unavailable; nothing
174
- * is ever absent. The reason precedence is: os_unsupported (the OS axis can't be
175
- * served at all) > the per-capability static feasibility > policy/liveness gates.
176
- */
177
- declare function negotiateCapabilities(ctx: NegotiationContext): SessionCapabilities;
178
-
179
- declare const STREAM_TOKEN_DEFAULT_TTL_SECONDS = 120;
180
- type MintStreamTokenInput = {
181
- workspaceId: string;
182
- sessionId: string;
183
- /** The sandbox_lease_holders viewer row id. */
184
- viewerId: string;
185
- /** The epoch the token is fenced to. For a Modal box this is the live LEASE
186
- * epoch (re-minted on box rollover). For a SELFHOSTED relay stream (M8b) this is
187
- * the session's swap `active_epoch`: the relay tracks the highest epoch any
188
- * viewer presented per channel and REJECTS a token with a lower epoch, so a
189
- * viewer whose token predates a swap-away cannot reach the machine the session
190
- * swapped off of. One field, two fences — the relay/in-box edge reads it as the
191
- * stale-viewer floor either way. */
192
- leaseEpoch: number;
193
- /** v1 is always "view"; "control" is the never-granted raw-input plane. */
194
- mode?: "view" | "control";
195
- /** The exposed stream port (noVNC); defaults to 6080. */
196
- port?: number;
197
- /** TTL in seconds; defaults to STREAM_TOKEN_DEFAULT_TTL_SECONDS. */
198
- ttlSeconds?: number;
199
- /** Override the issue clock (tests). Seconds since the epoch. */
200
- nowSeconds?: number;
201
- };
202
- /**
203
- * Mint a scoped stream token for one viewer holder. Builds the hard-narrow
204
- * StreamTokenPayload (the claim set the in-box edge / control plane validates)
205
- * and signs it with the resolved stream-token secret via the contracts HMAC
206
- * envelope (`ogs_` prefix). The token is RECORDED against the holder row by the
207
- * caller and is NEVER appended to the data-plane URL as a query param.
208
- */
209
- declare function mintStreamToken(secret: string, input: MintStreamTokenInput): Promise<string>;
210
- /**
211
- * Verify a scoped stream token. Returns the parsed claims on success, or null on
212
- * a bad prefix / malformed envelope / bad HMAC signature / schema-invalid claims
213
- * / expiry. Re-exports the contracts verify; the leaf is the agent-loop-free
214
- * import surface the API uses.
215
- *
216
- * The epoch fence (claim.leaseEpoch vs the LIVE lease epoch) and the
217
- * workspace+session scope are enforced at USE by the caller against the live
218
- * lease + route params — verify proves authenticity + freshness only.
219
- */
220
- declare function verifyStreamToken(secret: string, token: string, nowSeconds?: number): Promise<StreamTokenPayload | null>;
221
-
222
- declare const STREAM_PORT = 6080;
223
- declare const DISPLAY_STACK_TIMEOUT_MS = 90000;
224
- /** Desktop geometry for the framebuffer. v1 has no live RANDR: a resolution
225
- * change is a full down -> up restart (a separate op). */
226
- type DesktopGeometry = {
227
- width: number;
228
- height: number;
229
- dpi: number;
230
- };
231
- declare const DEFAULT_DESKTOP_GEOMETRY: DesktopGeometry;
232
- /** Thrown when a stage of the launch script failed. exitCode 11/12/13 map to
233
- * Xvfb / x11vnc / websockify respectively (the stage that died); 14 is the
234
- * PAINTABLE-FRAME gate (ports listening but scrot still yields an empty frame —
235
- * the display is up but not actually painting). Degradation is surfaced as a
236
- * value to viewers by the caller; this error is for diagnostics. */
237
- declare class DisplayStackError extends Error {
238
- readonly exitCode: number;
239
- readonly stage: "xvfb" | "x11vnc" | "websockify" | "paint" | "unknown";
240
- constructor(exitCode: number, output: string);
241
- }
242
- /** Thrown when the provider session cannot run commands (a headless-only
243
- * backend with neither `exec` nor `execCommand`). The desktop tier degrades to
244
- * Channel-A-only — the caller maps this to `DesktopStream.transport: null`. */
245
- declare class DisplayStackUnsupportedError extends Error {
246
- constructor(message: string);
247
- }
248
- type EnsureDisplayStackOptions = {
249
- geometry?: DesktopGeometry;
250
- /** The exposed stream port; defaults to 6080. */
251
- port?: number;
252
- /** Per-exec timeout; defaults to DISPLAY_STACK_TIMEOUT_MS. */
253
- timeoutMs?: number;
254
- };
255
- type EnsureDisplayStackResult = {
256
- /** The exposed port the stack listens on (websockify/noVNC). */
257
- port: number;
258
- geometry: DesktopGeometry;
259
- /** The raw `OPENGENI_DESKTOP_UP …` marker line, for diagnostics. Never
260
- * surfaced to viewers. */
261
- marker: string;
262
- };
263
- /**
264
- * Build the shell command that runs the idempotent up-script under an in-box
265
- * `flock`. The script is shipped in the image at /usr/local/bin/opengeni-desktop-up
266
- * (the canonical desktop image); we set the geometry/port env and wrap the call
267
- * in `flock` so two concurrent ensureDisplayStack callers (the API viewer op +
268
- * the agent turn, both racing after a rollover) serialize without a double
269
- * launch. The up-script's own per-stage PID guards make the second call a no-op.
270
- *
271
- * Exported (pure, side-effect-free) so the ensureDisplayStack unit test can
272
- * assert the exact command sequence without a live box.
273
- */
274
- declare function buildDisplayStackScript(options?: EnsureDisplayStackOptions): string;
275
- /**
276
- * Idempotently bring up the desktop display stack on the live box. Safe to call
277
- * N times (the in-box flock + the up-script's PID guards make a second call a
278
- * no-op). Resolves with the exposed port + geometry on success; throws
279
- * `DisplayStackError` on a stage failure and `DisplayStackUnsupportedError` when
280
- * the session cannot run commands.
281
- *
282
- * `session` is the externally-owned provider session (the `established.session`
283
- * from establishSandboxSessionFromEnvelope, or any SandboxSessionLike). We
284
- * prefer `session.exec` (structured `{exitCode}`) and fall back to
285
- * `session.execCommand` (bare string), inferring success from the up-script's
286
- * marker line in the fallback case.
287
- */
288
- declare function ensureDisplayStack(session: unknown, options?: EnsureDisplayStackOptions): Promise<EnsureDisplayStackResult>;
289
- /** Tear the stack down (down-script). Best-effort; never throws on a missing
290
- * process. Used by the geometry-change restart and cold/drain. */
291
- declare function tearDownDisplayStack(session: unknown): Promise<void>;
292
-
293
- declare const TERMINAL_SERVER_TIMEOUT_MS = 60000;
294
- /** Thrown when the ttyd launch failed inside the box. exitCode 14 maps to the
295
- * up-script's "ttyd failed to come up" stage; any other non-zero is unknown.
296
- * Degradation is surfaced as a value to clients by the caller (Terminal
297
- * transport falls back to sse-events / null); this error is for diagnostics. */
298
- declare class TerminalServerError extends Error {
299
- readonly exitCode: number;
300
- readonly stage: "ttyd" | "unknown";
301
- constructor(exitCode: number, output: string);
302
- }
303
- /** Thrown when the provider session cannot run commands (a headless-only backend
304
- * with neither `exec` nor `execCommand`). The terminal tier degrades to the
305
- * Channel-A sse-events firehose — the caller maps this to a `transport:null`
306
- * pty-ws (the read-only firehose still works). */
307
- declare class TerminalServerUnsupportedError extends Error {
308
- constructor(message: string);
309
- }
310
- type EnsureTerminalServerOptions = {
311
- /** The exposed terminal port; defaults to 7681 (ttyd default). */
312
- port?: number;
313
- /** Per-exec timeout; defaults to TERMINAL_SERVER_TIMEOUT_MS. */
314
- timeoutMs?: number;
315
- };
316
- type EnsureTerminalServerResult = {
317
- /** The exposed port ttyd listens on (PTY-over-websocket). */
318
- port: number;
319
- /** The raw `OPENGENI_TERMINAL_UP …` marker line, for diagnostics. Never
320
- * surfaced to clients. */
321
- marker: string;
322
- };
323
- /**
324
- * Build the shell command that runs the idempotent up-script under an in-box
325
- * `flock`. The script is shipped in the image at /usr/local/bin/opengeni-terminal-up
326
- * (the canonical desktop image, alongside opengeni-desktop-up); we set the port
327
- * env and wrap the call in `flock` so two concurrent ensureTerminalServer callers
328
- * (the API viewer op + the agent turn, both racing after a rollover) serialize
329
- * without a double launch. The up-script's own curl readiness probe makes the
330
- * second call a no-op.
331
- *
332
- * Exported (pure, side-effect-free) so the ensureTerminalServer unit test can
333
- * assert the exact command sequence without a live box. Mirrors
334
- * buildDisplayStackScript.
335
- */
336
- declare function buildTerminalServerScript(options?: EnsureTerminalServerOptions): string;
337
- /**
338
- * Idempotently bring up the ttyd PTY-over-websocket server on the live box. Safe
339
- * to call N times (the in-box flock + the up-script's curl readiness probe make a
340
- * second call a no-op). Resolves with the exposed port on success; throws
341
- * `TerminalServerError` on a launch failure and `TerminalServerUnsupportedError`
342
- * when the session cannot run commands.
343
- *
344
- * `session` is the externally-owned provider session (the `established.session`
345
- * from establishSandboxSessionFromEnvelope, or any SandboxSessionLike). We prefer
346
- * `session.exec` (structured `{exitCode}`) and fall back to `session.execCommand`
347
- * (bare string), inferring success from the up-script's marker line in the
348
- * fallback case. Mirrors ensureDisplayStack exactly.
349
- */
350
- declare function ensureTerminalServer(session: unknown, options?: EnsureTerminalServerOptions): Promise<EnsureTerminalServerResult>;
351
- /** Tear the terminal server down (down-script). Best-effort; never throws on a
352
- * missing process. Mirrors tearDownDisplayStack. */
353
- declare function tearDownTerminalServer(session: unknown): Promise<void>;
354
-
355
- /** The provider-resolved endpoint for an exposed port. Mirrors the SDK's
356
- * `ExposedPortEndpoint` (host/port/tls/query/...) WITHOUT importing the
357
- * agent-loop barrel — the leaf stays agent-loop-free. */
358
- type ExposedPortEndpoint = {
359
- host: string;
360
- port: number;
361
- tls?: boolean;
362
- query?: string;
363
- protocol?: string;
364
- url?: string;
365
- /** The URL path the socket connects on. Modal/Daytona/Blaxel serve the edge at
366
- * the root (`/`, the default); the selfhosted relay serves it at `/stream`
367
- * (M8b). When set, buildStreamUrl uses it instead of the root. */
368
- path?: string;
369
- [key: string]: unknown;
370
- };
371
- /** Thrown when the provider cannot expose the stream port (no resolveExposedPort,
372
- * or the provider tunnel lookup failed). The caller degrades the desktop cell to
373
- * `transport:null` (a value, never a crash) — a headless-only provider or a
374
- * transient tunnel failure must not fail the whole handshake. */
375
- declare class StreamPortUnavailableError extends Error {
376
- readonly cause?: unknown | undefined;
377
- constructor(message: string, cause?: unknown | undefined);
378
- }
379
- type ExposeStreamPortInput = {
380
- workspaceId: string;
381
- sessionId: string;
382
- /** The sandbox_lease_holders viewer row id the token is scoped to. */
383
- viewerId: string;
384
- /** The live lease epoch — the fence the token is pinned to. */
385
- leaseEpoch: number;
386
- /** The HMAC secret for the scoped stream token (resolveStreamTokenSecret). */
387
- streamTokenSecret: string;
388
- /** The exposed stream port; defaults to 6080. */
389
- port?: number;
390
- /** Token TTL in seconds; defaults to STREAM_TOKEN_DEFAULT_TTL_SECONDS. */
391
- ttlSeconds?: number;
392
- /** The framebuffer geometry to echo back to the client. */
393
- resolution?: [number, number];
394
- /** Override the issue clock (tests). Seconds since the epoch. */
395
- nowSeconds?: number;
396
- };
397
- type ExposeStreamPortResult = {
398
- /** The direct-to-provider WS URL the viewer connects to (provider-scoped; the
399
- * OpenGeni token is NOT appended). */
400
- url: string;
401
- /** The scoped OpenGeni stream token — recorded against the holder, NEVER a URL
402
- * query param. */
403
- token: string;
404
- /** ISO absolute expiry of the token (the rotation hot-swap window backstop). */
405
- expiresAt: string;
406
- /** The pixel transport the client speaks. */
407
- transport: "vnc-ws";
408
- /** The reference noVNC client the SDK helper mounts. */
409
- client: "novnc";
410
- resolution: [number, number];
411
- leaseEpoch: number;
412
- };
413
- /**
414
- * Assemble the direct-to-provider WS URL from a resolved endpoint. The SDK's
415
- * `urlForExposedPort(endpoint,'ws')` is the canonical tls-aware, IPv6-bracketing,
416
- * provider-query-preserving assembler — we reimplement its exact logic here so
417
- * the leaf stays agent-loop-free (the helper lives behind the bare
418
- * `@openai/agents-core` root, which the import-discipline test forbids). The
419
- * provider's own `endpoint.query` (Blaxel `bl_preview_token`, Daytona signed
420
- * token) is preserved; the OpenGeni token is NOT appended (it is recorded against
421
- * the holder + validated at the in-box websockify edge).
422
- */
423
- declare function buildStreamUrl(endpoint: ExposedPortEndpoint): string;
424
- /**
425
- * Resolve the provider's scoped tunnel for the stream port and mint the scoped
426
- * OpenGeni stream token. Returns a coherent `{url, token, expiresAt, transport,
427
- * client, resolution}` cell the caller records on the lease (data_plane_url) and
428
- * returns in the DesktopStream handshake.
429
- *
430
- * Throws `StreamPortUnavailableError` when the provider session cannot resolve
431
- * the port (no `resolveExposedPort`, or the tunnel lookup failed) — the caller
432
- * maps this to a `transport:null` degradation (a value, never a crash).
433
- */
434
- declare function exposeStreamPort(session: unknown, input: ExposeStreamPortInput): Promise<ExposeStreamPortResult>;
435
-
436
- type RecordingCodec = "h264-mp4" | "vp9-webm";
437
- type RecordingContentType = "video/mp4" | "video/webm";
438
- declare function contentTypeForCodec(codec: RecordingCodec): RecordingContentType;
439
- declare function extForCodec(codec: RecordingCodec): string;
440
- /** No exec/execCommand on the session — the box cannot run ffmpeg. */
441
- declare class RecordingUnavailableError extends Error {
442
- constructor(message: string);
443
- }
444
- /** ffmpeg failed, the file is missing, or the byte read failed. */
445
- declare class RecordingError extends Error {
446
- readonly reason: "ffmpeg-error" | "box-death" | "max-bytes-exceeded" | "display-unavailable";
447
- constructor(message: string, reason: "ffmpeg-error" | "box-death" | "max-bytes-exceeded" | "display-unavailable");
448
- }
449
- type StartRecordingInput = {
450
- recordingId: string;
451
- codec?: RecordingCodec;
452
- framerate?: number;
453
- maxSeconds?: number;
454
- dimensions?: [number, number];
455
- display?: string;
456
- runAs?: string;
457
- tmpDir?: string;
458
- };
459
- type RecordingProcess = {
460
- recordingId: string;
461
- codec: RecordingCodec;
462
- boxPath: string;
463
- pidFile: string;
464
- dimensions: [number, number];
465
- framerate: number;
466
- /** epoch-ms when ffmpeg was launched (for duration computation, F14). */
467
- startedAt: number;
468
- display: string;
469
- runAs?: string;
470
- };
471
- /**
472
- * Launch ffmpeg x11grab on :0 → an mp4/webm file on the box. Backgrounded with
473
- * `nohup … & echo $!` so the launch returns immediately (F12 — the exec does not
474
- * block on the recording). A hard `-t <maxSeconds>` ceiling bounds a runaway file
475
- * across a multi-day turn. Returns the handle the caller carries to stop+finalize.
476
- */
477
- declare function startRecording(session: unknown, input: StartRecordingInput): Promise<RecordingProcess>;
478
- /**
479
- * SIGINT ffmpeg (so it writes a clean moov atom / webm trailer) and wait for the
480
- * pid to exit. Bounded well under the yield window (F3). Idempotent: a missing
481
- * pid file is a no-op.
482
- */
483
- declare function stopRecording(session: unknown, proc: RecordingProcess): Promise<void>;
484
- type FinalizeRecordingResult = {
485
- bytes: Uint8Array;
486
- contentType: RecordingContentType;
487
- sizeBytes: number;
488
- durationSeconds: number;
489
- };
490
- /**
491
- * Read the finalized recording bytes off the box.
492
- *
493
- * TRANSPORT: the bytes are read via a DIRECT exec (`base64 <path>` over stdout),
494
- * NOT via session.readFile(). The recording artifact lives at an absolute /tmp
495
- * path on purpose — recordings must never be written inside the user's workspace
496
- * /git tree — but session.readFile() resolves every path against the manifest
497
- * workspace root and rejects anything outside it ("Sandbox path … escapes the
498
- * workspace root"), which fataled finalize. Raw exec runs unrestricted shell, so
499
- * `base64` reads the /tmp file directly; we decode the base64 back to bytes here.
500
- * The byte-read exec passes `maxOutputTokens: null` so the provider never
501
- * truncates a large recording's base64.
502
- *
503
- * F8: we DO NOT assume any over-limit behavior. First `stat` the file size on the
504
- * box; if it exceeds maxBytes, fail `max-bytes-exceeded` (never upload a truncated
505
- * video). Otherwise read the raw bytes.
506
- *
507
- * F9: this does NOT delete the box file. The caller deletes it (deleteRecordingArtifacts)
508
- * ONLY after the storage PUT + `available` commit — so a failed upload leaves the
509
- * bytes recoverable on the box for a retry.
510
- *
511
- * F14: duration is wall-clock (now − startedAt), a close approximation of the
512
- * SIGINT-flushed video length.
513
- */
514
- declare function readRecordingBytes(session: unknown, proc: RecordingProcess, maxBytes?: number): Promise<FinalizeRecordingResult>;
515
- /**
516
- * Delete the box artifacts. F9: call this ONLY after the storage PUT confirmed
517
- * and the `available` row committed — never before. Best-effort; never throws.
518
- */
519
- declare function deleteRecordingArtifacts(session: unknown, proc: RecordingProcess): Promise<void>;
520
- /** The storage object key for a recording artifact (parallels the file-asset layout). */
521
- declare function recordingStorageKey(workspaceId: string, sessionId: string, recordingId: string, codec: RecordingCodec): string;
522
-
523
- type ChannelAExecResult = {
524
- output?: string;
525
- stdout?: string;
526
- stderr?: string;
527
- exitCode?: number | null;
528
- sessionId?: number;
529
- wallTimeSeconds?: number;
530
- };
531
- type ChannelAExecArgs = {
532
- cmd: string;
533
- workdir?: string | undefined;
534
- shell?: string | undefined;
535
- login?: boolean | undefined;
536
- tty?: boolean | undefined;
537
- yieldTimeMs?: number | undefined;
538
- maxOutputTokens?: number | undefined;
539
- runAs?: string | undefined;
540
- };
541
- type ChannelAEditor = {
542
- createFile?(op: unknown): Promise<unknown>;
543
- updateFile?(op: unknown): Promise<unknown>;
544
- deleteFile?(op: unknown): Promise<unknown>;
545
- };
546
- type ChannelASession = {
547
- exec?(args: ChannelAExecArgs): Promise<ChannelAExecResult>;
548
- execCommand?(args: ChannelAExecArgs): Promise<string>;
549
- readFile?(args: {
550
- path: string;
551
- runAs?: string;
552
- maxBytes?: number;
553
- }): Promise<string | Uint8Array>;
554
- writeStdin?(args: {
555
- sessionId: number;
556
- chars?: string;
557
- yieldTimeMs?: number;
558
- maxOutputTokens?: number;
559
- }): Promise<string>;
560
- createEditor?(runAs?: string): ChannelAEditor;
561
- supportsPty?(): boolean;
562
- };
563
- declare class ChannelAValidationError extends Error {
564
- constructor(message: string);
565
- }
566
- declare class ChannelAConflictError extends Error {
567
- constructor(message: string);
568
- }
569
- declare class ChannelANotFoundError extends Error {
570
- constructor(message: string);
571
- }
572
- declare class ChannelAUnsupportedError extends Error {
573
- constructor(message: string);
574
- }
575
- type ChannelAEmitter = (events: {
576
- type: SessionEventType;
577
- payload: unknown;
578
- }[]) => Promise<void>;
579
- type SandboxChannelAServiceOptions = {
580
- session: ChannelASession;
581
- workspaceRoot?: string;
582
- leaseEpoch?: number;
583
- revision?: number;
584
- emit?: ChannelAEmitter;
585
- runAs?: string;
586
- };
587
- declare class SandboxChannelAService {
588
- private readonly session;
589
- private readonly workspaceRoot;
590
- private readonly leaseEpoch;
591
- private revision;
592
- private readonly emit?;
593
- private readonly runAs?;
594
- constructor(opts: SandboxChannelAServiceOptions);
595
- /** Capability probe — the compact Channel-A projection. */
596
- capabilities(repos?: string[]): SessionStructuredCapabilities;
597
- private run;
598
- fsList(req: FsListRequest): Promise<FsListResponse>;
599
- fsRead(req: FsReadRequest): Promise<FsReadResponse>;
600
- /** Read a file by base64-ing it through exec. Binary-safe and — crucially —
601
- * NOT subject to the provider's native-readFile workspace-escape validation,
602
- * so it can render a symlink whose target lives outside /workspace (the link
603
- * node itself is in-workspace). `base64 <path>` follows the symlink. */
604
- private fsReadViaExec;
605
- private shapeRead;
606
- fsWrite(req: FsWriteRequest): Promise<FsWriteResponse>;
607
- private tryEditorWrite;
608
- fsDelete(req: FsDeleteRequest): Promise<FsDeleteResponse>;
609
- fsMove(req: FsMoveRequest): Promise<FsMoveResponse>;
610
- fsMkdir(req: FsMkdirRequest): Promise<FsMkdirResponse>;
611
- gitStatus(req: GitStatusRequest): Promise<GitStatusResponse>;
612
- gitDiff(req: GitDiffRequest): Promise<GitDiffResponse>;
613
- gitLog(req: GitLogRequest): Promise<GitLogResponse>;
614
- gitShow(req: GitShowRequest): Promise<GitShowResponse>;
615
- /** Detect repo roots within the workspace (for the Git.repos capability). */
616
- detectRepos(): Promise<string[]>;
617
- /** Run a bounded command, return buffered stdout/stderr + exit code inline. The
618
- * long-running tail (when the process hasn't exited within timeoutMs) keeps
619
- * running in-box; if emitStream is set the buffered output is also published as
620
- * the agent firehose so other viewers see it. */
621
- terminalExec(req: TerminalExecRequest): Promise<TerminalExecResponse>;
622
- /** Open an interactive PTY: exec the shell with tty:true, yielding the numeric
623
- * exec-session id the caller persists (ptyId<->execSessionId) so subsequent
624
- * writeStdin can drive it. Returns the supportsInput gate (false when the
625
- * backend has no writeStdin). The caller emits terminal.pty.started after it
626
- * persists the row. */
627
- ptyOpen(req: PtyOpenRequest, ptyId: string): Promise<{
628
- response: PtyOpenResponse;
629
- execSessionId: number | null;
630
- shell: string;
631
- initialOutput: string;
632
- }>;
633
- /** Drive an open PTY's stdin. Returns the drained output (the caller publishes
634
- * it as terminal.pty.output.delta). Throws ChannelAUnsupportedError when the
635
- * backend has no writeStdin. */
636
- ptyWrite(_req: PtyWriteRequest, execSessionId: number, data: string): Promise<string>;
637
- /** Resize an open PTY (SIGWINCH via stty against the exec-session). The SDK has
638
- * no resize method; stty in the same tty session updates the geometry. */
639
- ptyResize(req: PtyResizeRequest, execSessionId: number): Promise<void>;
640
- /** Close an open PTY: write exit/EOF. The caller marks the row closed + emits
641
- * terminal.pty.exited. */
642
- ptyClose(_req: PtyCloseRequest, execSessionId: number | null): Promise<void>;
643
- /** The current FS revision (for the caller to persist/seed). */
644
- currentRevision(): number;
645
- private joinRoot;
646
- private repoWorkdir;
647
- private emitEvents;
648
- private emitFsChanged;
649
- /** Re-probe git after a mutation and emit git.changed (best-effort, used by the
650
- * worker agent-turn side after FS-mutating tools). */
651
- emitGitChanged(repoPath: string, reason: GitChangedPayload["reason"]): Promise<void>;
652
- }
653
- declare function stripExecBanner(raw: string): string;
654
- declare function isWorkspaceEscapeError(error: unknown): boolean;
655
- declare function isExecSessionLostBanner(out: string, execSessionId: number): boolean;
656
- declare function parseExecBannerSessionId(raw: string): number | null;
657
- declare function assertSafeRelPath(p: string): string;
658
- declare function parsePorcelainV2(z: string): Omit<GitStatusResponse, "revision">;
659
- type NumstatEntry = {
660
- additions: number;
661
- deletions: number;
662
- binary: boolean;
663
- oldPath: string | null;
664
- newPath: string;
665
- };
666
- declare function parseNumstatZ(z: string): NumstatEntry[];
667
- declare function parseUnifiedPatch(patch: string): {
668
- hunks: GitDiffHunk[];
669
- status: GitFileStatusCode;
670
- };
671
-
672
- type SelfhostedUnavailableReason = Extract<CapabilityUnavailableReason, "agent_offline" | "agent_reconnecting" | "consent_required" | "display_unavailable">;
673
- /**
674
- * The selfhosted control-plane transport seam. ONE method: `request` — send a
675
- * `ControlRequest` to the agent addressed by subject and await its
676
- * `ControlResponse`. The subject is `subjectFor(workspaceId, agentId)`.
677
- *
678
- * The CONTRACT every implementor MUST honour (the M3 ruling): a
679
- * no-responder / request-timeout is NOT an exception that means "not found" — it
680
- * is surfaced as a `ControlResponse` carrying an `AgentError` with code
681
- * `AGENT_OFFLINE` (no responder at all) or, when the caller can distinguish a
682
- * transient blip, `TIMEOUT` (→ `agent_reconnecting`). The session maps these to
683
- * the runtime error taxonomy; it NEVER lets agent-offline look like a provider
684
- * NotFound (which would cold-create a rival box for a user's real machine).
685
- */
686
- interface ControlRpc {
687
- request(subject: string, req: ControlRequest, opts: {
688
- timeoutMs: number;
689
- }): Promise<ControlResponse>;
690
- }
691
- /** The control-plane RPC subject for an enrolled agent — its subscription IS the
692
- * registry (the binding two-plane decision). */
693
- declare function subjectFor(workspaceId: string, agentId: string): string;
694
- /**
695
- * The runtime-level error a `SelfhostedSession` op throws when the agent returns
696
- * an `AgentError` (or no responder / timeout maps to one). It carries:
697
- * - `code` — the wire `ErrorCode` (single-source-of-truth);
698
- * - `reason` — the negotiated `CapabilityUnavailableReason` the capability /
699
- * liveness surface uses (`agent_offline` / `agent_reconnecting`
700
- * / `consent_required`), or null for op-level errors
701
- * (OS/NOT_FOUND/UNSUPPORTED/STREAM/PROTOCOL) that are not a
702
- * machine-liveness condition;
703
- * - `retryable`— whether the caller should re-resolve + retry (DRAINING /
704
- * FENCED / a reconnecting blip);
705
- * - `notFound` — ALWAYS the provider-NotFound discriminator value: for
706
- * selfhosted this is true ONLY for an OS-level NOT_FOUND of a
707
- * path/ref (a real "the file does not exist"), and is FALSE for
708
- * AGENT_OFFLINE (the machine isn't recreatable — never let the
709
- * lease cold-create a rival). `isProviderSandboxNotFoundError`
710
- * reads this.
711
- */
712
- declare class SelfhostedControlError extends Error {
713
- readonly name = "SelfhostedControlError";
714
- readonly code: ErrorCode;
715
- readonly reason: SelfhostedUnavailableReason | null;
716
- readonly retryable: boolean;
717
- readonly fenced: boolean;
718
- readonly draining: boolean;
719
- readonly agentOffline: boolean;
720
- readonly osNotFound: boolean;
721
- readonly detail: Record<string, string>;
722
- constructor(input: {
723
- message: string;
724
- code: ErrorCode;
725
- reason: SelfhostedUnavailableReason | null;
726
- retryable: boolean;
727
- fenced?: boolean;
728
- draining?: boolean;
729
- agentOffline?: boolean;
730
- osNotFound?: boolean;
731
- detail?: Record<string, string>;
732
- });
733
- }
734
- /**
735
- * Map an `AgentError` (from a `ControlResponse`) to the runtime
736
- * `SelfhostedControlError`. THE load-bearing mapping (the M3 ruling):
737
- * - AGENT_OFFLINE → reason `agent_offline`, agentOffline=true,
738
- * osNotFound=FALSE (NEVER a provider NotFound).
739
- * - TIMEOUT (a transient missed-window / no-responder blip the caller marked
740
- * retryable) → reason `agent_reconnecting`.
741
- * - CONSENT_REQUIRED → reason `consent_required`.
742
- * - DRAINING → no capability reason; retryable (turn pauses + retries).
743
- * - FENCED → no capability reason; retryable (the existing
744
- * epoch-fence retry; the caller re-resolves + retries).
745
- * - NOT_FOUND → an OS-level path/ref NotFound — osNotFound=true (a
746
- * real "file does not exist"), no machine-liveness
747
- * reason. (This is the ONLY NotFound; it is NOT the
748
- * box-gone NotFound that licenses a cold restore.)
749
- * - OS / UNSUPPORTED / STREAM / PROTOCOL / UNSPECIFIED → op-level error, no
750
- * reason, non-retryable.
751
- */
752
- declare function agentErrorToControlError(err: AgentError): SelfhostedControlError;
753
- /** Build a synthesized AGENT_OFFLINE `AgentError` — the control plane uses this
754
- * when no agent responds on the subject at all. */
755
- declare function offlineAgentError(message?: string): AgentError;
756
- /** Build a synthesized TIMEOUT `AgentError` — the control plane uses this when a
757
- * responder existed but the request timed out (a transient blip → reconnecting). */
758
- declare function timeoutAgentError(message?: string): AgentError;
759
- /**
760
- * The minimal NATS request/reply surface `NatsControlRpc` needs. It mirrors the
761
- * `nats` `NatsConnection.request` signature WITHOUT importing `nats` into the
762
- * agent-loop-free runtime leaf: the API/worker injects the live connection (the
763
- * SAME `@opengeni/events` bus connection). A factory may return `null` when NATS
764
- * is not configured (boot must not require a live NATS) — `NatsControlRpc` then
765
- * surfaces `agent_offline` for every request rather than throwing.
766
- */
767
- interface NatsRequestConnection {
768
- request(subject: string, payload: Uint8Array, opts: {
769
- timeout: number;
770
- }): Promise<{
771
- data: Uint8Array;
772
- }>;
773
- }
774
- /**
775
- * A thin `ControlRpc` over a NATS request/reply connection. Constructed with a
776
- * LAZY factory: the connection is resolved on first `request` (so boot never
777
- * requires a live NATS). A null factory result, a no-responder error, or a
778
- * request timeout each yield a `ControlResponse` carrying a synthesized
779
- * `AgentError` (AGENT_OFFLINE / TIMEOUT) — NEVER a thrown transport error and
780
- * NEVER a NotFound.
781
- *
782
- * The factory is async and memoized; it may itself dial the bus. M4 replaces the
783
- * factory's body with the Accounts-scoped, hardened connection — this class's
784
- * shape does not change.
785
- */
786
- declare class NatsControlRpc implements ControlRpc {
787
- private readonly connect;
788
- private connection;
789
- constructor(connect: () => Promise<NatsRequestConnection | null>);
790
- private resolveConnection;
791
- request(subject: string, req: ControlRequest, opts: {
792
- timeoutMs: number;
793
- }): Promise<ControlResponse>;
794
- }
795
- /** A `ControlResponse` carrying a synthesized AGENT_OFFLINE error. */
796
- declare function offlineControlResponse(requestId: string): ControlResponse;
797
- /** A `ControlResponse` carrying a synthesized TIMEOUT error (→ reconnecting). */
798
- declare function timeoutControlResponse(requestId: string): ControlResponse;
799
-
800
- /** The V4A-diff applier the SDK's apply_patch editor uses. The leaf cannot import
801
- * `@openai/agents`'s `applyDiff` (the agent-loop root the leaf forbids), so the
802
- * runtime barrel (`packages/runtime/src/index.ts`, which DOES import that root)
803
- * injects it via `setSelfhostedApplyDiff` at module load. Until injected,
804
- * `createEditor()` surfaces a clear error rather than a silent wrong-edit. */
805
- type SelfhostedApplyDiff = (input: string, diff: string, mode?: "default" | "create") => string;
806
- /** Register the SDK's `applyDiff` so `SelfhostedSession.createEditor()` can apply
807
- * V4A diffs over the NATS fs ops. Called once by the runtime barrel. */
808
- declare function setSelfhostedApplyDiff(fn: SelfhostedApplyDiff): void;
809
- /** The structural Editor surface the SDK's filesystem capability consumes (the
810
- * three apply_patch operations). Mirrors `@openai/agents-core`'s `Editor`. */
811
- interface SelfhostedEditor {
812
- createFile(operation: {
813
- path: string;
814
- diff: string;
815
- }, context?: unknown): Promise<{
816
- output?: string;
817
- } | void>;
818
- updateFile(operation: {
819
- path: string;
820
- diff: string;
821
- moveTo?: string;
822
- }, context?: unknown): Promise<{
823
- output?: string;
824
- } | void>;
825
- deleteFile(operation: {
826
- path: string;
827
- }, context?: unknown): Promise<{
828
- output?: string;
829
- } | void>;
830
- }
831
- /** The image tool-output shape the SDK's view_image tool expects (mirror of
832
- * `ToolOutputImage` — not re-exported by `@openai/agents/sandbox`, so structural). */
833
- interface SelfhostedImageOutput {
834
- type: "image";
835
- image: {
836
- data: Uint8Array;
837
- mediaType: string;
838
- };
839
- }
840
- /** Default control-op timeout. A transient miss surfaces as `agent_reconnecting`
841
- * (the turn pauses + retries); it is NOT a hard failure. */
842
- declare const SELFHOSTED_DEFAULT_TIMEOUT_MS = 30000;
843
- /** The relay-URL shape config the session needs to build a stream endpoint. M8b
844
- * wires the real relay deployment behind THIS seam so `buildStreamUrl` works
845
- * unchanged behind `resolveExposedPort`. */
846
- interface SelfhostedRelayConfig {
847
- /** The relay edge host (no scheme), e.g. "relay.opengeni.ai". */
848
- host: string;
849
- /** The relay port. Defaults to 443 (the relay terminates TLS). */
850
- port?: number;
851
- /** Whether the relay endpoint is TLS (wss/https). Defaults true. */
852
- tls?: boolean;
853
- /** The relay's stream-dial path (the `opengeni-relay` wss route). Defaults to
854
- * "/stream" — the route the relay listens on (M8b). */
855
- path?: string;
856
- }
857
- /** The relay's default wss dial path (the `opengeni-relay` server route). */
858
- declare const SELFHOSTED_RELAY_STREAM_PATH = "/stream";
859
- interface SelfhostedSessionDeps {
860
- workspaceId: string;
861
- agentId: string;
862
- controlRpc: ControlRpc;
863
- relay: SelfhostedRelayConfig;
864
- /** The lease/active epoch this session is fenced under (echoed on every
865
- * ControlRequest so the agent can reject a stale op with ERROR_CODE_FENCED).
866
- * Defaults to 0 (no fence) for the negotiation-only / test path. */
867
- epoch?: number;
868
- /** Override the control-op timeout (tests). */
869
- timeoutMs?: number;
870
- /**
871
- * The run's declared sandbox environment — the SAME `Record<string,string>` the
872
- * worker turn passes to `runtime.buildAgent`'s `sandboxEnvironment` (and that the
873
- * agent's TARGET manifest, `buildManifest`, carries). The SDK injects this
874
- * selfhosted session NON-OWNED and applies the agent's manifest as a provided-
875
- * session delta; `validateNoEnvironmentDelta` throws "Live sandbox sessions cannot
876
- * change manifest environment variables" on ANY env mismatch. So `state.manifest`'s
877
- * `environment` MUST EQUAL the turn's environment for the delta to be empty. The
878
- * selfhosted exec routes over NATS and does NOT consume the env, but the manifest
879
- * must carry it for parity. Omitted → `{}` (the negotiation-only / test path,
880
- * which never applies a turn manifest, so there is no delta to validate).
881
- */
882
- environment?: Record<string, string>;
883
- /**
884
- * The session's working directory — the BASE every path/cwd is rooted under (see
885
- * `toMachinePath` / SELFHOSTED_VIRTUAL_ROOT). A launch-workspace_root-relative
886
- * subdir (resolved under workspace_root by the agent's `resolve_cwd`) or an
887
- * absolute machine path. Omitted/empty (the default) ⇒ "" ⇒ today's behavior
888
- * exactly (an empty cwd lets the agent substitute its workspace_root).
889
- */
890
- workingDir?: string;
891
- }
892
- /** The Channel-A `exec` result shape (a structural superset of the SDK's). */
893
- interface SelfhostedExecResult {
894
- output: string;
895
- stdout: string;
896
- stderr: string;
897
- exitCode: number | null;
898
- }
899
- /** The `exec` args the structural surface accepts (mirrors ChannelAExecArgs). */
900
- interface SelfhostedExecArgs {
901
- cmd: string;
902
- workdir?: string | undefined;
903
- shell?: string | undefined;
904
- login?: boolean | undefined;
905
- tty?: boolean | undefined;
906
- runAs?: string | undefined;
907
- }
908
- /**
909
- * The persistable session state. For selfhosted this is `{agentId}` ONLY — there
910
- * is NO provider box id, no snapshot, no manifest. Resume re-addresses the live
911
- * subject; the machine itself is the persistence (`persistable:false`).
912
- */
913
- interface SelfhostedSessionState {
914
- agentId: string;
915
- }
916
- /**
917
- * A live selfhosted session — the structural `SandboxSessionLike` surface over a
918
- * `ControlRpc`. Mirrors Modal's session shape so Channel-A/viewer/computer-use
919
- * consume it unchanged.
920
- */
921
- declare class SelfhostedSession {
922
- readonly backendId: "selfhosted";
923
- readonly workspaceId: string;
924
- readonly agentId: string;
925
- private readonly controlRpc;
926
- private readonly relay;
927
- private readonly epoch;
928
- private readonly timeoutMs;
929
- private readonly subject;
930
- /** The session working directory — the path/cwd base every op is rooted under
931
- * (see `toMachinePath`). "" by default ⇒ today's workspace_root behavior. */
932
- private readonly workingDir;
933
- /**
934
- * The structural `state` slice consumers read. `agentId`/`instanceId` serve the
935
- * channel-a `readInstanceId` + docker-network decoration (the agentId IS the
936
- * identity). `manifest` is the slice the @openai/agents SDK reads AND writes per
937
- * turn (serializeManifestEnvironment / validateProvidedSessionManifestUpdate read
938
- * `manifest.root` + iterate `manifest.environment`; providedSessionManifest WRITES
939
- * `state.manifest = next`). It must be a real, MUTABLE Manifest field — when the
940
- * RoutingSandboxSession proxy resolves THIS as the active backend it returns
941
- * `session.state` BY REFERENCE, so the SDK's read and write must both land on a
942
- * well-formed Manifest here (defined `root`, object `environment`). Without it the
943
- * SDK crashes with `undefined is not an object (evaluating 'current.root')`.
944
- *
945
- * `manifest` is intentionally a plain mutable field (not `readonly`) so the SDK's
946
- * `state.manifest = next` write succeeds. It is NOT part of the persistable state
947
- * (`serializeSessionState` round-trips `{agentId}` only).
948
- *
949
- * `environment` is the SDK `SandboxSessionState.environment` (a `Record<string,
950
- * string>`). It MUST be present because the GROUP box's client serializes THIS
951
- * (the active backend's) state at end-of-turn — the non-owned injected session is
952
- * serialized via the CONFIGURED client (modal in prod), NOT the selfhosted client.
953
- * Modal's `serializeRemoteSandboxSessionState` does `Object.entries(state.environment)`;
954
- * an absent field crashes the post-turn RunState serialize with "Object.entries
955
- * requires that input parameter not be null or undefined". It carries the run's
956
- * threaded environment (or `{}`). The resulting modal-tagged envelope is inert for
957
- * selfhosted (resume re-addresses the machine by agentId via the lease pointer,
958
- * never from this SDK envelope), so its only job is to not crash the serialize.
959
- */
960
- readonly state: {
961
- agentId: string;
962
- instanceId: string;
963
- manifest: Manifest;
964
- environment: Record<string, string>;
965
- };
966
- constructor(deps: SelfhostedSessionDeps);
967
- /** Issue a control op, decoding the agent's reply or throwing the mapped
968
- * `SelfhostedControlError` on an AgentError (incl. a synthesized offline /
969
- * timeout error from the transport). */
970
- private call;
971
- /** Channel-A `exec`: run a command on the machine and return its output. */
972
- exec(args: SelfhostedExecArgs): Promise<SelfhostedExecResult>;
973
- /** SDK shell capability `execCommand`: run a command and return its stdout (the
974
- * `exec_command` tool). Selfhosted exec is non-interactive (no PTY) — `tty` is
975
- * ignored; `supportsPty()` is false so the SDK never offers a stdin session. */
976
- execCommand(args: {
977
- cmd: string;
978
- workdir?: string;
979
- runAs?: string;
980
- }): Promise<string>;
981
- /** SDK shell capability never calls this (gated on `supportsPty()` which is
982
- * false), but the surface advertises it. Selfhosted exec has no interactive PTY
983
- * session over the structured RPC, so a stdin write is unsupported. */
984
- supportsPty(): boolean;
985
- /** SDK filesystem capability `view_image`: read the image bytes off the machine
986
- * and wrap them in the tool-output image shape (magic-byte sniff + path fallback,
987
- * mirroring the SDK's `imageOutputFromBytes`). */
988
- viewImage(args: {
989
- path: string;
990
- runAs?: string;
991
- }): Promise<SelfhostedImageOutput>;
992
- /** SDK skills/filesystem `pathExists`: whether a path exists on the machine. */
993
- pathExists(path: string, _runAs?: string): Promise<boolean>;
994
- /** SDK skills `listDir`: list a directory as `{name, path, type}[]`. */
995
- listDir(args: {
996
- path: string;
997
- runAs?: string;
998
- }): Promise<Array<{
999
- name: string;
1000
- path: string;
1001
- type: "file" | "dir" | "other";
1002
- }>>;
1003
- /** SDK manifest-delta `materializeEntry`: a NO-OP for selfhosted. Source
1004
- * materialization (cloning repos / staging files into the box) is how cloud
1005
- * providers prepare a fresh box; a bring-your-own machine already owns its
1006
- * filesystem and is prepared by the agent itself, so there is nothing to stage.
1007
- * Present (not absent) so the SDK's provided-session manifest apply path — which
1008
- * requires `applyManifest()` OR `materializeEntry()` when the agent declares
1009
- * entries — is satisfied without error. The selfhosted manifest declares no
1010
- * entries, so in practice this is never invoked with a real entry. */
1011
- materializeEntry(_args: {
1012
- path: string;
1013
- entry: unknown;
1014
- runAs?: string;
1015
- }): Promise<void>;
1016
- /** SDK filesystem capability `createEditor`: the apply_patch host. Applies V4A
1017
- * diffs over the NATS fs ops (read → applyDiff → write). `applyDiff` is the SDK's
1018
- * own parser, injected by the runtime barrel (the leaf cannot import it). */
1019
- createEditor(runAs?: string): SelfhostedEditor;
1020
- /** Channel-A `readFile`: read a file off the machine (binary-safe). */
1021
- readFile(args: {
1022
- path: string;
1023
- runAs?: string;
1024
- maxBytes?: number;
1025
- }): Promise<Uint8Array>;
1026
- /** Write a file onto the machine (the fs surface the descriptor advertises). */
1027
- writeFile(args: {
1028
- path: string;
1029
- content: string | Uint8Array;
1030
- createParents?: boolean;
1031
- append?: boolean;
1032
- }): Promise<number>;
1033
- /** List a directory on the machine. */
1034
- listFiles(args: {
1035
- path: string;
1036
- recursive?: boolean;
1037
- }): Promise<NonNullable<ControlResponse["result"]> & {
1038
- $case: "fsList";
1039
- }>;
1040
- /** Stat a path on the machine. */
1041
- statFile(args: {
1042
- path: string;
1043
- }): Promise<{
1044
- exists: boolean;
1045
- }>;
1046
- /** Computer-use WRITE op: inject one synthetic desktop input event (pointer/key/
1047
- * scroll) on the machine's OWN display. The agent injects via CGEvent (macOS) /
1048
- * XTEST (Linux) and CONSENT-GATES it — an unconsented call never touches the OS
1049
- * and surfaces the mapped control error (ERROR_CODE_CONSENT_REQUIRED) via `call()`. */
1050
- desktopInput(event: DesktopInputRequest["event"]): Promise<void>;
1051
- /** Computer-use VIEW op: capture a single PNG screenshot of the machine's desktop
1052
- * plus its geometry (via ScreenCaptureKit / x11). NOT consent-gated (a view op —
1053
- * the view/control decoupling), so it works with a display but no screen-control
1054
- * consent. Returns the raw encoded bytes + width/height. */
1055
- screenshot(): Promise<{
1056
- png: Uint8Array;
1057
- width: number;
1058
- height: number;
1059
- }>;
1060
- /** A cheap liveness probe — request a Ping on the subject; returns true iff a
1061
- * responder answered (no AgentError). Used by `negotiateSelfhostedCapabilities`.
1062
- * The wire `nonce` is a uint64 (a numeric string), so the default is a random
1063
- * numeric value — NOT a UUID (which would fail proto uint64 encoding). */
1064
- ping(nonce?: string): Promise<boolean>;
1065
- /**
1066
- * Resolve an exposed port to a relay stream endpoint (the viewer/pty plane).
1067
- * Returns the relay URL SHAPE — `{host:relay, port, tls, query:channel-key}` —
1068
- * after asking the agent to ensure a stream channel for the port. M8b wires the
1069
- * real relay tier (the byte pump) behind THIS seam.
1070
- *
1071
- * THE CHANNEL-KEY QUERY (the M8b relay-dial contract, dossier §10.5): the relay
1072
- * routes by `{workspaceId, agentId, port}` — the EXACT `ChannelKey::query` the
1073
- * agent's relay client (`opengeni-agent-stream`) appends when it registers the
1074
- * producer side: `ws=<workspaceId>&agent=<agentId>&port=<port>`. We append the
1075
- * agent-registered `channel=<channelId>` as a correlation hint. So the viewer
1076
- * dials `wss://<relay>/stream?ws=&agent=&port=&channel=` and presents the minted
1077
- * `ogs_` token in-band (NEVER as a URL param) — the relay pairs it with the
1078
- * producer by the routing key.
1079
- */
1080
- resolveExposedPort(port: number): Promise<ExposedPortEndpoint>;
1081
- /** Round-trip the persistable state — `{agentId}` ONLY (resume = re-address). */
1082
- serializeSessionState(): Promise<SelfhostedSessionState>;
1083
- }
1084
- /**
1085
- * The selfhosted SDK-client surface the registry builds. `backendId:"selfhosted"`
1086
- * (the resume-fence field asserted against the descriptor). `create()`/`resume()`
1087
- * return a `SelfhostedSession` bound to `{workspaceId, agentId, controlRpc}`.
1088
- *
1089
- * `create()` and `resume()` are IDENTICAL for selfhosted — there is no box to
1090
- * provision (the machine already exists); both just bind a session to the live
1091
- * subject. `serializeSessionState`/`deserializeSessionState` round-trip
1092
- * `{agentId}` only.
1093
- *
1094
- * The `controlRpc` is constructed LAZILY via an injected factory (defaulting to
1095
- * `NatsControlRpc`); a session built before NATS is configured surfaces
1096
- * `agent_offline` on its first op rather than failing at construction.
1097
- */
1098
- declare class SelfhostedSandboxClient {
1099
- readonly backendId: "selfhosted";
1100
- readonly supportsDefaultOptions = false;
1101
- private readonly workspaceId;
1102
- private readonly relay;
1103
- private readonly controlRpcFactory;
1104
- private readonly defaultAgentId;
1105
- private readonly epoch;
1106
- private readonly timeoutMs;
1107
- private readonly environment;
1108
- private readonly workingDir;
1109
- private controlRpcMemo;
1110
- constructor(opts: {
1111
- workspaceId: string;
1112
- relay: SelfhostedRelayConfig;
1113
- /** Lazily build the ControlRpc (defaults to NatsControlRpc in the provider). */
1114
- controlRpcFactory: () => ControlRpc;
1115
- /** The agentId a bare create()/resume() (no state) binds to. Optional: the
1116
- * resume path supplies it via deserializeSessionState. */
1117
- agentId?: string;
1118
- epoch?: number;
1119
- timeoutMs?: number;
1120
- /** The run's declared sandbox environment, threaded into every bound session's
1121
- * `state.manifest.environment` so the SDK's per-turn manifest-env delta is
1122
- * empty (validateNoEnvironmentDelta). See SelfhostedSessionDeps.environment.
1123
- * Omitted → `{}` (the negotiation-only path; no turn manifest is applied). */
1124
- environment?: Record<string, string>;
1125
- /** The session working directory threaded into every bound session (the path/
1126
- * cwd base; see SelfhostedSessionDeps.workingDir). Omitted/empty ⇒ the default
1127
- * workspace_root behavior. */
1128
- workingDir?: string;
1129
- });
1130
- private controlRpc;
1131
- private bind;
1132
- /** Bind a session to the live agent subject. There is no box to provision. */
1133
- create(_manifest?: unknown, _options?: unknown): Promise<SelfhostedSession>;
1134
- /** Resume = re-address the subject. Identical to create — no provider state. */
1135
- resume(state: SelfhostedSessionState | Record<string, unknown>, _options?: unknown): Promise<SelfhostedSession>;
1136
- /** Serialize a live session's state → `{agentId}` ONLY. */
1137
- serializeSessionState(state: SelfhostedSessionState | {
1138
- agentId?: string;
1139
- } | unknown): Promise<SelfhostedSessionState>;
1140
- /** Deserialize `{agentId}` from the persisted envelope. */
1141
- deserializeSessionState(state: Record<string, unknown>): Promise<SelfhostedSessionState>;
1142
- /** selfhosted is NOT persistable — there is no owned session state to preserve
1143
- * (the machine is the persistence). The lease never snapshots it. */
1144
- canPersistOwnedSessionState(): Promise<boolean>;
1145
- private requireAgentId;
1146
- }
1147
- /**
1148
- * The dependency shape `buildSelfhostedBackendSession` needs to bind a live
1149
- * selfhosted session to a target machine. A structural superset of the fields the
1150
- * routing resolver (backend-resolver.ts) reads off its deps + pointer, and the
1151
- * fields the WORKER turn's machine-primary establish branch threads in — so a
1152
- * SINGLE build shape is shared by both (never two divergent constructions of the
1153
- * same SelfhostedSandboxClient/resume pair).
1154
- */
1155
- interface SelfhostedSessionBuild {
1156
- /** The workspace the machine's control-plane subject is scoped to. */
1157
- workspaceId: string;
1158
- /** The enrollment id == the agent id `agent.<ws>.<id>.rpc` addresses. */
1159
- agentId: string;
1160
- /** The relay-URL shape for stream endpoints. */
1161
- relay: SelfhostedRelayConfig;
1162
- /** Lazily build the live ControlRpc (the request-scoped NATS connection). */
1163
- controlRpcFactory: () => ControlRpc;
1164
- /** The lease/active epoch the session is fenced under (echoed on every op). */
1165
- epoch: number;
1166
- /** The run's declared sandbox environment → the session manifest.environment
1167
- * (env-parity; see SelfhostedSessionDeps.environment). */
1168
- environment?: Record<string, string>;
1169
- /** The session working directory (the path/cwd base). Null/absent ⇒ workspace_root. */
1170
- workingDir?: string | null;
1171
- /** Override the control-op timeout (tests). */
1172
- timeoutMs?: number;
1173
- }
1174
- /**
1175
- * Build a live selfhosted session bound to a target machine: construct a request-
1176
- * scoped `SelfhostedSandboxClient` (fenced under `epoch`, carrying the run's env +
1177
- * working dir) and `resume()` it (= re-address the live subject — no provider box
1178
- * is created). Returns BOTH the client (the OWNED-sandbox client the turn injects,
1179
- * whose `serializeSessionState` round-trips `{agentId}`) and the live session.
1180
- *
1181
- * Shared by:
1182
- * - the routing resolver (backend-resolver.ts) — a swap target, where only the
1183
- * session is needed; and
1184
- * - the worker turn's machine-primary establish branch — where the client is the
1185
- * owned-sandbox client AND the session is the pinned routing default.
1186
- * Factoring it here keeps the two builds identical (no divergence in the fence
1187
- * epoch, env threading, or working-dir base).
1188
- */
1189
- declare function buildSelfhostedBackendSession(deps: SelfhostedSessionBuild): Promise<{
1190
- client: SelfhostedSandboxClient;
1191
- session: SelfhostedSession;
1192
- }>;
1193
- /**
1194
- * The selfhosted NotFound discriminator — THE load-bearing safety property
1195
- * (dossier §10.2/§19): for selfhosted, `agent-offline` (no responder) is NEVER a
1196
- * provider NotFound. A user's real machine is not recreatable; if the lease saw
1197
- * agent-offline as NotFound it would cold-create a RIVAL box (a Modal box) for
1198
- * the user's machine. So this ALWAYS returns FALSE for selfhosted — there is no
1199
- * "box gone, recreate it" condition. An OS-level file NotFound is an op-level
1200
- * error the fs layer 404s; it is likewise NOT a session-recreate condition.
1201
- *
1202
- * `establishSandboxSessionFromEnvelope` cold-restores ONLY when the per-backend
1203
- * NotFound discriminator returns true; returning false here guarantees the
1204
- * selfhosted path never cold-creates a rival — the op surfaces agent_offline and
1205
- * the caller backs off / retries.
1206
- */
1207
- declare function isSelfhostedProviderNotFoundError(_error: unknown): false;
1208
-
1209
- /**
1210
- * The structural slice of the M2 `@opengeni/db` `EnrollmentRecord` the selfhosted
1211
- * negotiation reads. Defined STRUCTURALLY (not imported from `@opengeni/db`) so
1212
- * the agent-loop-free sandbox leaf does not couple to the DB package's graph —
1213
- * the API/worker pass an `EnrollmentRecord`, which satisfies this shape. The
1214
- * fields: `status` (active gates reachability), `exposure` +
1215
- * `allowScreenControl` (whole-machine + screen-control consent),`hasDisplay`
1216
- * (the display plane), `lastSeenAt` (the reconnecting-window disambiguator).
1217
- */
1218
- interface SelfhostedEnrollment {
1219
- status: string;
1220
- exposure: string;
1221
- allowScreenControl: boolean;
1222
- hasDisplay: boolean;
1223
- lastSeenAt: string | null;
1224
- }
1225
- /** The derived liveness state of a selfhosted machine (the online/offline/
1226
- * reconnecting/consent/display matrix). */
1227
- interface SelfhostedLivenessState {
1228
- /** The dominant machine state. */
1229
- state: "online" | "reconnecting" | "offline";
1230
- /** Whole-machine + screen-control consent acknowledged (gates desktop input). */
1231
- consented: boolean;
1232
- /** A display (real or Xvfb) is present (gates the desktop pixel plane). */
1233
- hasDisplay: boolean;
1234
- }
1235
- /**
1236
- * The window after `lastSeenAt` within which a missed liveness probe is read as a
1237
- * transient BLIP (`reconnecting`) rather than a hard `offline`. Mirrors the
1238
- * resiliency model (§10.6: reconnecting after 1 missed window, offline after
1239
- * ~30s). A probe miss with a lastSeenAt inside this window → reconnecting.
1240
- */
1241
- declare const SELFHOSTED_RECONNECT_WINDOW_MS = 30000;
1242
- /**
1243
- * Derive the selfhosted liveness state from the enrollment row + a liveness probe
1244
- * outcome. The probe is the authoritative "is the agent answering NOW" signal;
1245
- * `lastSeenAt` disambiguates a probe-miss into reconnecting (recent) vs offline
1246
- * (stale / never seen).
1247
- *
1248
- * - no enrollment / revoked → offline (the machine isn't enrolled).
1249
- * - probe responded → online.
1250
- * - probe missed, lastSeenAt recent → reconnecting (a transient blip).
1251
- * - probe missed, lastSeenAt stale → offline.
1252
- */
1253
- declare function selfhostedLiveness(input: {
1254
- enrollment: SelfhostedEnrollment | null;
1255
- /** The ControlRpc Ping outcome: true iff a responder answered. */
1256
- probeResponded: boolean;
1257
- /** Override the clock (tests). */
1258
- now?: Date;
1259
- }): SelfhostedLivenessState;
1260
- interface SelfhostedNegotiationInput {
1261
- sessionId: string;
1262
- os?: SandboxOs;
1263
- leaseEpoch: number;
1264
- /** The M2 enrollment row for the machine (null → never enrolled → offline). */
1265
- enrollment: SelfhostedEnrollment | null;
1266
- /** A live liveness probe — typically `session.ping()`. When a session is
1267
- * provided this is called; otherwise pass `probeResponded` explicitly. */
1268
- session?: Pick<SelfhostedSession, "ping">;
1269
- /** Explicit probe outcome (when no session is given, e.g. a pure read). */
1270
- probeResponded?: boolean;
1271
- /** The deployment desktop/terminal/computer-use policy toggles (threaded
1272
- * through to the base negotiation). */
1273
- desktopEnabled?: boolean;
1274
- terminalEnabled?: boolean;
1275
- computerUseEnabled?: boolean;
1276
- /** Whether the calling principal acknowledged the un-redacted desktop. */
1277
- desktopAcknowledged?: boolean;
1278
- shared?: boolean;
1279
- sharedSessionIds?: string[];
1280
- /** Override the clock (tests). */
1281
- now?: Date;
1282
- }
1283
- /**
1284
- * Negotiate the full `SessionCapabilities` document for a selfhosted machine,
1285
- * with the online/offline/reconnecting/consent_required/display_unavailable cells
1286
- * correctly decided. Async because it issues the liveness probe.
1287
- */
1288
- declare function negotiateSelfhostedCapabilities(input: SelfhostedNegotiationInput): Promise<SessionCapabilities>;
1289
-
1290
- /** A pluggable exec handler — given an ExecRequest, return an ExecResponse (or
1291
- * throw to surface a synthesized error). Defaults to a trivial echo. */
1292
- type MockExecHandler = (req: ExecRequest) => ExecResponse | Promise<ExecResponse>;
1293
- interface MockAgentResponderOptions {
1294
- /** Whether a responder exists at all. When false EVERY request yields an
1295
- * AGENT_OFFLINE error (the "machine is offline" condition) — used to drive the
1296
- * agent_offline capability + the isProviderSandboxNotFoundError test. */
1297
- online?: boolean;
1298
- /** Whether the agent has acknowledged whole-machine / screen-control consent.
1299
- * When false, an op gated on consent yields CONSENT_REQUIRED. Defaults true. */
1300
- consented?: boolean;
1301
- /** Force the agent into a draining posture (every op → DRAINING). */
1302
- draining?: boolean;
1303
- /** Seed files (path → string|Uint8Array) into the virtual filesystem. */
1304
- files?: Record<string, string | Uint8Array>;
1305
- /** A custom exec handler; defaults to an echo of argv. */
1306
- exec?: MockExecHandler;
1307
- /** The hostname the mock reports (so PTY/exec `$HOSTNAME`-style asserts work). */
1308
- hostname?: string;
1309
- }
1310
- /**
1311
- * An in-process `ControlRpc` answering the agent op table against an in-memory
1312
- * virtual filesystem. Drive a `SelfhostedSession` with this to test exec /
1313
- * readFile / writeFile / list / stat round-trips without any NATS.
1314
- */
1315
- declare class MockAgentResponder implements ControlRpc {
1316
- private online;
1317
- private readonly consented;
1318
- private readonly draining;
1319
- private readonly files;
1320
- private readonly execHandler;
1321
- readonly hostname: string;
1322
- /** Every request seen, for assertion (subject + decoded ControlRequest). */
1323
- readonly requests: Array<{
1324
- subject: string;
1325
- req: ControlRequest;
1326
- }>;
1327
- constructor(opts?: MockAgentResponderOptions);
1328
- /** Flip the responder offline mid-test (a deliberate stop / blip). */
1329
- setOnline(online: boolean): void;
1330
- /** Read a file the session wrote (test assertion helper). */
1331
- fileText(path: string): string | undefined;
1332
- request(subject: string, req: ControlRequest, _opts: {
1333
- timeoutMs: number;
1334
- }): Promise<ControlResponse>;
1335
- }
1336
-
1337
- /** The per-session active-sandbox pointer the proxy re-reads on every op. Mirror
1338
- * of `@opengeni/db`'s `ActiveSandboxPointer` (structural, so the leaf does not
1339
- * import the DB package). `activeSandboxId === null` == "use the session's own
1340
- * group sandbox" (the default/backward-compat target). */
1341
- interface ActivePointer {
1342
- activeSandboxId: string | null;
1343
- activeEpoch: number;
1344
- /** The session's working directory — the path/cwd base for a selfhosted backend
1345
- * (threaded into the SelfhostedSession via the resolver). `null`/absent ⇒ the
1346
- * default workspace_root behavior. Optional so the default-pointer fallback
1347
- * (`{ activeSandboxId: null, activeEpoch: 0 }`) the readPointer wiring synthesizes
1348
- * when no row exists needs no extra field. Only the selfhosted branch reads it;
1349
- * the modal/default branches ignore it. */
1350
- workingDir?: string | null;
1351
- }
1352
- /**
1353
- * The structural slice of a backend session the routing proxy forwards to. It is
1354
- * a superset-by-optionality of every backend's surface (Modal's `SandboxSession`
1355
- * AND the `SelfhostedSession`): each method is optional because a heterogeneous
1356
- * target may or may not implement it, and the proxy reflects that at call-time.
1357
- */
1358
- interface RoutableBackendSession {
1359
- state?: unknown;
1360
- exec?(args: unknown): Promise<unknown>;
1361
- execCommand?(args: unknown): Promise<string>;
1362
- writeStdin?(args: unknown): Promise<string>;
1363
- readFile?(args: unknown): Promise<string | Uint8Array>;
1364
- writeFile?(args: unknown): Promise<unknown>;
1365
- createEditor?(runAs?: string): unknown;
1366
- listDir?(args: unknown): Promise<unknown>;
1367
- pathExists?(path: string, runAs?: string): Promise<boolean>;
1368
- viewImage?(args: unknown): Promise<unknown>;
1369
- materializeEntry?(args: unknown): Promise<void>;
1370
- supportsPty?(): boolean;
1371
- resolveExposedPort?(port: number): Promise<ExposedPortEndpoint>;
1372
- serializeSessionState?(): Promise<unknown>;
1373
- desktopInput?(event: unknown): Promise<void>;
1374
- screenshot?(): Promise<{
1375
- png: Uint8Array;
1376
- width: number;
1377
- height: number;
1378
- }>;
1379
- }
1380
- /** The resolved active backend for an epoch: the live session + the sandbox id it
1381
- * belongs to (`null` == the group sandbox) so a fence-retry can detect a move. */
1382
- interface ResolvedActiveBackend {
1383
- session: RoutableBackendSession;
1384
- /** The sandbox id this backend serves (`null` == the session's group sandbox). */
1385
- sandboxId: string | null;
1386
- /** A label for diagnostics ("modal" | "selfhosted" | the sandbox name). */
1387
- kind: string;
1388
- }
1389
- interface RoutingSandboxSessionDeps {
1390
- /**
1391
- * The DEFAULT backend resolved at construction time (the same shape `resolve()`
1392
- * caches as `lastResolved`). This seeds `session.state` BEFORE the first op so a
1393
- * consumer that reads `session.state.manifest` at turn START — the @openai/agents
1394
- * SDK does, before any tool runs — sees the real default backend's state object
1395
- * (and writes to `session.state.manifest = …` land on it by reference), instead
1396
- * of an empty `{}` that crashes serializeManifestEnvironment /
1397
- * validateProvidedSessionManifestUpdate. The default-pointer case
1398
- * (`activeSandboxId === null`) resolves synchronously to this same backend, so
1399
- * seeding it here is byte-identical to what the first `resolve()` would produce.
1400
- */
1401
- defaultResolved?: ResolvedActiveBackend;
1402
- /** Re-read the per-session active pointer. Called on EVERY op (the per-call
1403
- * re-resolve that makes a mid-turn swap visible to the next tool call). */
1404
- readPointer(): Promise<ActivePointer>;
1405
- /**
1406
- * Resolve the active backend session for a pointer. The proxy memoizes the
1407
- * result by `activeEpoch`, so this is called at most once per epoch (per op the
1408
- * pointer is re-read, but the heavy resolve only re-runs when the epoch moved).
1409
- * For `pointer.activeSandboxId === null` this returns the default/group backend
1410
- * (typically the already-established turn box); for a non-null target it builds
1411
- * the target backend (a sibling Modal box or a selfhosted machine session).
1412
- */
1413
- resolveActiveBackend(pointer: ActivePointer): Promise<ResolvedActiveBackend>;
1414
- /** Max fence/stale retries within a single op before surfacing the error.
1415
- * Defaults to 3 — enough to absorb a couple of concurrent swaps, bounded so a
1416
- * swap-storm cannot loop forever. */
1417
- maxFenceRetries?: number;
1418
- /** Optional structured-log sink for swap/fence transitions (diagnostics). */
1419
- onTransition?: (event: RoutingTransitionEvent) => void;
1420
- }
1421
- interface RoutingTransitionEvent {
1422
- type: "resolved" | "fenced-retry" | "epoch-changed";
1423
- fromEpoch: number;
1424
- toEpoch: number;
1425
- sandboxId: string | null;
1426
- kind: string;
1427
- }
1428
- /** Thrown when the active backend does not implement the requested op (a
1429
- * heterogeneous target whose surface lacks the method the caller reached for). */
1430
- declare class RoutingUnsupportedError extends Error {
1431
- readonly name = "RoutingUnsupportedError";
1432
- constructor(op: string, kind: string);
1433
- }
1434
- /**
1435
- * ONE stable session-shaped object the SDK binds to. Every method re-reads the
1436
- * pointer, resolves the active backend (cached by epoch), and dispatches. A
1437
- * stale-epoch fence (the pointer moved mid-op) re-resolves and retries.
1438
- *
1439
- * The proxy implements ALL of the consumed surface so the SDK (which binds method
1440
- * presence ONCE) always sees `exec`/`readFile`/`resolveExposedPort`/… present. If
1441
- * the CURRENTLY-active backend lacks a method, the proxy applies the natural
1442
- * fallback (`exec`→`execCommand`) or throws `RoutingUnsupportedError` — degrade is
1443
- * a value, not a crash.
1444
- *
1445
- * `state` is a STABLE getter so a consumer reading `session.state` (channel-a's
1446
- * `readInstanceId`, the docker-network decoration) gets a coherent snapshot of the
1447
- * currently-active backend without a method call.
1448
- */
1449
- declare class RoutingSandboxSession implements RoutableBackendSession {
1450
- private readonly deps;
1451
- private readonly maxFenceRetries;
1452
- private cachedEpoch;
1453
- private cached;
1454
- private lastResolved;
1455
- desktopInput?: (event: unknown) => Promise<void>;
1456
- screenshot?: () => Promise<{
1457
- png: Uint8Array;
1458
- width: number;
1459
- height: number;
1460
- }>;
1461
- constructor(deps: RoutingSandboxSessionDeps);
1462
- /**
1463
- * A method-free read of the active backend's `state` (best-effort: the last
1464
- * resolved backend, falling back to the default backend resolved at construction
1465
- * so this is non-empty BEFORE the first op). Consumers that read `session.state`
1466
- * (instanceId/decoration) get the active backend's state.
1467
- *
1468
- * CRITICAL: this returns the underlying backend's `state` OBJECT BY REFERENCE
1469
- * (never a fresh `{}` when a backend exists). The @openai/agents SDK both READS
1470
- * `session.state.manifest` and WRITES `session.state.manifest = nextManifest`
1471
- * (providedSessionManifest); returning the live object by reference means those
1472
- * property writes land on the real backend state and persist. Only when NO
1473
- * backend has been resolved yet (no default seeded, no op dispatched) do we
1474
- * return an empty object — and that path no longer occurs in the turn wiring,
1475
- * which always seeds `defaultResolved`.
1476
- */
1477
- get state(): unknown;
1478
- /**
1479
- * Re-read the pointer and resolve the active backend, using the per-epoch cache.
1480
- * The cache is keyed by `activeEpoch`: if the epoch is unchanged we return the
1481
- * cached backend; if it moved (a swap) we re-resolve and update the cache. This
1482
- * is THE per-call re-read that makes a mid-turn swap land on the next op.
1483
- */
1484
- private resolve;
1485
- /**
1486
- * Dispatch an op to the currently-active backend, retrying on a stale-epoch
1487
- * fence. The sequence per attempt:
1488
- * 1. re-read the pointer + resolve the active backend (cached by epoch),
1489
- * 2. run `fn(activeSession)`,
1490
- * 3. on a FENCE error (the pointer moved under us / the backend rejected a
1491
- * stale epoch), INVALIDATE the cache and retry against the re-resolved
1492
- * active sandbox — up to `maxFenceRetries`.
1493
- * A non-fence error propagates immediately (it is a real op failure, not a swap
1494
- * race).
1495
- */
1496
- private dispatch;
1497
- exec(args: unknown): Promise<unknown>;
1498
- execCommand(args: unknown): Promise<string>;
1499
- writeStdin(args: unknown): Promise<string>;
1500
- readFile(args: unknown): Promise<string | Uint8Array>;
1501
- writeFile(args: unknown): Promise<unknown>;
1502
- listDir(args: unknown): Promise<unknown>;
1503
- pathExists(path: string, runAs?: string): Promise<boolean>;
1504
- viewImage(args: unknown): Promise<unknown>;
1505
- materializeEntry(args: unknown): Promise<void>;
1506
- /** PTY support reflects the LAST-resolved backend (a synchronous probe; the SDK
1507
- * reads it to decide if the terminal is interactive). It cannot re-read the
1508
- * pointer (synchronous), so it answers from the last resolve — coherent with
1509
- * the resolve the surrounding op already performed. Defaults false before the
1510
- * first resolve. */
1511
- supportsPty(): boolean;
1512
- /** createEditor is a synchronous factory in the SDK surface; it binds to the
1513
- * last-resolved backend's editor (or the default backend before the first op).
1514
- * Returns undefined when the active backend has no editor (channel-a falls back
1515
- * to its exec-based write path). */
1516
- createEditor(runAs?: string): unknown;
1517
- resolveExposedPort(port: number): Promise<ExposedPortEndpoint>;
1518
- /** Serialize the active backend's session state. Used by the resume-by-id seam
1519
- * to fold the live box onto the lease. Dispatches to the active backend. */
1520
- serializeSessionState(): Promise<unknown>;
1521
- /** Force a resolve (priming the proxy before the first op so `state`/`supportsPty`
1522
- * read a real backend). Optional — every op resolves lazily anyway. */
1523
- prime(): Promise<ResolvedActiveBackend>;
1524
- }
1525
-
1526
- /** The structural slice of a first-class sandbox the resolver reads (mirror of
1527
- * `@opengeni/db`'s `SandboxRecord`; structural so the leaf does not import DB). */
1528
- interface RoutableSandbox {
1529
- id: string;
1530
- kind: "modal" | "selfhosted" | string;
1531
- name: string;
1532
- /** For a selfhosted sandbox this is its enrollment id (== the agent id the
1533
- * control-plane subject `agent.<ws>.<id>.rpc` addresses). Null for modal. */
1534
- enrollmentId: string | null;
1535
- }
1536
- interface ActiveBackendResolverDeps {
1537
- /** The workspace the session belongs to (the control-plane subject scope). */
1538
- workspaceId: string;
1539
- /** The session's own group sandbox session — the DEFAULT target
1540
- * (`activeSandboxId === null`). Already established (lease-owned); the proxy
1541
- * never re-establishes it. */
1542
- defaultBackend: RoutableBackendSession;
1543
- /** A label for the default backend (its backend id: "modal"/"selfhosted"/…). */
1544
- defaultKind: string;
1545
- /** Look up a first-class sandbox by id (the swap target). Returns null when the
1546
- * id is unknown or not in this workspace (the caller 409s the swap). */
1547
- getSandbox(sandboxId: string): Promise<RoutableSandbox | null>;
1548
- /** Build a live `ControlRpc` for the selfhosted control plane (the request-
1549
- * scoped NATS connection). Returns a ControlRpc whose offline/timeout maps to
1550
- * agent_offline/agent_reconnecting (never a NotFound). */
1551
- controlRpcFactory(): ControlRpc;
1552
- /** The relay-URL shape config for selfhosted stream endpoints. */
1553
- relay: SelfhostedRelayConfig;
1554
- /** Establish (resume-by-id) a NON-DEFAULT modal target's box session for a swap.
1555
- * Supplied by the API/worker (a closure over the sibling sandbox's lease). When
1556
- * absent, a modal swap target surfaces as unsupported (the caller validated
1557
- * liveness, so this is the "modal swap not wired in this context" guard). */
1558
- establishModalTarget?: (sandbox: RoutableSandbox) => Promise<RoutableBackendSession>;
1559
- /** Override the selfhosted control-op timeout (tests). */
1560
- selfhostedTimeoutMs?: number;
1561
- /**
1562
- * The run's declared sandbox environment — the SAME `Record<string,string>` the
1563
- * worker turn threads into the agent's TARGET manifest (and into the group box at
1564
- * create). Threaded into a selfhosted swap target's session so its
1565
- * `state.manifest.environment` EQUALS the turn's, making the SDK's per-turn
1566
- * provided-session manifest-env delta empty (validateNoEnvironmentDelta).
1567
- * WITHOUT this a pin-to-vm turn throws "Live sandbox sessions cannot change
1568
- * manifest environment variables". Omitted → `{}` (the test/negotiation path).
1569
- */
1570
- environment?: Record<string, string>;
1571
- /**
1572
- * A pre-established selfhosted session to PIN for the STEADY-STATE machine
1573
- * pointer (the worker turn's machine-primary path, Stage D). When the pointer
1574
- * targets THIS sandbox at THIS epoch, the resolver returns this SAME instance
1575
- * instead of building a fresh `SelfhostedSession`. This is the instance-identity
1576
- * pin: the SDK reads/writes `state.manifest` at turn START via the proxy's `state`
1577
- * getter (which reads the default/last-resolved backend's state) and then reads it
1578
- * per op via this resolver — those MUST land on ONE SelfhostedSession/manifest, or
1579
- * a turn-start manifest write is invisible to the per-op reads (two-instance
1580
- * divergence). A swap AWAY (a different sandbox id, or the same id at a moved epoch)
1581
- * falls through to a fresh build under the new epoch. Omitted for the API/live-swap
1582
- * path (which always builds fresh — it has no pre-established turn session).
1583
- */
1584
- pinnedSelfhosted?: {
1585
- sandboxId: string;
1586
- epoch: number;
1587
- session: RoutableBackendSession;
1588
- };
1589
- }
1590
- /** Thrown when a swap target cannot be resolved (unknown sandbox, or a modal
1591
- * target with no establisher in this context). The caller maps it to a 409. */
1592
- declare class ActiveBackendUnresolvableError extends Error {
1593
- readonly name = "ActiveBackendUnresolvableError";
1594
- constructor(message: string);
1595
- }
1596
- /**
1597
- * Build the `resolveActiveBackend(pointer)` closure for a `RoutingSandboxSession`.
1598
- * The returned closure is re-invoked by the proxy whenever the active_epoch moves
1599
- * (the per-epoch cache miss), so it must be cheap-and-correct for the steady-state
1600
- * (default pointer → the already-established group box) and build a fresh backend
1601
- * for a swap target.
1602
- *
1603
- * - `activeSandboxId === null` → the default group backend (no re-establish).
1604
- * - a selfhosted target → a `SelfhostedSession` bound to the enrollment agentId,
1605
- * fenced under `pointer.activeEpoch` (echoed on every ControlRequest so the
1606
- * agent can reject a stale op with ERROR_CODE_FENCED — the swap-race fence).
1607
- * - a modal target → `establishModalTarget` (the resume-by-id closure), else
1608
- * unresolvable.
1609
- */
1610
- declare function makeActiveBackendResolver(deps: ActiveBackendResolverDeps): (pointer: ActivePointer) => Promise<ResolvedActiveBackend>;
1611
-
1612
- /**
1613
- * Construct the raw provider SandboxClient for the configured backend. Registry-
1614
- * driven (the old flat if/else is gone): the backend's ProviderRegistration owns
1615
- * validateCredentials + build, with per-provider units/field-names. Returns
1616
- * undefined for "none".
1617
- *
1618
- * The desktop stream port (6080) is merged into exposedPorts for every desktop-
1619
- * capable (backend, os) when desktop is enabled AND the provider cannot expose
1620
- * ports on demand (modal/runloop/e2b pre-declare; blaxel resolves on demand).
1621
- * Existing modal/docker/local construction is behavior-preserved.
1622
- */
1623
- declare function createSandboxClient(settings: Settings, environment?: Record<string, string>): unknown;
1624
- /**
1625
- * Construct the raw provider SandboxClient for an EXPLICIT backend, independent
1626
- * of settings.sandboxBackend. This is the resume-by-id builder the per-turn
1627
- * resume path (and the API-direct control plane) call: a lease's box was created
1628
- * on a specific backend (the envelope's backendId / the lease's
1629
- * resume_backend_id), and the client that reattaches to it must be built for
1630
- * THAT backend, not the process's currently-configured default. When the backend
1631
- * equals settings.sandboxBackend this is identical to createSandboxClient
1632
- * (behavior-preserved). Returns undefined for "none".
1633
- */
1634
- declare function createSandboxClientForBackend(backend: SandboxBackend, settings: Settings, environment?: Record<string, string>): unknown;
1635
- /**
1636
- * Extract the sandbox recovery entry from a run state as a plain JSON record,
1637
- * for storage decoupled from the RunState blob (issue #35). Encapsulates the
1638
- * underscore-internal `_sandbox` read in exactly one place.
1639
- */
1640
- declare function sandboxStateEntryFromRunState(state: unknown): Record<string, unknown> | null;
1641
- /**
1642
- * Items-mode counterpart of restoredSandboxSessionState: rebuild the live
1643
- * sandbox session state from a stored entry (as produced by
1644
- * sandboxStateEntryFromRunState) instead of from a RunState blob.
1645
- */
1646
- declare function restoredSandboxSessionStateFromEntry(entry: Record<string, unknown>, client: unknown): Promise<SandboxSessionState | undefined>;
1647
- /**
1648
- * Read the persisted /workspace snapshot archive off a lease envelope's
1649
- * `sessionState` (sandbox-file-persistence). The reaper (persistDrainSnapshot)
1650
- * folds the base64 archive — a Modal native snapshot-ref or a tar archive, the
1651
- * exact bytes `session.persistWorkspace()` returned — at
1652
- * `sessionState.workspaceArchive`. Cold-restore decodes it and replays it via
1653
- * `session.hydrateWorkspace(archive)` on the freshly-created box so /workspace is
1654
- * restored. Returns undefined when the envelope carries no archive (a box that
1655
- * was never drain-persisted, or a non-persistence config that stored none).
1656
- *
1657
- * It is deliberately read SEPARATELY from deserializeSandboxSessionStateEnvelope:
1658
- * the archive does NOT ride serializeSessionState (it originates at reaper time),
1659
- * and the SDK's deserializeSessionState must NOT receive it (it is an opaque
1660
- * runtime-level field, not provider state).
1661
- */
1662
- declare function readWorkspaceArchiveFromEnvelopeSessionState(sessionState: unknown): Uint8Array | undefined;
1663
- /** Decode the Modal snapshot id out of a persisted base64 archive ref, or
1664
- * undefined when the archive is a tar payload (no provider snapshot to GC) or
1665
- * is unparseable. Used only for keep-latest-per-lease snapshot GC. */
1666
- declare function decodeModalSnapshotId(archive: Uint8Array): string | undefined;
1667
- /**
1668
- * Best-effort GC of a SUPERSEDED Modal filesystem/directory snapshot
1669
- * (sandbox-file-persistence). restoreSnapshotFilesystem terminates the previous
1670
- * SANDBOX but never deletes the prior SNAPSHOT image, so snapshots accumulate
1671
- * unbounded across warm/cold cycles. The reaper keeps only the latest per lease:
1672
- * when it writes a NEW archive it passes the PRIOR archive here to delete its
1673
- * image via the live session's Modal client (`session.modal.images.delete(id)` —
1674
- * the same API the SDK uses for directory images). Never throws (GC is a
1675
- * best-effort backstop; a leaked snapshot is a cost issue, not a correctness one).
1676
- * A tar archive (no snapshot id) is a no-op. Returns the deleted snapshot id (or
1677
- * undefined when nothing was deleted) for observability.
1678
- */
1679
- declare function deletePriorPersistedSnapshot(session: unknown, priorArchiveBase64: string | null | undefined): Promise<string | undefined>;
1680
- declare function deserializeSandboxSessionStateEnvelope(client: SandboxClient, envelope: unknown): Promise<SandboxSessionState | undefined>;
1681
- /** A live, externally-owned sandbox session re-established from the group lease
1682
- * envelope. The caller injects `{client, session, sessionState}` NON-OWNED into
1683
- * the run (or drives session.exec/readFile/resolveExposedPort directly) and
1684
- * drops the handle when done — the lease, not this handle, owns the box. */
1685
- type EstablishedSandboxSession = {
1686
- client: unknown;
1687
- session: unknown;
1688
- sessionState: unknown;
1689
- instanceId: string;
1690
- backendId: string;
1691
- };
1692
- /**
1693
- * Per-provider NotFound discriminator. The @openai/agents-extensions
1694
- * `isProviderSandboxNotFoundError` / `assertResumeRecreateAllowed` helpers live
1695
- * under `@openai/agents-extensions/sandbox/shared`, which is NOT an exported
1696
- * subpath (the package `exports` map only exposes `./sandbox/<provider>`), so we
1697
- * re-implement the discrimination here by inspecting the thrown error shape.
1698
- *
1699
- * "Box no longer running" (the box was reaped / idled out / 24h-ceiling) is the
1700
- * ONLY error that licenses a cold-restore via create(). Every other resume
1701
- * failure (transient provider error, auth, network) must propagate so the caller
1702
- * backs off — never spawns a rival box. We err on the side of NOT recreating:
1703
- * an unrecognized error is treated as "not NotFound" (propagate), because a
1704
- * false-positive recreate is the dangerous direction (double-spawn).
1705
- */
1706
- declare function isProviderSandboxNotFoundError(backendId: string, error: unknown): boolean;
1707
- /**
1708
- * Resume the one box by id from its recovery envelope, or cold-restore from the
1709
- * snapshot when the provider reports it gone. The envelope is the lease's
1710
- * box-identity descriptor (the same per-turn `_sandbox` envelope upserted by the
1711
- * turn activity). A null envelope means a cold session that was never warmed →
1712
- * create() directly.
1713
- *
1714
- * - `opts.backendOverride ?? envelope.backendId ?? settings.sandboxBackend`
1715
- * selects the backend; the client is built for THAT backend (resume-by-id is
1716
- * fenced to the original provider).
1717
- * - warm reattach: deserialize the envelope sessionState → client.resume(state)
1718
- * (no lock; R4-safe). On a provider NotFound, cold-restore via create().
1719
- * - cold restore / cold session: client.create() — the ONLY create() site.
1720
- */
1721
- declare function establishSandboxSessionFromEnvelope(settings: Settings, envelope: Record<string, unknown> | null, opts: {
1722
- sessionId: string;
1723
- backendOverride?: SandboxBackend;
1724
- environment?: Record<string, string>;
1725
- }): Promise<EstablishedSandboxSession>;
1726
- /**
1727
- * Fold a freshly-established (or resumed) sandbox session into the persistable
1728
- * `resume_state` envelope the lease stores — the SAME `{ backendId, sessionState }`
1729
- * shape `establishSandboxSessionFromEnvelope` consumes to RESUME BY ID. The
1730
- * API-direct control plane (viewer attach / Channel-A) MUST persist this onto the
1731
- * lease at warm-commit time, or a later op (which reads the lease's resume_state)
1732
- * has nothing to resume from and COLD-CREATES A RIVAL BOX — the box-churn the
1733
- * prove-it surfaced (fs.write then fs.read 404'd on a different box; N Channel-A
1734
- * ops leaked N boxes). Returns null when the client cannot serialize (the caller
1735
- * stores null and the box rides the provider idle-timeout — no rival spawn, just
1736
- * no warm-reattach).
1737
- */
1738
- declare function serializeEstablishedSandboxEnvelope(established: EstablishedSandboxSession): Promise<Record<string, unknown> | null>;
1739
-
1740
- export { type ActiveBackendResolverDeps, ActiveBackendUnresolvableError, type ActivePointer, ChannelAConflictError, type ChannelAEmitter, type ChannelAExecArgs, type ChannelAExecResult, ChannelANotFoundError, type ChannelASession, ChannelAUnsupportedError, ChannelAValidationError, type ControlRpc, DEFAULT_DESKTOP_GEOMETRY, DISPLAY_STACK_TIMEOUT_MS, type DesktopGeometry, DisplayStackError, DisplayStackUnsupportedError, type EnsureDisplayStackOptions, type EnsureDisplayStackResult, type EnsureTerminalServerOptions, type EnsureTerminalServerResult, type EstablishedSandboxSession, type ExposeStreamPortInput, type ExposeStreamPortResult, type ExposedPortEndpoint, type FinalizeRecordingResult, type MintStreamTokenInput, MockAgentResponder, type MockAgentResponderOptions, type MockExecHandler, NatsControlRpc, type NatsRequestConnection, type NegotiationContext, type NumstatEntry, PROVIDER_REGISTRY, type ProviderConstructionContext, type ProviderRegistration, type RecordingCodec, type RecordingContentType, RecordingError, type RecordingProcess, RecordingUnavailableError, type ResolvedActiveBackend, type RoutableBackendSession, type RoutableSandbox, RoutingSandboxSession, type RoutingSandboxSessionDeps, type RoutingTransitionEvent, RoutingUnsupportedError, SELFHOSTED_DEFAULT_TIMEOUT_MS, SELFHOSTED_RECONNECT_WINDOW_MS, SELFHOSTED_RELAY_STREAM_PATH, STREAM_PORT, STREAM_TOKEN_DEFAULT_TTL_SECONDS, SandboxChannelAService, type SandboxChannelAServiceOptions, SandboxConfigError, SandboxProviderUnavailableError, type SelfhostedApplyDiff, SelfhostedControlError, type SelfhostedEditor, type SelfhostedEnrollment, type SelfhostedExecArgs, type SelfhostedExecResult, type SelfhostedImageOutput, type SelfhostedLivenessState, type SelfhostedNegotiationInput, type SelfhostedRelayConfig, SelfhostedSandboxClient, SelfhostedSession, type SelfhostedSessionBuild, type SelfhostedSessionDeps, type SelfhostedSessionState, type SelfhostedUnavailableReason, type StartRecordingInput, StreamPortUnavailableError, TERMINAL_SERVER_TIMEOUT_MS, TerminalServerError, TerminalServerUnsupportedError, agentErrorToControlError, assertDescriptorRegistryInvariants, assertProviderRegistryInvariants, assertSafeRelPath, backendSupportsOs, buildDisplayStackScript, buildSelfhostedBackendSession, buildStreamUrl, buildTerminalServerScript, contentTypeForCodec, createSandboxClient, createSandboxClientForBackend, decodeModalSnapshotId, deletePriorPersistedSnapshot, deleteRecordingArtifacts, deserializeSandboxSessionStateEnvelope, desktopCapableBackend, ensureDisplayStack, ensureTerminalServer, establishSandboxSessionFromEnvelope, exposeStreamPort, extForCodec, isExecSessionLostBanner, isProviderSandboxNotFoundError, isSelfhostedProviderNotFoundError, isWorkspaceEscapeError, makeActiveBackendResolver, mintStreamToken, negotiateCapabilities, negotiateSelfhostedCapabilities, offlineAgentError, offlineControlResponse, parseExecBannerSessionId, parseNumstatZ, parsePorcelainV2, parseUnifiedPatch, readRecordingBytes, readWorkspaceArchiveFromEnvelopeSessionState, recordingStorageKey, restoredSandboxSessionStateFromEntry, sandboxStateEntryFromRunState, selectBackend, selfhostedLiveness, serializeEstablishedSandboxEnvelope, setSelfhostedApplyDiff, startRecording, stopRecording, stripExecBanner, subjectFor, tearDownDisplayStack, tearDownTerminalServer, timeoutAgentError, timeoutControlResponse, verifyStreamToken };
3
+ import '@openai/agents/sandbox';
4
+ export { A as ActiveBackendResolverDeps, a as ActiveBackendUnresolvableError, b as ActivePointer, C as ChannelAConflictError, c as ChannelAEmitter, d as ChannelAExecArgs, e as ChannelAExecResult, f as ChannelANotFoundError, g as ChannelASession, h as ChannelAUnsupportedError, i as ChannelAValidationError, j as ControlRpc, D as DEFAULT_DESKTOP_GEOMETRY, k as DISPLAY_STACK_TIMEOUT_MS, l as DesktopGeometry, m as DisplayStackError, n as DisplayStackUnsupportedError, E as EnsureDisplayStackOptions, o as EnsureDisplayStackResult, p as EnsureTerminalServerOptions, q as EnsureTerminalServerResult, r as EstablishedSandboxSession, s as ExposeStreamPortInput, t as ExposeStreamPortResult, u as ExposedPortEndpoint, F as FinalizeRecordingResult, L as LiveModalSandboxLeaseAttribution, M as MintStreamTokenInput, v as MockAgentResponder, w as MockAgentResponderOptions, x as MockExecHandler, y as ModalOrphanSweepResult, z as ModalSandboxAttribution, N as NatsControlRpc, B as NatsRequestConnection, G as NegotiationContext, H as NumstatEntry, P as PROVIDER_REGISTRY, I as ProviderConstructionContext, J as ProviderRegistration, K as RecordingCodec, O as RecordingContentType, Q as RecordingError, S as RecordingProcess, T as RecordingUnavailableError, U as ResolvedActiveBackend, V as RoutableBackendSession, W as RoutableSandbox, X as RoutingSandboxSession, Y as RoutingSandboxSessionDeps, Z as RoutingTransitionEvent, _ as RoutingUnsupportedError, $ as SELFHOSTED_DEFAULT_TIMEOUT_MS, a0 as SELFHOSTED_RECONNECT_WINDOW_MS, a1 as SELFHOSTED_RELAY_STREAM_PATH, a2 as STREAM_PORT, a3 as STREAM_TOKEN_DEFAULT_TTL_SECONDS, a4 as SandboxChannelAService, a5 as SandboxChannelAServiceOptions, a6 as SandboxConfigError, a7 as SandboxCreatedCallback, a8 as SandboxProviderUnavailableError, a9 as SelfhostedApplyDiff, aa as SelfhostedControlError, ab as SelfhostedEditor, ac as SelfhostedEnrollment, ad as SelfhostedExecArgs, ae as SelfhostedExecResult, af as SelfhostedImageOutput, ag as SelfhostedLivenessState, ah as SelfhostedNegotiationInput, ai as SelfhostedRelayConfig, aj as SelfhostedSandboxClient, ak as SelfhostedSession, al as SelfhostedSessionBuild, am as SelfhostedSessionDeps, an as SelfhostedSessionState, ao as SelfhostedUnavailableReason, ap as StartRecordingInput, aq as StreamPortUnavailableError, ar as TERMINAL_SERVER_TIMEOUT_MS, as as TerminalServerError, at as TerminalServerUnsupportedError, au as agentErrorToControlError, av as assertDescriptorRegistryInvariants, aw as assertProviderRegistryInvariants, ax as assertSafeRelPath, ay as backendSupportsOs, az as buildDisplayStackScript, aA as buildSelfhostedBackendSession, aB as buildStreamUrl, aC as buildTerminalServerScript, aD as contentTypeForCodec, aE as createSandboxClient, aF as createSandboxClientForBackend, aG as decodeModalSnapshotId, aH as deletePriorPersistedSnapshot, aI as deleteRecordingArtifacts, aJ as deserializeSandboxSessionStateEnvelope, aK as desktopCapableBackend, aL as ensureDisplayStack, aM as ensureTerminalServer, aN as establishSandboxSessionFromEnvelope, aO as exposeStreamPort, aP as extForCodec, aQ as isExecSessionLostBanner, aR as isProviderSandboxNotFoundError, aS as isSelfhostedProviderNotFoundError, aT as isWorkspaceEscapeError, aU as makeActiveBackendResolver, aV as mintStreamToken, aW as modalSandboxAttributionEnvironment, aX as modalSandboxAttributionTags, aY as negotiateCapabilities, aZ as negotiateSelfhostedCapabilities, a_ as offlineAgentError, a$ as offlineControlResponse, b0 as parseExecBannerSessionId, b1 as parseNumstatZ, b2 as parsePorcelainV2, b3 as parseUnifiedPatch, b4 as readRecordingBytes, b5 as readWorkspaceArchiveFromEnvelopeSessionState, b6 as recordingStorageKey, b7 as restoredSandboxSessionStateFromEntry, b8 as sandboxStateEntryFromRunState, b9 as selectBackend, ba as selfhostedLiveness, bb as serializeEstablishedSandboxEnvelope, bc as setSelfhostedApplyDiff, bd as startRecording, be as stopRecording, bf as stripExecBanner, bg as subjectFor, bh as sweepModalOrphanSandboxes, bi as tagModalSandbox, bj as tearDownDisplayStack, bk as tearDownTerminalServer, bl as terminateModalSandboxById, bm as timeoutAgentError, bn as timeoutControlResponse, bo as verifyStreamToken } from '../index-CSGkld-v.js';
5
+ import 'modal';
6
+ import '@opengeni/agent-proto';