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