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