@opengeni/runtime 0.14.1 → 0.14.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/{chunk-4YJ5Z52Z.js → chunk-7E2ZSCZK.js} +6 -6
  2. package/dist/{chunk-4YJ5Z52Z.js.map → chunk-7E2ZSCZK.js.map} +1 -1
  3. package/dist/{chunk-HHWIZHQM.js → chunk-GNEGANUF.js} +942 -300
  4. package/dist/chunk-GNEGANUF.js.map +1 -0
  5. package/dist/codex-tool-search.d.ts +100 -0
  6. package/dist/context-compaction.d.ts +236 -0
  7. package/dist/history-sanitizer.d.ts +320 -0
  8. package/dist/index.d.ts +186 -718
  9. package/dist/index.js +612 -126
  10. package/dist/index.js.map +1 -1
  11. package/dist/{localSources-T3HZ6KY4.js → localSources-BXHBKGF4.js} +2 -2
  12. package/dist/mcp-network.d.ts +32 -35
  13. package/dist/metrics.d.ts +33 -0
  14. package/dist/sandbox/capabilities.d.ts +11 -0
  15. package/dist/sandbox/channel-a.d.ts +226 -0
  16. package/dist/sandbox/display-stack.d.ts +99 -0
  17. package/dist/sandbox/errors.d.ts +16 -0
  18. package/dist/sandbox/exec-banner.d.ts +19 -0
  19. package/dist/sandbox/index.d.ts +190 -6
  20. package/dist/sandbox/index.js +20 -4
  21. package/dist/sandbox/op-correlation.d.ts +24 -0
  22. package/dist/sandbox/provider-errors.d.ts +32 -0
  23. package/dist/sandbox/provider-operation-gate.d.ts +12 -0
  24. package/dist/sandbox/providers/blaxel.d.ts +2 -0
  25. package/dist/sandbox/providers/cloudflare.d.ts +2 -0
  26. package/dist/sandbox/providers/daytona.d.ts +2 -0
  27. package/dist/sandbox/providers/docker.d.ts +2 -0
  28. package/dist/sandbox/providers/e2b.d.ts +2 -0
  29. package/dist/sandbox/providers/index.d.ts +11 -0
  30. package/dist/sandbox/providers/local.d.ts +2 -0
  31. package/dist/sandbox/providers/modal.d.ts +132 -0
  32. package/dist/sandbox/providers/none.d.ts +2 -0
  33. package/dist/sandbox/providers/runloop.d.ts +2 -0
  34. package/dist/sandbox/providers/selfhosted.d.ts +2 -0
  35. package/dist/sandbox/providers/types.d.ts +30 -0
  36. package/dist/sandbox/providers/vercel.d.ts +2 -0
  37. package/dist/sandbox/recording.d.ts +80 -0
  38. package/dist/sandbox/routing/backend-resolver.d.ts +173 -0
  39. package/dist/sandbox/routing/routing-session.d.ts +401 -0
  40. package/dist/sandbox/run-credentials.d.ts +65 -0
  41. package/dist/sandbox/select.d.ts +120 -0
  42. package/dist/sandbox/selfhosted/capabilities.d.ts +91 -0
  43. package/dist/sandbox/selfhosted/control-rpc.d.ts +175 -0
  44. package/dist/sandbox/selfhosted/fault-rendering.d.ts +40 -0
  45. package/dist/sandbox/selfhosted/op-observer.d.ts +40 -0
  46. package/dist/sandbox/selfhosted/op-stream.d.ts +104 -0
  47. package/dist/sandbox/selfhosted/op-testing.d.ts +108 -0
  48. package/dist/sandbox/selfhosted/op-transport.d.ts +60 -0
  49. package/dist/sandbox/selfhosted/retry-policy.d.ts +106 -0
  50. package/dist/sandbox/selfhosted/session.d.ts +552 -0
  51. package/dist/sandbox/selfhosted/testing.d.ts +48 -0
  52. package/dist/sandbox/stream-port.d.ts +80 -0
  53. package/dist/sandbox/stream-token.d.ts +44 -0
  54. package/dist/sandbox/terminal-server.d.ts +67 -0
  55. package/dist/sandbox/toolspace-token.d.ts +23 -0
  56. package/dist/sandbox/turn-tool-cancellation.d.ts +72 -0
  57. package/dist/sandbox/workspace-archive.d.ts +39 -0
  58. package/dist/sandbox-computer.d.ts +227 -0
  59. package/dist/screenshot-error-card.d.ts +1 -0
  60. package/dist/skill-library.d.ts +10 -12
  61. package/dist/usage-telemetry.d.ts +45 -0
  62. package/dist/workspace-skills.d.ts +31 -0
  63. package/package.json +7 -7
  64. package/src/context-compaction.ts +600 -6
  65. package/src/history-sanitizer.ts +10 -92
  66. package/src/index.ts +301 -62
  67. package/src/sandbox/channel-a.ts +267 -81
  68. package/src/sandbox/index.ts +56 -94
  69. package/src/sandbox/provider-operation-gate.ts +110 -0
  70. package/src/sandbox/providers/docker.ts +13 -1
  71. package/src/sandbox/providers/modal.ts +433 -3
  72. package/src/sandbox/routing/routing-session.ts +84 -8
  73. package/src/sandbox/selfhosted/session.ts +7 -6
  74. package/src/sandbox/turn-tool-cancellation.ts +47 -7
  75. package/src/sandbox/workspace-archive.ts +176 -73
  76. package/src/sandbox-computer.ts +25 -75
  77. package/dist/chunk-HHWIZHQM.js.map +0 -1
  78. package/dist/index-BraazrAO.d.ts +0 -2931
  79. /package/dist/{localSources-T3HZ6KY4.js.map → localSources-BXHBKGF4.js.map} +0 -0
@@ -0,0 +1,80 @@
1
+ import { DESKTOP_STREAM_PORT } from "@opengeni/contracts";
2
+ export { DESKTOP_STREAM_PORT };
3
+ export type RecordingCodec = "h264-mp4" | "vp9-webm";
4
+ export type RecordingContentType = "video/mp4" | "video/webm";
5
+ export declare function contentTypeForCodec(codec: RecordingCodec): RecordingContentType;
6
+ export declare function extForCodec(codec: RecordingCodec): string;
7
+ /** No exec/execCommand on the session — the box cannot run ffmpeg. */
8
+ export declare class RecordingUnavailableError extends Error {
9
+ constructor(message: string);
10
+ }
11
+ /** ffmpeg failed, the file is missing, or the artifact is over its size limit. */
12
+ export declare class RecordingError extends Error {
13
+ readonly reason: "ffmpeg-error" | "box-death" | "max-bytes-exceeded" | "display-unavailable";
14
+ constructor(message: string, reason: "ffmpeg-error" | "box-death" | "max-bytes-exceeded" | "display-unavailable");
15
+ }
16
+ export type StartRecordingInput = {
17
+ recordingId: string;
18
+ codec?: RecordingCodec;
19
+ framerate?: number;
20
+ maxSeconds?: number;
21
+ dimensions?: [number, number];
22
+ display?: string;
23
+ runAs?: string;
24
+ tmpDir?: string;
25
+ };
26
+ export type RecordingProcess = {
27
+ recordingId: string;
28
+ codec: RecordingCodec;
29
+ boxPath: string;
30
+ pidFile: string;
31
+ dimensions: [number, number];
32
+ framerate: number;
33
+ /** epoch-ms when ffmpeg was launched (for duration computation, F14). */
34
+ startedAt: number;
35
+ display: string;
36
+ runAs?: string;
37
+ };
38
+ /**
39
+ * Launch ffmpeg x11grab on :0 → an mp4/webm file on the box. Backgrounded with
40
+ * `nohup … & echo $!` so the launch returns immediately (F12 — the exec does not
41
+ * block on the recording). A hard `-t <maxSeconds>` ceiling bounds a runaway file
42
+ * across a multi-day turn. Returns the handle the caller carries to stop+finalize.
43
+ */
44
+ export declare function startRecording(session: unknown, input: StartRecordingInput): Promise<RecordingProcess>;
45
+ /**
46
+ * SIGINT ffmpeg (so it writes a clean moov atom / webm trailer) and wait for the
47
+ * pid to exit. Bounded well under the yield window (F3). Idempotent: a missing
48
+ * pid file is a no-op.
49
+ */
50
+ export declare function stopRecording(session: unknown, proc: RecordingProcess): Promise<void>;
51
+ export type RecordingArtifactMetadata = {
52
+ contentType: RecordingContentType;
53
+ sizeBytes: number;
54
+ durationSeconds: number;
55
+ };
56
+ /**
57
+ * Inspect and size-gate the finalized recording while it is still on the box.
58
+ * No recording bytes cross into the worker process.
59
+ */
60
+ export declare function inspectRecordingArtifact(session: unknown, proc: RecordingProcess, maxBytes?: number): Promise<RecordingArtifactMetadata>;
61
+ /**
62
+ * Upload the finalized artifact directly from the sandbox to a scoped object-
63
+ * storage URL. The worker carries only the short URL/headers and metadata; it
64
+ * never materializes a base64 string or Uint8Array for a recording that may be
65
+ * hundreds of megabytes. curl's success marker makes an exec transport that
66
+ * returns non-zero command output instead of throwing still fail closed.
67
+ */
68
+ export declare function uploadRecordingArtifact(session: unknown, proc: RecordingProcess, input: {
69
+ url: string;
70
+ requiredHeaders: Record<string, string>;
71
+ maxBytes?: number;
72
+ maxSeconds?: number;
73
+ }): Promise<RecordingArtifactMetadata>;
74
+ /**
75
+ * Delete the box artifacts. F9: call this ONLY after the storage PUT confirmed
76
+ * and the `available` row committed — never before. Best-effort; never throws.
77
+ */
78
+ export declare function deleteRecordingArtifacts(session: unknown, proc: RecordingProcess): Promise<void>;
79
+ /** The storage object key for a recording artifact (parallels the file-asset layout). */
80
+ export declare function recordingStorageKey(workspaceId: string, sessionId: string, recordingId: string, codec: RecordingCodec): string;
@@ -0,0 +1,173 @@
1
+ import { type SelfhostedRelayConfig } from "../selfhosted/session";
2
+ import type { SelfhostedOpObserver } from "../selfhosted/op-observer";
3
+ import type { ControlRpc } from "../selfhosted/control-rpc";
4
+ import type { ActivePointer, RoutableBackendSession, ResolvedActiveBackend } from "./routing-session";
5
+ /** The structural slice of a first-class sandbox the resolver reads (mirror of
6
+ * `@opengeni/db`'s `SandboxRecord`; structural so the leaf does not import DB). */
7
+ export interface RoutableSandbox {
8
+ id: string;
9
+ kind: "modal" | "selfhosted" | string;
10
+ name: string;
11
+ /** For a selfhosted sandbox this is its enrollment id (== the agent id the
12
+ * control-plane subject `agent.<ws>.<id>.rpc` addresses). Null for modal. */
13
+ enrollmentId: string | null;
14
+ }
15
+ export interface ActiveBackendResolverDeps {
16
+ /** The workspace the session belongs to (the control-plane subject scope). */
17
+ workspaceId: string;
18
+ /** The session's own group sandbox session — the DEFAULT target
19
+ * (`activeSandboxId === null`). Already established (lease-owned); the proxy
20
+ * never re-establishes it. */
21
+ defaultBackend: RoutableBackendSession;
22
+ /** A label for the default backend (its backend id: "modal"/"selfhosted"/…). */
23
+ defaultKind: string;
24
+ /**
25
+ * Re-resolve the session's HOME backend after the route cache is fenced.
26
+ *
27
+ * The normal path returns `defaultBackend` (the turn's already-established
28
+ * handle), but an API-side same-target repair can rematerialize that lease
29
+ * while a worker turn is still alive. Such a repair advances the route epoch
30
+ * without changing the null pointer; callers that can resume the current
31
+ * lease-owned identity may provide this callback so the next epoch does not
32
+ * keep dispatching to the dead pre-repair object. This callback must never
33
+ * create a provider or replay an operation.
34
+ */
35
+ resolveDefaultBackend?: (pointer: ActivePointer) => Promise<ResolvedActiveBackend>;
36
+ /** Look up a first-class sandbox by id (the swap target). Returns null when the
37
+ * id is unknown or not in this workspace (the caller 409s the swap). */
38
+ getSandbox(sandboxId: string): Promise<RoutableSandbox | null>;
39
+ /** Build a live `ControlRpc` for the selfhosted control plane (the request-
40
+ * scoped NATS connection). Returns a ControlRpc whose offline/timeout maps to
41
+ * agent_offline/agent_reconnecting (never a NotFound). */
42
+ controlRpcFactory(): ControlRpc;
43
+ /** The relay-URL shape config for selfhosted stream endpoints. */
44
+ relay: SelfhostedRelayConfig;
45
+ /** Establish (resume-by-id) a NON-DEFAULT modal target's box session for a swap.
46
+ * Supplied by the API/worker (a closure over the sibling sandbox's lease). When
47
+ * absent, a modal swap target surfaces as unsupported (the caller validated
48
+ * liveness, so this is the "modal swap not wired in this context" guard). */
49
+ establishModalTarget?: (sandbox: RoutableSandbox) => Promise<RoutableBackendSession>;
50
+ /** The selfhosted CONTROL-op timeout (ping/fs/desktop/pty) for a swap/pin target.
51
+ * Absent ⇒ the session's 30s default. */
52
+ selfhostedTimeoutMs?: number;
53
+ /** The selfhosted EXEC process deadline for a swap/pin target, distinct from the
54
+ * control timeout (a swapped-to machine runs real commands too). Absent ⇒ falls
55
+ * back to the control timeout, as in the session leaf. */
56
+ selfhostedExecTimeoutMs?: number;
57
+ /** The per-op observer threaded into a selfhosted swap/pin target (out-of-band
58
+ * telemetry — metrics + machine.* events). Absent ⇒ no-op. */
59
+ selfhostedOnOp?: SelfhostedOpObserver;
60
+ /**
61
+ * The run's declared sandbox environment — the SAME `Record<string,string>` the
62
+ * worker turn threads into the agent's TARGET manifest (and into the group box at
63
+ * create). Threaded into a selfhosted swap target's session so its
64
+ * `state.manifest.environment` EQUALS the turn's, making the SDK's per-turn
65
+ * provided-session manifest-env delta empty (validateNoEnvironmentDelta).
66
+ * WITHOUT this a pin-to-vm turn throws "Live sandbox sessions cannot change
67
+ * manifest environment variables". Omitted → `{}` (the test/negotiation path).
68
+ */
69
+ environment?: Record<string, string>;
70
+ /**
71
+ * A pre-established selfhosted session to PIN for the STEADY-STATE machine
72
+ * pointer (the worker turn's machine-primary path, Stage D). When the pointer
73
+ * targets THIS sandbox at THIS epoch, the resolver returns this SAME instance
74
+ * instead of building a fresh `SelfhostedSession`. This is the instance-identity
75
+ * pin: the SDK reads/writes `state.manifest` at turn START via the proxy's `state`
76
+ * getter (which reads the default/last-resolved backend's state) and then reads it
77
+ * per op via this resolver — those MUST land on ONE SelfhostedSession/manifest, or
78
+ * a turn-start manifest write is invisible to the per-op reads (two-instance
79
+ * divergence). A swap AWAY (a different sandbox id, or the same id at a moved epoch)
80
+ * falls through to a fresh build under the new epoch. Omitted for the API/live-swap
81
+ * path (which always builds fresh — it has no pre-established turn session).
82
+ */
83
+ pinnedSelfhosted?: {
84
+ sandboxId: string;
85
+ epoch: number;
86
+ session: RoutableBackendSession;
87
+ };
88
+ /**
89
+ * Whether `defaultBackend` IS the session's home box — i.e. whether the null pointer
90
+ * (== home) may resolve to it. Defaults to TRUE (omitted): a genuine machine-HOME turn
91
+ * (home IS the machine) and a Modal-home turn established on its group box both pass/omit
92
+ * true, so null resolves to `defaultBackend` as before. Set explicitly FALSE only for a
93
+ * machine-primary turn of a Modal-HOME session (pinned to a machine, group box never
94
+ * established this turn): the null pointer then has nothing to resolve to THIS turn, so
95
+ * rather than silently keep serving the pinned machine as if the detach never happened,
96
+ * the null branch throws a typed `home_unavailable_this_turn` error — the detach's pointer
97
+ * commit stands and takes effect on the NEXT turn, which starts null and establishes the
98
+ * home box normally. Lazily establishing the home box mid-turn on such a clear is a
99
+ * deferred follow-up (issue #341); until then this makes the gap fail typed-and-specific,
100
+ * never silent.
101
+ */
102
+ defaultIsHome?: boolean;
103
+ }
104
+ /** Why a persisted pointer / swap target cannot be turned into a live backend. A
105
+ * typed discriminant so callers (turn-start reconcile, the swap tool, future
106
+ * reports) can distinguish a stale pointer from an unaddressable enrollment from a
107
+ * backend the turn context simply cannot establish — instead of string-matching
108
+ * one opaque message (issue #341: "typed diagnostics that distinguish stale
109
+ * pointer, offline enrollment, unsupported backend context, transient failure").
110
+ * - `stale_pointer` — the pointed-at sandbox row is gone (deleted/orphaned).
111
+ * - `offline_enrollment` — a selfhosted target carries no enrollment/agent id to address.
112
+ * - `unsupported_backend_context` — a target no turn routing context can establish
113
+ * (a non-group Modal sibling; an unknown kind).
114
+ * - `transient_establishment` — a momentary establish failure worth a retry (reserved;
115
+ * control-plane timeouts surface as their own typed error).
116
+ * - `home_unavailable_this_turn` — the pointer was cleared to the session default (home)
117
+ * mid-turn, but this turn started pinned to a machine and
118
+ * never established the home box, so null has nothing to
119
+ * resolve to THIS turn (the detach takes effect next turn). */
120
+ export type BackendUnresolvableCode = "stale_pointer" | "offline_enrollment" | "unsupported_backend_context" | "transient_establishment" | "home_unavailable_this_turn";
121
+ /** Thrown when a swap target cannot be resolved (unknown sandbox, or a modal
122
+ * target with no establisher in this context). The caller maps it to a 409.
123
+ * Carries a typed {@link BackendUnresolvableCode} in addition to the message. */
124
+ export declare class ActiveBackendUnresolvableError extends Error {
125
+ readonly name = "ActiveBackendUnresolvableError";
126
+ readonly code: BackendUnresolvableCode;
127
+ constructor(code: BackendUnresolvableCode, message: string);
128
+ }
129
+ /** The outcome of {@link swapTargetEstablishability}. */
130
+ export type SwapTargetEstablishability = {
131
+ ok: true;
132
+ } | {
133
+ ok: false;
134
+ code: BackendUnresolvableCode;
135
+ reason: string;
136
+ };
137
+ /**
138
+ * THE SINGLE SOURCE OF TRUTH for "can a turn's routing context ESTABLISH this swap
139
+ * target?" — shared by swap/seed ADMISSION (`fleet.resolveTarget`, before the CAS
140
+ * pointer commit) and turn-time ESTABLISHMENT (this resolver + `wrapTurnBoxWithRouting`).
141
+ * Admission MUST reject any target this predicate calls unestablishable, so the
142
+ * pointer is never committed to a backend no turn can resume (issue #341 Shape 1:
143
+ * a Modal-sibling swap was admitted + epoch-bumped, then every following op
144
+ * stranded because no context wires an establisher for it).
145
+ *
146
+ * - the session's OWN group box (`isSessionGroup`) is the null/home target and is
147
+ * always establishable (it IS the default backend);
148
+ * - a `selfhosted` machine target is establishable (`buildSelfhostedBackendSession`);
149
+ * admission's separate liveness probe — not this predicate — gates online-ness;
150
+ * - a NON-group `modal` target is NOT establishable: no turn context wires
151
+ * `establishModalTarget` (cross-group Modal resume-by-id is a future, billing-
152
+ * touching feature — sandbox-routing.ts). Wiring that establisher is the ONE
153
+ * toggle that flips this branch; keep admission and the resolver in lockstep.
154
+ */
155
+ export declare function swapTargetEstablishability(target: {
156
+ kind: string;
157
+ isSessionGroup: boolean;
158
+ }): SwapTargetEstablishability;
159
+ /**
160
+ * Build the `resolveActiveBackend(pointer)` closure for a `RoutingSandboxSession`.
161
+ * The returned closure is re-invoked by the proxy whenever the active_epoch moves
162
+ * (the per-epoch cache miss), so it must be cheap-and-correct for the steady-state
163
+ * (default pointer → the already-established group box) and build a fresh backend
164
+ * for a swap target.
165
+ *
166
+ * - `activeSandboxId === null` → the default group backend (no re-establish).
167
+ * - a selfhosted target → a `SelfhostedSession` bound to the enrollment agentId,
168
+ * fenced under `pointer.activeEpoch` (echoed on every ControlRequest so the
169
+ * agent can reject a stale op with ERROR_CODE_FENCED — the swap-race fence).
170
+ * - a modal target → `establishModalTarget` (the resume-by-id closure), else
171
+ * unresolvable.
172
+ */
173
+ export declare function makeActiveBackendResolver(deps: ActiveBackendResolverDeps): (pointer: ActivePointer) => Promise<ResolvedActiveBackend>;
@@ -0,0 +1,401 @@
1
+ import type { ExposedPortEndpoint } from "../stream-port";
2
+ /** The per-session active-sandbox pointer the proxy re-reads on every op. Mirror
3
+ * of `@opengeni/db`'s `ActiveSandboxPointer` (structural, so the leaf does not
4
+ * import the DB package). `activeSandboxId === null` == "use the session's own
5
+ * group sandbox" (the default/backward-compat target). */
6
+ export interface ActivePointer {
7
+ activeSandboxId: string | null;
8
+ activeEpoch: number;
9
+ /** The session's working directory — the path/cwd base for a selfhosted backend
10
+ * (threaded into the SelfhostedSession via the resolver). `null`/absent ⇒ the
11
+ * default workspace_root behavior. Optional so the default-pointer fallback
12
+ * (`{ activeSandboxId: null, activeEpoch: 0 }`) the readPointer wiring synthesizes
13
+ * when no row exists needs no extra field. Only the selfhosted branch reads it;
14
+ * the modal/default branches ignore it. */
15
+ workingDir?: string | null;
16
+ }
17
+ /**
18
+ * The structural slice of a backend session the routing proxy forwards to. It is
19
+ * a superset-by-optionality of every backend's surface (Modal's `SandboxSession`
20
+ * AND the `SelfhostedSession`): each method is optional because a heterogeneous
21
+ * target may or may not implement it, and the proxy reflects that at call-time.
22
+ */
23
+ export interface RoutableBackendSession {
24
+ state?: unknown;
25
+ exec?(args: unknown): Promise<unknown>;
26
+ execCommand?(args: unknown): Promise<string>;
27
+ writeStdin?(args: unknown): Promise<string>;
28
+ cancelExecCommand?(opId: string): Promise<boolean>;
29
+ readFile?(args: unknown): Promise<string | Uint8Array>;
30
+ writeFile?(args: unknown): Promise<unknown>;
31
+ createEditor?(runAs?: string): unknown;
32
+ listDir?(args: unknown): Promise<unknown>;
33
+ pathExists?(path: string, runAs?: string): Promise<boolean>;
34
+ viewImage?(args: unknown): Promise<unknown>;
35
+ materializeEntry?(args: unknown): Promise<void>;
36
+ supportsPty?(): boolean;
37
+ resolveExposedPort?(port: number): Promise<ExposedPortEndpoint>;
38
+ serializeSessionState?(): Promise<unknown>;
39
+ desktopInput?(event: unknown): Promise<void>;
40
+ screenshot?(): Promise<{
41
+ png: Uint8Array;
42
+ width: number;
43
+ height: number;
44
+ nativeWidth: number;
45
+ nativeHeight: number;
46
+ }>;
47
+ }
48
+ /** The resolved active backend for an epoch: the live session + the sandbox id it
49
+ * belongs to (`null` == the group sandbox) so a fence-retry can detect a move. */
50
+ export interface ResolvedActiveBackend {
51
+ session: RoutableBackendSession;
52
+ /** The sandbox id this backend serves (`null` == the session's group sandbox). */
53
+ sandboxId: string | null;
54
+ /** A label for diagnostics ("modal" | "selfhosted" | the sandbox name). */
55
+ kind: string;
56
+ /** Exact durable home-lease epoch for a persistable provider. Absent for
57
+ * connected-machine and other non-persistable route targets. */
58
+ leaseEpoch?: number;
59
+ /** Exact durable provider identity paired with `leaseEpoch`. This is internal
60
+ * routing metadata only; it is never projected to an agent or public API. */
61
+ providerInstanceId?: string;
62
+ /** Active-pointer epoch observed when this route was resolved. The proxy fills
63
+ * this internally even when a resolver omits it. */
64
+ activeEpoch?: number;
65
+ }
66
+ /** Durable identity assigned to a provider exec that yielded instead of
67
+ * exiting. The UUID is OpenGeni authority; the numeric provider session id is
68
+ * only a locator within the exact copied backend route. */
69
+ export type RoutingRetainedProcess = {
70
+ id: string;
71
+ providerSessionId: number;
72
+ };
73
+ /** A yielded process was durably promoted, but the mutable authority checked
74
+ * after that commit was stale. The provider output must still be rejected;
75
+ * this result carries only the safe process identity needed to drain the exact
76
+ * original backend without replaying the mutation. */
77
+ export type RoutingMutationSettlementResult = {
78
+ status: "retained_process_durable_output_rejected";
79
+ retainedProcess: RoutingRetainedProcess;
80
+ };
81
+ /** Exact copied route identity required to reconstruct an already-durable
82
+ * retained process after the request/runtime object that opened it is gone.
83
+ * The live session is deliberately not caller-supplied: adoption may bind only
84
+ * to this proxy's construction-time default backend, so it cannot resolve or
85
+ * follow the current active pointer. */
86
+ export type RoutingRetainedProcessAdoption = {
87
+ process: RoutingRetainedProcess;
88
+ backend: {
89
+ sandboxId: string | null;
90
+ leaseEpoch?: number;
91
+ providerInstanceId?: string;
92
+ activeEpoch: number;
93
+ };
94
+ };
95
+ export type RoutingRetainedProcessTerminalProof = {
96
+ outcome: "exited";
97
+ exitCode: number;
98
+ reason: "provider_exit_banner";
99
+ } | {
100
+ outcome: "lost";
101
+ exitCode: null;
102
+ reason: "provider_session_lost_banner";
103
+ };
104
+ export interface RoutingSandboxSessionDeps {
105
+ /**
106
+ * The DEFAULT backend resolved at construction time (the same shape `resolve()`
107
+ * caches as `lastResolved`). This seeds `session.state` BEFORE the first op so a
108
+ * consumer that reads `session.state.manifest` at turn START — the @openai/agents
109
+ * SDK does, before any tool runs — sees the real default backend's state object
110
+ * (and writes to `session.state.manifest = …` land on it by reference), instead
111
+ * of an empty `{}` that crashes serializeManifestEnvironment /
112
+ * validateProvidedSessionManifestUpdate. The default-pointer case
113
+ * (`activeSandboxId === null`) resolves synchronously to this same backend, so
114
+ * seeding it here is byte-identical to what the first `resolve()` would produce.
115
+ */
116
+ defaultResolved?: ResolvedActiveBackend;
117
+ /** Re-read the per-session active pointer. Called on EVERY op (the per-call
118
+ * re-resolve that makes a mid-turn swap visible to the next tool call). */
119
+ readPointer(): Promise<ActivePointer>;
120
+ /**
121
+ * Resolve the active backend session for a pointer. The proxy memoizes the
122
+ * result by `activeEpoch`, so this is called at most once per epoch (per op the
123
+ * pointer is re-read, but the heavy resolve only re-runs when the epoch moved).
124
+ * For `pointer.activeSandboxId === null` this returns the default/group backend
125
+ * (typically the already-established turn box); for a non-null target it builds
126
+ * the target backend (a sibling Modal box or a selfhosted machine session).
127
+ */
128
+ resolveActiveBackend(pointer: ActivePointer): Promise<ResolvedActiveBackend>;
129
+ /** Max fence/stale retries within a single op before surfacing the error.
130
+ * Defaults to 3 — enough to absorb a couple of concurrent swaps, bounded so a
131
+ * swap-storm cannot loop forever. */
132
+ maxFenceRetries?: number;
133
+ /** Optional structured-log sink for swap/fence transitions (diagnostics). */
134
+ onTransition?: (event: RoutingTransitionEvent) => void;
135
+ /** Admit a filesystem-writing operation after resolving its exact route but
136
+ * before invoking the provider. A rejection fails closed and is deliberately
137
+ * outside provider fence-retry/error handling, so it can never replay the op
138
+ * against a rival backend. */
139
+ beforeMutation?: (input: {
140
+ op: string;
141
+ backend: ResolvedActiveBackend;
142
+ }) => Promise<unknown>;
143
+ /** Mark a mutation physically settled after its provider promise resolves OR
144
+ * rejects. A resolved result is also revalidated against the same durable
145
+ * route and attempt fence before its output is accepted. Settlement rejection
146
+ * leaves a fail-closed blocker and the mutation must never be replayed. */
147
+ afterMutation?: (input: {
148
+ op: string;
149
+ backend: ResolvedActiveBackend;
150
+ admission: unknown;
151
+ outcome: "resolved" | "rejected";
152
+ /** Provider result for a resolved call. Never present for rejection. */
153
+ result?: unknown;
154
+ /** Stable candidate generated before durable promotion is attempted. It is
155
+ * supplied only when the provider returned a positive yielded-session id. */
156
+ retainedProcess?: RoutingRetainedProcess;
157
+ }) => Promise<void | RoutingMutationSettlementResult>;
158
+ /** Admit one model/user-visible stdin mutation under the already-durable
159
+ * retained process authority. Control polling and helper execs never call it. */
160
+ beforeProcessMutation?: (input: {
161
+ op: string;
162
+ backend: ResolvedActiveBackend;
163
+ process: RoutingRetainedProcess;
164
+ }) => Promise<unknown>;
165
+ /** Physically settle one process-scoped stdin admission. */
166
+ afterProcessMutation?: (input: {
167
+ op: string;
168
+ backend: ResolvedActiveBackend;
169
+ process: RoutingRetainedProcess;
170
+ admission: unknown;
171
+ outcome: "resolved" | "rejected";
172
+ result?: unknown;
173
+ }) => Promise<void>;
174
+ /** Close the durable parent admission/process holder only after exact exit or
175
+ * matching provider-loss proof. Tracking is removed only after this resolves. */
176
+ settleProcess?: (input: {
177
+ backend: ResolvedActiveBackend;
178
+ process: RoutingRetainedProcess;
179
+ proof: RoutingRetainedProcessTerminalProof;
180
+ }) => Promise<void>;
181
+ /** Called only when an operation against the default/home backend throws a
182
+ * non-fence error. Wiring may classify definitive provider disappearance and
183
+ * atomically retire the exact lease epoch. Returning a result makes dispatch
184
+ * throw a typed recovery-required error WITHOUT replaying the operation (the
185
+ * original mutation may have reached the provider). */
186
+ onDefaultBackendError?: (input: {
187
+ op: string;
188
+ error: unknown;
189
+ kind: string;
190
+ backend: ResolvedActiveBackend;
191
+ }) => Promise<DefaultBackendLossResult | null>;
192
+ }
193
+ export type DefaultBackendLossResult = {
194
+ leaseEpoch: number;
195
+ recovery: "pending" | "degraded" | "unrecoverable" | "superseded";
196
+ };
197
+ export interface RoutingTransitionEvent {
198
+ type: "resolved" | "fenced-retry" | "epoch-changed";
199
+ fromEpoch: number;
200
+ toEpoch: number;
201
+ sandboxId: string | null;
202
+ kind: string;
203
+ }
204
+ /** Thrown when the active backend does not implement the requested op (a
205
+ * heterogeneous target whose surface lacks the method the caller reached for). */
206
+ export declare class RoutingUnsupportedError extends Error {
207
+ readonly name = "RoutingUnsupportedError";
208
+ constructor(op: string, kind: string);
209
+ }
210
+ export declare class RoutingBackendRecoveryRequiredError extends Error {
211
+ readonly op: string;
212
+ readonly leaseEpoch: number;
213
+ readonly recovery: DefaultBackendLossResult["recovery"];
214
+ readonly name = "RoutingBackendRecoveryRequiredError";
215
+ readonly retryable: boolean;
216
+ constructor(op: string, leaseEpoch: number, recovery: DefaultBackendLossResult["recovery"]);
217
+ }
218
+ /** A mutating provider call was admitted but could not be settled against the
219
+ * exact route that admitted it. The provider may have applied the effect, so the
220
+ * proxy rejects the output and explicitly forbids an automatic replay. */
221
+ export declare class RoutingMutationOutcomeUnknownError extends Error {
222
+ readonly op: string;
223
+ readonly name = "RoutingMutationOutcomeUnknownError";
224
+ readonly retryable = false;
225
+ readonly retainedProcess: RoutingRetainedProcess | null;
226
+ constructor(op: string, message: string, options?: {
227
+ cause?: unknown;
228
+ retainedProcess?: RoutingRetainedProcess;
229
+ });
230
+ }
231
+ /** An explicit process-aware operation named a numeric provider locator that
232
+ * is not retained by this routing session. Callers must never fall back to the
233
+ * current active pointer for such an operation. */
234
+ export declare class RoutingRetainedProcessNotFoundError extends Error {
235
+ readonly providerSessionId: number;
236
+ readonly name = "RoutingRetainedProcessNotFoundError";
237
+ readonly retryable = false;
238
+ constructor(providerSessionId: number);
239
+ }
240
+ /**
241
+ * ONE stable session-shaped object the SDK binds to. Every method re-reads the
242
+ * pointer, resolves the active backend (cached by epoch), and dispatches. A
243
+ * stale-epoch fence (the pointer moved mid-op) re-resolves and retries.
244
+ *
245
+ * The proxy implements ALL of the consumed surface so the SDK (which binds method
246
+ * presence ONCE) always sees `exec`/`readFile`/`resolveExposedPort`/… present. If
247
+ * the CURRENTLY-active backend lacks a method, the proxy applies the natural
248
+ * fallback (`exec`→`execCommand`) or throws `RoutingUnsupportedError` — degrade is
249
+ * a value, not a crash.
250
+ *
251
+ * `state` is a STABLE getter so a consumer reading `session.state` (channel-a's
252
+ * `readInstanceId`, the docker-network decoration) gets a coherent snapshot of the
253
+ * currently-active backend without a method call.
254
+ */
255
+ export declare class RoutingSandboxSession implements RoutableBackendSession {
256
+ private readonly deps;
257
+ private readonly maxFenceRetries;
258
+ private cachedEpoch;
259
+ private cachedSandboxId;
260
+ private cached;
261
+ private lastResolved;
262
+ /** Provider session ids are scoped to one backend instance. Each entry copies
263
+ * that exact resolved route so pointer movement can never redirect stdin,
264
+ * polling, or process-group helpers to another box. */
265
+ private readonly retainedProcesses;
266
+ desktopInput?: (event: unknown) => Promise<void>;
267
+ screenshot?: () => Promise<{
268
+ png: Uint8Array;
269
+ width: number;
270
+ height: number;
271
+ nativeWidth: number;
272
+ nativeHeight: number;
273
+ }>;
274
+ constructor(deps: RoutingSandboxSessionDeps);
275
+ /**
276
+ * A method-free read of the active backend's `state` (best-effort: the last
277
+ * resolved backend, falling back to the default backend resolved at construction
278
+ * so this is non-empty BEFORE the first op). Consumers that read `session.state`
279
+ * (instanceId/decoration) get the active backend's state.
280
+ *
281
+ * CRITICAL: this returns the underlying backend's `state` OBJECT BY REFERENCE
282
+ * (never a fresh `{}` when a backend exists). The @openai/agents SDK both READS
283
+ * `session.state.manifest` and WRITES `session.state.manifest = nextManifest`
284
+ * (providedSessionManifest); returning the live object by reference means those
285
+ * property writes land on the real backend state and persist. Only when NO
286
+ * backend has been resolved yet (no default seeded, no op dispatched) do we
287
+ * return an empty object — and that path no longer occurs in the turn wiring,
288
+ * which always seeds `defaultResolved`.
289
+ */
290
+ get state(): unknown;
291
+ /**
292
+ * Re-read the pointer and resolve the active backend, using the per-epoch cache.
293
+ * The cache is keyed by `activeEpoch`: if the epoch is unchanged we return the
294
+ * cached backend; if it moved (a swap) we re-resolve and update the cache. This
295
+ * is THE per-call re-read that makes a mid-turn swap land on the next op.
296
+ */
297
+ private resolve;
298
+ private invalidate;
299
+ private registerRetainedProcess;
300
+ private retainedProcess;
301
+ /** Reconstruct one process that was already promoted durably by an earlier
302
+ * request. Adoption never reads the active pointer and never repeats parent
303
+ * promotion. It can bind only to the exact construction-time default backend;
304
+ * callers must reject non-default routes until they can independently rebuild
305
+ * that provider session from its copied durable identity. */
306
+ adoptRetainedProcess(input: RoutingRetainedProcessAdoption): void;
307
+ private ensureParentPromotion;
308
+ private confirmDurableRejectedPromotion;
309
+ private settleRetainedProcess;
310
+ private flushPendingProcessMutation;
311
+ private dispatchProcessMutation;
312
+ private dispatchProcessControl;
313
+ /**
314
+ * Dispatch an op to the currently-active backend, retrying on a stale-epoch
315
+ * fence. The sequence per attempt:
316
+ * 1. re-read the pointer + resolve the active backend (cached by epoch),
317
+ * 2. run `fn(activeSession)`,
318
+ * 3. on a FENCE error (the pointer moved under us / the backend rejected a
319
+ * stale epoch), INVALIDATE the cache and retry against the re-resolved
320
+ * active sandbox — up to `maxFenceRetries`.
321
+ * A non-fence error propagates immediately (it is a real op failure, not a swap
322
+ * race).
323
+ */
324
+ private dispatch;
325
+ /**
326
+ * The failure-visibility boundary for the `exec_command` SDK capability tool — the
327
+ * dominant fault surface, and the one whose thrown `SelfhostedControlError` reaches
328
+ * the model wrapped by the SDK's generic tool-error function as
329
+ * "…Please try again. Error: …" — actively wrong for a machine that is offline, a
330
+ * consent that is not granted, or an oversized reply. Since the SDK closure-captures
331
+ * its `errorFunction` (there is no seam to attach one to its internally-built tools),
332
+ * we render the fault into the doctrine's four fields HERE and return it as the
333
+ * tool's string result — legible, in-band, and free of the misleading wrapper.
334
+ * (`apply_patch` already surfaces `error.message` via the SDK's own catch; the
335
+ * skills / `view_image` tools consume their session methods internally, so they are
336
+ * rendered elsewhere or left to their own SDK renderers — not this string boundary.)
337
+ *
338
+ * A FENCE error is re-thrown, NEVER rendered: `dispatch` already retries it against
339
+ * a re-resolved backend, and a fence that escapes retries is a routing condition the
340
+ * turn handles, not a model-facing fault. Any non-selfhosted error (a Modal fault, a
341
+ * `RoutingUnsupportedError`) is re-thrown unchanged.
342
+ */
343
+ private renderSelfhostedFaultOrThrow;
344
+ exec(args: unknown): Promise<unknown>;
345
+ execCommand(args: unknown): Promise<string>;
346
+ writeStdin(args: unknown): Promise<string>;
347
+ /** Whether a positive provider session locator is still pinned to the exact
348
+ * backend that yielded it. This synchronous probe is used only to select the
349
+ * process-aware routing surface; it is not itself durable authority. */
350
+ hasRetainedProcess(providerSessionId: number): boolean;
351
+ /** Return only OpenGeni's durable UUID + provider locator. Backend/session
352
+ * objects remain private so a caller cannot forge route authority from this
353
+ * diagnostic handoff. */
354
+ retainedProcessIdentity(providerSessionId: number): RoutingRetainedProcess | null;
355
+ /** Model/user-visible stdin is a distinct workspace mutation admission under
356
+ * the durable retained-process holder. It never re-reads the active pointer. */
357
+ writeStdinForProcessMutation(args: unknown): Promise<string>;
358
+ /** Cancellation and drain polling are control operations. They stay pinned to
359
+ * the process backend and may prove terminal state, but never advance the
360
+ * workspace generation. */
361
+ writeStdinForProcessControl(args: unknown): Promise<string>;
362
+ /** Run a PID/PGID marker or signal helper on the retained process's exact
363
+ * backend. The helper is control-plane work and therefore has no workspace
364
+ * mutation admission of its own. */
365
+ execCommandForProcessControl(providerSessionId: number, args: unknown): Promise<string>;
366
+ cancelExecCommand(opId: string): Promise<boolean>;
367
+ readFile(args: unknown): Promise<string | Uint8Array>;
368
+ writeFile(args: unknown): Promise<unknown>;
369
+ listDir(args: unknown): Promise<unknown>;
370
+ pathExists(path: string, runAs?: string): Promise<boolean>;
371
+ viewImage(args: unknown): Promise<unknown>;
372
+ materializeEntry(args: unknown): Promise<void>;
373
+ /** PTY support reflects the LAST-resolved backend (a synchronous probe; the SDK
374
+ * reads it to decide if the terminal is interactive). It cannot re-read the
375
+ * pointer (synchronous), so it answers from the last resolve — coherent with
376
+ * the resolve the surrounding op already performed. Defaults false before the
377
+ * first resolve. */
378
+ supportsPty(): boolean;
379
+ /** createEditor is a synchronous factory in the SDK surface. The SDK's filesystem
380
+ * capability calls it ONCE at tool-BIND time — `FilesystemCapability.tools()`,
381
+ * every turn, before any tool runs — and throws "Filesystem sandbox sessions must
382
+ * provide createEditor()" if it returns falsy. When a backend is already resolved
383
+ * (eager/selfhosted routing) we bind to its editor directly, byte-for-byte as
384
+ * before. But under LAZY provisioning the backend is not established yet
385
+ * (defaultResolved is the synthetic unprovisioned session with no editor), so a
386
+ * direct delegate returns undefined and every lazy turn would die at bind. Return a
387
+ * LAZY EDITOR PROXY instead: a non-null editor whose async ops resolve the active
388
+ * backend (establishing the box on first use, via `dispatch`) and delegate to its
389
+ * real editor — mirroring how this proxy defers exec/readFile. Even when an
390
+ * eager editor already exists, returning it directly would bypass per-edit
391
+ * route resolution and mutation-generation admission, so every editor is
392
+ * represented by this dispatching proxy. */
393
+ createEditor(runAs?: string): unknown;
394
+ resolveExposedPort(port: number): Promise<ExposedPortEndpoint>;
395
+ /** Serialize the active backend's session state. Used by the resume-by-id seam
396
+ * to fold the live box onto the lease. Dispatches to the active backend. */
397
+ serializeSessionState(): Promise<unknown>;
398
+ /** Force a resolve (priming the proxy before the first op so `state`/`supportsPty`
399
+ * read a real backend). Optional — every op resolves lazily anyway. */
400
+ prime(): Promise<ResolvedActiveBackend>;
401
+ }