@opengeni/runtime 0.2.0

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 (65) hide show
  1. package/dist/chunk-2PO56VAL.js +3478 -0
  2. package/dist/chunk-2PO56VAL.js.map +1 -0
  3. package/dist/index.d.ts +912 -0
  4. package/dist/index.js +3663 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/sandbox/index.d.ts +1738 -0
  7. package/dist/sandbox/index.js +187 -0
  8. package/dist/sandbox/index.js.map +1 -0
  9. package/package.json +49 -0
  10. package/src/bundled_hashicorp_terraform_skills/LICENSE +373 -0
  11. package/src/bundled_hashicorp_terraform_skills/README.md +18 -0
  12. package/src/bundled_hashicorp_terraform_skills/UPSTREAM_GIT_SHA +1 -0
  13. package/src/bundled_hashicorp_terraform_skills/azure-verified-modules/SKILL.md +613 -0
  14. package/src/bundled_hashicorp_terraform_skills/checkov/SKILL.md +43 -0
  15. package/src/bundled_hashicorp_terraform_skills/refactor-module/SKILL.md +538 -0
  16. package/src/bundled_hashicorp_terraform_skills/social-media-marketing/SKILL.md +35 -0
  17. package/src/bundled_hashicorp_terraform_skills/terraform-search-import/SKILL.md +372 -0
  18. package/src/bundled_hashicorp_terraform_skills/terraform-search-import/references/MANUAL-IMPORT.md +113 -0
  19. package/src/bundled_hashicorp_terraform_skills/terraform-search-import/scripts/list_resources.sh +38 -0
  20. package/src/bundled_hashicorp_terraform_skills/terraform-stacks/SKILL.md +480 -0
  21. package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/api-monitoring.md +543 -0
  22. package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/component-blocks.md +476 -0
  23. package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/deployment-blocks.md +391 -0
  24. package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/examples.md +1529 -0
  25. package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/linked-stacks.md +187 -0
  26. package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/troubleshooting.md +671 -0
  27. package/src/bundled_hashicorp_terraform_skills/terraform-style-guide/SKILL.md +353 -0
  28. package/src/bundled_hashicorp_terraform_skills/terraform-test/SKILL.md +451 -0
  29. package/src/bundled_hashicorp_terraform_skills/terraform-test/references/CI_CD.md +80 -0
  30. package/src/bundled_hashicorp_terraform_skills/terraform-test/references/EXAMPLES.md +314 -0
  31. package/src/bundled_hashicorp_terraform_skills/terraform-test/references/MOCK_PROVIDERS.md +171 -0
  32. package/src/codex-tool-search.ts +267 -0
  33. package/src/context-compaction.ts +538 -0
  34. package/src/history-sanitizer.ts +719 -0
  35. package/src/index.ts +3299 -0
  36. package/src/sandbox/capabilities.ts +69 -0
  37. package/src/sandbox/channel-a.ts +1031 -0
  38. package/src/sandbox/display-stack.ts +231 -0
  39. package/src/sandbox/errors.ts +34 -0
  40. package/src/sandbox/index.ts +832 -0
  41. package/src/sandbox/providers/blaxel.ts +35 -0
  42. package/src/sandbox/providers/cloudflare.ts +24 -0
  43. package/src/sandbox/providers/daytona.ts +34 -0
  44. package/src/sandbox/providers/docker.ts +17 -0
  45. package/src/sandbox/providers/e2b.ts +36 -0
  46. package/src/sandbox/providers/index.ts +107 -0
  47. package/src/sandbox/providers/local.ts +13 -0
  48. package/src/sandbox/providers/modal.ts +55 -0
  49. package/src/sandbox/providers/none.ts +13 -0
  50. package/src/sandbox/providers/runloop.ts +32 -0
  51. package/src/sandbox/providers/selfhosted.ts +96 -0
  52. package/src/sandbox/providers/types.ts +38 -0
  53. package/src/sandbox/providers/vercel.ts +29 -0
  54. package/src/sandbox/recording.ts +286 -0
  55. package/src/sandbox/routing/backend-resolver.ts +189 -0
  56. package/src/sandbox/routing/routing-session.ts +455 -0
  57. package/src/sandbox/select.ts +371 -0
  58. package/src/sandbox/selfhosted/capabilities.ts +255 -0
  59. package/src/sandbox/selfhosted/control-rpc.ts +351 -0
  60. package/src/sandbox/selfhosted/session.ts +930 -0
  61. package/src/sandbox/selfhosted/testing.ts +230 -0
  62. package/src/sandbox/stream-port.ts +185 -0
  63. package/src/sandbox/stream-token.ts +90 -0
  64. package/src/sandbox/terminal-server.ts +203 -0
  65. package/src/sandbox-computer.ts +835 -0
@@ -0,0 +1,455 @@
1
+ // `RoutingSandboxSession` — the per-session hot-swap routing proxy (M7).
2
+ //
3
+ // THE load-bearing SDK finding (dossier §10.3 / §20-M7): when a box is injected
4
+ // NON-OWNED into a turn (`ownedSandbox.session`), the agent SDK's sandbox
5
+ // capabilities bind to that ONE session OBJECT ONCE and call ITS methods
6
+ // (`exec`/`execCommand`/`readFile`/`listDir`/`resolveExposedPort`/…) per tool
7
+ // call WITHOUT re-resolving the session. So to make the active sandbox flippable
8
+ // mid-turn we cannot swap the object the SDK holds — we must give the SDK ONE
9
+ // STABLE session-shaped object that, on EACH method call, re-reads the
10
+ // per-session active pointer `(active_sandbox_id, active_epoch)` and DISPATCHES
11
+ // to the CURRENTLY-active backend session (Modal or selfhosted).
12
+ //
13
+ // The contract (dossier §10.3):
14
+ // - ONE stable object implementing the `SandboxSessionLike` structural surface.
15
+ // - On EVERY op, re-read `(activeSandboxId, activeEpoch)` via `readPointer`.
16
+ // - Cache the resolved backend session keyed by `activeEpoch`; when the epoch
17
+ // changes mid-turn (a swap bumped it), re-resolve so the NEXT op hits the new
18
+ // backend. Single active at a time (NOT parallel multi-attach).
19
+ // - An in-flight op fenced by a STALE `active_epoch` (the backend rejects with a
20
+ // fence error, OR the pointer moved under us between read and dispatch)
21
+ // RETRIES against the new active sandbox — reusing the existing fenced-retry
22
+ // role. Bounded retries so a pathological swap-storm can't loop forever.
23
+ //
24
+ // This module is agent-loop-free (it lives in the sandbox leaf). It depends ONLY
25
+ // on injected closures (`readPointer` + `resolveActiveBackend`), so the API
26
+ // (`withChannelA`) and the worker (`resumeBoxForTurn`/the turn) wire it to the
27
+ // real `readActiveSandbox` DAO + a backend resolver without coupling the leaf to
28
+ // `@opengeni/db`.
29
+
30
+ import type { ExposedPortEndpoint } from "../stream-port";
31
+
32
+ /** The per-session active-sandbox pointer the proxy re-reads on every op. Mirror
33
+ * of `@opengeni/db`'s `ActiveSandboxPointer` (structural, so the leaf does not
34
+ * import the DB package). `activeSandboxId === null` == "use the session's own
35
+ * group sandbox" (the default/backward-compat target). */
36
+ export interface ActivePointer {
37
+ activeSandboxId: string | null;
38
+ activeEpoch: number;
39
+ /** The session's working directory — the path/cwd base for a selfhosted backend
40
+ * (threaded into the SelfhostedSession via the resolver). `null`/absent ⇒ the
41
+ * default workspace_root behavior. Optional so the default-pointer fallback
42
+ * (`{ activeSandboxId: null, activeEpoch: 0 }`) the readPointer wiring synthesizes
43
+ * when no row exists needs no extra field. Only the selfhosted branch reads it;
44
+ * the modal/default branches ignore it. */
45
+ workingDir?: string | null;
46
+ }
47
+
48
+ /**
49
+ * The structural slice of a backend session the routing proxy forwards to. It is
50
+ * a superset-by-optionality of every backend's surface (Modal's `SandboxSession`
51
+ * AND the `SelfhostedSession`): each method is optional because a heterogeneous
52
+ * target may or may not implement it, and the proxy reflects that at call-time.
53
+ */
54
+ export interface RoutableBackendSession {
55
+ state?: unknown;
56
+ exec?(args: unknown): Promise<unknown>;
57
+ execCommand?(args: unknown): Promise<string>;
58
+ writeStdin?(args: unknown): Promise<string>;
59
+ readFile?(args: unknown): Promise<string | Uint8Array>;
60
+ writeFile?(args: unknown): Promise<unknown>;
61
+ createEditor?(runAs?: string): unknown;
62
+ listDir?(args: unknown): Promise<unknown>;
63
+ pathExists?(path: string, runAs?: string): Promise<boolean>;
64
+ viewImage?(args: unknown): Promise<unknown>;
65
+ materializeEntry?(args: unknown): Promise<void>;
66
+ supportsPty?(): boolean;
67
+ resolveExposedPort?(port: number): Promise<ExposedPortEndpoint>;
68
+ serializeSessionState?(): Promise<unknown>;
69
+ // The native-desktop control-plane surface (self-hosted / macOS): a backend that
70
+ // drives the desktop NATIVELY (input inject + frame capture) instead of shelling
71
+ // xdotool/scrot over `exec`. Optional like the rest — only a `SelfhostedSession`
72
+ // implements these; a Modal box does not. The computer-use capability duck-types
73
+ // on their PRESENCE (`isNativeDesktopSession`) to pick the native vs exec Computer.
74
+ // `event` is kept `unknown` (mirroring the interface's structural style + avoiding
75
+ // a proto import into the leaf); the SelfhostedSession takes `DesktopInputRequest["event"]`.
76
+ desktopInput?(event: unknown): Promise<void>;
77
+ screenshot?(): Promise<{ png: Uint8Array; width: number; height: number }>;
78
+ }
79
+
80
+ /** The resolved active backend for an epoch: the live session + the sandbox id it
81
+ * belongs to (`null` == the group sandbox) so a fence-retry can detect a move. */
82
+ export interface ResolvedActiveBackend {
83
+ session: RoutableBackendSession;
84
+ /** The sandbox id this backend serves (`null` == the session's group sandbox). */
85
+ sandboxId: string | null;
86
+ /** A label for diagnostics ("modal" | "selfhosted" | the sandbox name). */
87
+ kind: string;
88
+ }
89
+
90
+ export interface RoutingSandboxSessionDeps {
91
+ /**
92
+ * The DEFAULT backend resolved at construction time (the same shape `resolve()`
93
+ * caches as `lastResolved`). This seeds `session.state` BEFORE the first op so a
94
+ * consumer that reads `session.state.manifest` at turn START — the @openai/agents
95
+ * SDK does, before any tool runs — sees the real default backend's state object
96
+ * (and writes to `session.state.manifest = …` land on it by reference), instead
97
+ * of an empty `{}` that crashes serializeManifestEnvironment /
98
+ * validateProvidedSessionManifestUpdate. The default-pointer case
99
+ * (`activeSandboxId === null`) resolves synchronously to this same backend, so
100
+ * seeding it here is byte-identical to what the first `resolve()` would produce.
101
+ */
102
+ defaultResolved?: ResolvedActiveBackend;
103
+ /** Re-read the per-session active pointer. Called on EVERY op (the per-call
104
+ * re-resolve that makes a mid-turn swap visible to the next tool call). */
105
+ readPointer(): Promise<ActivePointer>;
106
+ /**
107
+ * Resolve the active backend session for a pointer. The proxy memoizes the
108
+ * result by `activeEpoch`, so this is called at most once per epoch (per op the
109
+ * pointer is re-read, but the heavy resolve only re-runs when the epoch moved).
110
+ * For `pointer.activeSandboxId === null` this returns the default/group backend
111
+ * (typically the already-established turn box); for a non-null target it builds
112
+ * the target backend (a sibling Modal box or a selfhosted machine session).
113
+ */
114
+ resolveActiveBackend(pointer: ActivePointer): Promise<ResolvedActiveBackend>;
115
+ /** Max fence/stale retries within a single op before surfacing the error.
116
+ * Defaults to 3 — enough to absorb a couple of concurrent swaps, bounded so a
117
+ * swap-storm cannot loop forever. */
118
+ maxFenceRetries?: number;
119
+ /** Optional structured-log sink for swap/fence transitions (diagnostics). */
120
+ onTransition?: (event: RoutingTransitionEvent) => void;
121
+ }
122
+
123
+ export interface RoutingTransitionEvent {
124
+ type: "resolved" | "fenced-retry" | "epoch-changed";
125
+ fromEpoch: number;
126
+ toEpoch: number;
127
+ sandboxId: string | null;
128
+ kind: string;
129
+ }
130
+
131
+ /** Thrown when the active backend does not implement the requested op (a
132
+ * heterogeneous target whose surface lacks the method the caller reached for). */
133
+ export class RoutingUnsupportedError extends Error {
134
+ readonly name = "RoutingUnsupportedError";
135
+ constructor(op: string, kind: string) {
136
+ super(`the active sandbox (${kind}) does not support "${op}"`);
137
+ }
138
+ }
139
+
140
+ /** Recognize a stale-epoch FENCE error from a backend op so the proxy retries
141
+ * against the re-resolved active sandbox (the existing fenced-retry role). A
142
+ * selfhosted `SelfhostedControlError` carries `.fenced`; a generic fence is
143
+ * matched on the message as a fallback. */
144
+ function isFenceError(error: unknown): boolean {
145
+ if (!error || typeof error !== "object") {
146
+ return false;
147
+ }
148
+ if ((error as { fenced?: unknown }).fenced === true) {
149
+ return true;
150
+ }
151
+ const name = typeof (error as { name?: unknown }).name === "string" ? (error as { name: string }).name : "";
152
+ const message = error instanceof Error ? error.message : String((error as { message?: unknown }).message ?? "");
153
+ const haystack = `${name} ${message}`.toLowerCase();
154
+ return haystack.includes("fenced") || haystack.includes("epoch") && haystack.includes("super");
155
+ }
156
+
157
+ /**
158
+ * ONE stable session-shaped object the SDK binds to. Every method re-reads the
159
+ * pointer, resolves the active backend (cached by epoch), and dispatches. A
160
+ * stale-epoch fence (the pointer moved mid-op) re-resolves and retries.
161
+ *
162
+ * The proxy implements ALL of the consumed surface so the SDK (which binds method
163
+ * presence ONCE) always sees `exec`/`readFile`/`resolveExposedPort`/… present. If
164
+ * the CURRENTLY-active backend lacks a method, the proxy applies the natural
165
+ * fallback (`exec`→`execCommand`) or throws `RoutingUnsupportedError` — degrade is
166
+ * a value, not a crash.
167
+ *
168
+ * `state` is a STABLE getter so a consumer reading `session.state` (channel-a's
169
+ * `readInstanceId`, the docker-network decoration) gets a coherent snapshot of the
170
+ * currently-active backend without a method call.
171
+ */
172
+ export class RoutingSandboxSession implements RoutableBackendSession {
173
+ private readonly deps: RoutingSandboxSessionDeps;
174
+ private readonly maxFenceRetries: number;
175
+ // The per-epoch resolved-backend cache. Keyed by activeEpoch: a swap bumps the
176
+ // epoch, invalidating the cache so the NEXT op re-resolves the new backend.
177
+ private cachedEpoch: number | undefined;
178
+ private cached: ResolvedActiveBackend | undefined;
179
+ // The last-resolved backend, exposed via the `state` getter (a method-free read
180
+ // of the active backend's `state`). Updated on every resolve.
181
+ private lastResolved: ResolvedActiveBackend | undefined;
182
+
183
+ // The native-desktop control-plane ops (self-hosted / macOS). Declared as OPTIONAL
184
+ // INSTANCE fields — NOT prototype methods — because their PRESENCE is the selection
185
+ // signal `isNativeDesktopSession` (sandbox-computer.ts) uses to pick the native vs
186
+ // exec-shelling Computer. If they were unconditional prototype methods, this proxy
187
+ // would ALWAYS duck-type as native — misclassifying a Modal-fronting proxy (whose
188
+ // real backend has no native surface) and driving CGEvent/screenshot ops at a box
189
+ // that cannot serve them. So the constructor assigns them ONLY when the
190
+ // construction-time default backend actually implements the native surface (below).
191
+ desktopInput?: (event: unknown) => Promise<void>;
192
+ screenshot?: () => Promise<{ png: Uint8Array; width: number; height: number }>;
193
+
194
+ constructor(deps: RoutingSandboxSessionDeps) {
195
+ this.deps = deps;
196
+ this.maxFenceRetries = deps.maxFenceRetries ?? 3;
197
+
198
+ // Conditionally expose the native-desktop surface. Presence = the computer-use
199
+ // native/exec selection signal (isNativeDesktopSession duck-types on
200
+ // desktopInput+screenshot being functions); unconditional presence would
201
+ // misclassify Modal-fronting proxies as native. So we mint these per-INSTANCE
202
+ // arrow properties ONLY when the default backend resolved at construction is
203
+ // itself native-capable — the machine-primary (selfhosted) case. Each dispatches
204
+ // to the ACTIVE backend at call-time; if a mid-turn swap lands on a backend that
205
+ // lacks the op (a cross-kind swap to a Modal box), dispatch throws
206
+ // RoutingUnsupportedError — a legible tool failure, never a silent Linux-tool
207
+ // shell onto a Mac.
208
+ const def = deps.defaultResolved?.session;
209
+ if (typeof def?.desktopInput === "function" && typeof def?.screenshot === "function") {
210
+ this.desktopInput = (event: unknown) =>
211
+ this.dispatch("desktopInput", async (s) => {
212
+ if (!s.desktopInput) {
213
+ throw new RoutingUnsupportedError("desktopInput", this.cached?.kind ?? "unknown");
214
+ }
215
+ return s.desktopInput(event);
216
+ });
217
+ this.screenshot = () =>
218
+ this.dispatch("screenshot", async (s) => {
219
+ if (!s.screenshot) {
220
+ throw new RoutingUnsupportedError("screenshot", this.cached?.kind ?? "unknown");
221
+ }
222
+ return s.screenshot();
223
+ });
224
+ }
225
+ }
226
+
227
+ /**
228
+ * A method-free read of the active backend's `state` (best-effort: the last
229
+ * resolved backend, falling back to the default backend resolved at construction
230
+ * so this is non-empty BEFORE the first op). Consumers that read `session.state`
231
+ * (instanceId/decoration) get the active backend's state.
232
+ *
233
+ * CRITICAL: this returns the underlying backend's `state` OBJECT BY REFERENCE
234
+ * (never a fresh `{}` when a backend exists). The @openai/agents SDK both READS
235
+ * `session.state.manifest` and WRITES `session.state.manifest = nextManifest`
236
+ * (providedSessionManifest); returning the live object by reference means those
237
+ * property writes land on the real backend state and persist. Only when NO
238
+ * backend has been resolved yet (no default seeded, no op dispatched) do we
239
+ * return an empty object — and that path no longer occurs in the turn wiring,
240
+ * which always seeds `defaultResolved`.
241
+ */
242
+ get state(): unknown {
243
+ const backendState = (this.lastResolved ?? this.deps.defaultResolved)?.session.state;
244
+ return backendState ?? {};
245
+ }
246
+
247
+ /**
248
+ * Re-read the pointer and resolve the active backend, using the per-epoch cache.
249
+ * The cache is keyed by `activeEpoch`: if the epoch is unchanged we return the
250
+ * cached backend; if it moved (a swap) we re-resolve and update the cache. This
251
+ * is THE per-call re-read that makes a mid-turn swap land on the next op.
252
+ */
253
+ private async resolve(): Promise<ResolvedActiveBackend> {
254
+ const pointer = await this.deps.readPointer();
255
+ if (this.cachedEpoch === pointer.activeEpoch && this.cached) {
256
+ return this.cached;
257
+ }
258
+ const fromEpoch = this.cachedEpoch ?? pointer.activeEpoch;
259
+ const resolved = await this.deps.resolveActiveBackend(pointer);
260
+ this.cachedEpoch = pointer.activeEpoch;
261
+ this.cached = resolved;
262
+ this.lastResolved = resolved;
263
+ this.deps.onTransition?.({
264
+ type: this.cachedEpoch !== undefined && fromEpoch !== pointer.activeEpoch ? "epoch-changed" : "resolved",
265
+ fromEpoch,
266
+ toEpoch: pointer.activeEpoch,
267
+ sandboxId: resolved.sandboxId,
268
+ kind: resolved.kind,
269
+ });
270
+ return resolved;
271
+ }
272
+
273
+ /**
274
+ * Dispatch an op to the currently-active backend, retrying on a stale-epoch
275
+ * fence. The sequence per attempt:
276
+ * 1. re-read the pointer + resolve the active backend (cached by epoch),
277
+ * 2. run `fn(activeSession)`,
278
+ * 3. on a FENCE error (the pointer moved under us / the backend rejected a
279
+ * stale epoch), INVALIDATE the cache and retry against the re-resolved
280
+ * active sandbox — up to `maxFenceRetries`.
281
+ * A non-fence error propagates immediately (it is a real op failure, not a swap
282
+ * race).
283
+ */
284
+ private async dispatch<T>(op: string, fn: (session: RoutableBackendSession) => Promise<T>): Promise<T> {
285
+ let attempt = 0;
286
+ let lastError: unknown;
287
+ while (attempt <= this.maxFenceRetries) {
288
+ const backend = await this.resolve();
289
+ try {
290
+ return await fn(backend.session);
291
+ } catch (error) {
292
+ if (!isFenceError(error)) {
293
+ throw error;
294
+ }
295
+ // Stale-epoch fence: the active pointer moved mid-op. Drop the cache so
296
+ // the next resolve re-reads the NEW pointer and the op lands on the new
297
+ // active sandbox (the fenced-retry role). Bounded by maxFenceRetries.
298
+ lastError = error;
299
+ this.cachedEpoch = undefined;
300
+ this.cached = undefined;
301
+ this.deps.onTransition?.({
302
+ type: "fenced-retry",
303
+ fromEpoch: backend.sandboxId === null ? 0 : 0,
304
+ toEpoch: 0,
305
+ sandboxId: backend.sandboxId,
306
+ kind: backend.kind,
307
+ });
308
+ attempt += 1;
309
+ }
310
+ }
311
+ // Exhausted retries against a relentless swap-storm: surface the fence so the
312
+ // caller (turn) backs off — never loop forever.
313
+ throw lastError ?? new Error(`routing op "${op}" exhausted fence retries`);
314
+ }
315
+
316
+ // ── The forwarded structural surface ──────────────────────────────────────
317
+ // Every method is PRESENT on the proxy (the SDK binds presence once) and
318
+ // dispatches to the active backend at call-time. A missing backend method
319
+ // degrades via the natural fallback or RoutingUnsupportedError.
320
+
321
+ async exec(args: unknown): Promise<unknown> {
322
+ return this.dispatch("exec", async (s) => {
323
+ if (s.exec) {
324
+ return s.exec(args);
325
+ }
326
+ // Some backends (selfhosted) only expose exec; others only execCommand.
327
+ if (s.execCommand) {
328
+ return s.execCommand(args);
329
+ }
330
+ throw new RoutingUnsupportedError("exec", this.cached?.kind ?? "unknown");
331
+ });
332
+ }
333
+
334
+ async execCommand(args: unknown): Promise<string> {
335
+ return this.dispatch("execCommand", async (s) => {
336
+ if (s.execCommand) {
337
+ return s.execCommand(args);
338
+ }
339
+ if (s.exec) {
340
+ const r = (await s.exec(args)) as { stdout?: string; output?: string };
341
+ return r.stdout ?? r.output ?? "";
342
+ }
343
+ throw new RoutingUnsupportedError("execCommand", this.cached?.kind ?? "unknown");
344
+ });
345
+ }
346
+
347
+ async writeStdin(args: unknown): Promise<string> {
348
+ return this.dispatch("writeStdin", async (s) => {
349
+ if (!s.writeStdin) {
350
+ throw new RoutingUnsupportedError("writeStdin", this.cached?.kind ?? "unknown");
351
+ }
352
+ return s.writeStdin(args);
353
+ });
354
+ }
355
+
356
+ async readFile(args: unknown): Promise<string | Uint8Array> {
357
+ return this.dispatch("readFile", async (s) => {
358
+ if (!s.readFile) {
359
+ throw new RoutingUnsupportedError("readFile", this.cached?.kind ?? "unknown");
360
+ }
361
+ return s.readFile(args);
362
+ });
363
+ }
364
+
365
+ async writeFile(args: unknown): Promise<unknown> {
366
+ return this.dispatch("writeFile", async (s) => {
367
+ if (!s.writeFile) {
368
+ throw new RoutingUnsupportedError("writeFile", this.cached?.kind ?? "unknown");
369
+ }
370
+ return s.writeFile(args);
371
+ });
372
+ }
373
+
374
+ async listDir(args: unknown): Promise<unknown> {
375
+ return this.dispatch("listDir", async (s) => {
376
+ if (!s.listDir) {
377
+ throw new RoutingUnsupportedError("listDir", this.cached?.kind ?? "unknown");
378
+ }
379
+ return s.listDir(args);
380
+ });
381
+ }
382
+
383
+ async pathExists(path: string, runAs?: string): Promise<boolean> {
384
+ return this.dispatch("pathExists", async (s) => {
385
+ if (!s.pathExists) {
386
+ throw new RoutingUnsupportedError("pathExists", this.cached?.kind ?? "unknown");
387
+ }
388
+ return s.pathExists(path, runAs);
389
+ });
390
+ }
391
+
392
+ async viewImage(args: unknown): Promise<unknown> {
393
+ return this.dispatch("viewImage", async (s) => {
394
+ if (!s.viewImage) {
395
+ throw new RoutingUnsupportedError("viewImage", this.cached?.kind ?? "unknown");
396
+ }
397
+ return s.viewImage(args);
398
+ });
399
+ }
400
+
401
+ async materializeEntry(args: unknown): Promise<void> {
402
+ return this.dispatch("materializeEntry", async (s) => {
403
+ if (!s.materializeEntry) {
404
+ throw new RoutingUnsupportedError("materializeEntry", this.cached?.kind ?? "unknown");
405
+ }
406
+ return s.materializeEntry(args);
407
+ });
408
+ }
409
+
410
+ /** PTY support reflects the LAST-resolved backend (a synchronous probe; the SDK
411
+ * reads it to decide if the terminal is interactive). It cannot re-read the
412
+ * pointer (synchronous), so it answers from the last resolve — coherent with
413
+ * the resolve the surrounding op already performed. Defaults false before the
414
+ * first resolve. */
415
+ supportsPty(): boolean {
416
+ const s = (this.lastResolved ?? this.deps.defaultResolved)?.session;
417
+ return Boolean(s?.supportsPty?.());
418
+ }
419
+
420
+ /** createEditor is a synchronous factory in the SDK surface; it binds to the
421
+ * last-resolved backend's editor (or the default backend before the first op).
422
+ * Returns undefined when the active backend has no editor (channel-a falls back
423
+ * to its exec-based write path). */
424
+ createEditor(runAs?: string): unknown {
425
+ return (this.lastResolved ?? this.deps.defaultResolved)?.session.createEditor?.(runAs);
426
+ }
427
+
428
+ async resolveExposedPort(port: number): Promise<ExposedPortEndpoint> {
429
+ return this.dispatch("resolveExposedPort", async (s) => {
430
+ if (!s.resolveExposedPort) {
431
+ throw new RoutingUnsupportedError("resolveExposedPort", this.cached?.kind ?? "unknown");
432
+ }
433
+ return s.resolveExposedPort(port);
434
+ });
435
+ }
436
+
437
+ /** Serialize the active backend's session state. Used by the resume-by-id seam
438
+ * to fold the live box onto the lease. Dispatches to the active backend. */
439
+ async serializeSessionState(): Promise<unknown> {
440
+ return this.dispatch("serializeSessionState", async (s) => {
441
+ if (!s.serializeSessionState) {
442
+ // No-op for a backend with no serializable state (selfhosted state is
443
+ // re-addressed, not snapshotted) — surface undefined, not an error.
444
+ return undefined;
445
+ }
446
+ return s.serializeSessionState();
447
+ });
448
+ }
449
+
450
+ /** Force a resolve (priming the proxy before the first op so `state`/`supportsPty`
451
+ * read a real backend). Optional — every op resolves lazily anyway. */
452
+ async prime(): Promise<ResolvedActiveBackend> {
453
+ return this.resolve();
454
+ }
455
+ }