@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,3478 @@
1
+ // src/sandbox/index.ts
2
+ import { collectSandboxEnvironment, parseExposedPorts } from "@opengeni/config";
3
+ import { DESKTOP_STREAM_PORT as DESKTOP_STREAM_PORT7, TERMINAL_STREAM_PORT as TERMINAL_STREAM_PORT2 } from "@opengeni/contracts";
4
+
5
+ // src/sandbox/providers/index.ts
6
+ import { SandboxBackend as SandboxBackend2 } from "@opengeni/contracts";
7
+
8
+ // src/sandbox/capabilities.ts
9
+ import {
10
+ CAPABILITY_DESCRIPTORS,
11
+ DESKTOP_STREAM_PORT,
12
+ SandboxBackend
13
+ } from "@opengeni/contracts";
14
+ function assertDescriptorRegistryInvariants() {
15
+ for (const backend of SandboxBackend.options) {
16
+ const descriptor = CAPABILITY_DESCRIPTORS[backend];
17
+ if (!descriptor) {
18
+ throw new Error(`No CapabilityDescriptor for backend "${backend}"`);
19
+ }
20
+ if (descriptor.backend !== backend) {
21
+ throw new Error(`Descriptor.backend mismatch for "${backend}" (got "${descriptor.backend}")`);
22
+ }
23
+ if (descriptor.capabilities.DesktopStream.available && descriptor.portExposure.kind === "none") {
24
+ throw new Error(`"${backend}" claims DesktopStream but portExposure.kind=none`);
25
+ }
26
+ if (descriptor.capabilities.DesktopStream.available && descriptor.capabilities.DesktopStream.transport === null) {
27
+ throw new Error(`"${backend}" claims DesktopStream but transport is null`);
28
+ }
29
+ if (descriptor.capabilities.DesktopStream.available !== descriptor.capabilities.Recording.available) {
30
+ throw new Error(
31
+ `"${backend}" Recording.available (${descriptor.capabilities.Recording.available}) must equal DesktopStream.available (${descriptor.capabilities.DesktopStream.available})`
32
+ );
33
+ }
34
+ if (descriptor.persistable && descriptor.snapshot.kind === "none") {
35
+ throw new Error(`"${backend}" persistable but snapshot.kind=none`);
36
+ }
37
+ if (descriptor.nativeBucketMount && (descriptor.tier === "dev" || descriptor.tier === "none")) {
38
+ throw new Error(`"${backend}" claims nativeBucketMount on tier=${descriptor.tier}`);
39
+ }
40
+ }
41
+ }
42
+
43
+ // src/sandbox/providers/blaxel.ts
44
+ import { BlaxelSandboxClient } from "@openai/agents-extensions/sandbox/blaxel";
45
+
46
+ // src/sandbox/errors.ts
47
+ var SandboxConfigError = class extends Error {
48
+ backend;
49
+ constructor(backend, message) {
50
+ super(`[sandbox:${backend}] ${message}`);
51
+ this.name = "SandboxConfigError";
52
+ this.backend = backend;
53
+ }
54
+ };
55
+ var SandboxProviderUnavailableError = class extends Error {
56
+ backend;
57
+ constructor(backend) {
58
+ super(`provider ${backend} not available in installed @openai/agents-extensions`);
59
+ this.name = "SandboxProviderUnavailableError";
60
+ this.backend = backend;
61
+ }
62
+ };
63
+
64
+ // src/sandbox/providers/blaxel.ts
65
+ var blaxelProvider = {
66
+ backend: "blaxel",
67
+ descriptor: CAPABILITY_DESCRIPTORS.blaxel,
68
+ validateCredentials(settings) {
69
+ if (!settings.blaxelApiKey) {
70
+ throw new SandboxConfigError("blaxel", "OPENGENI_BLAXEL_API_KEY is required");
71
+ }
72
+ },
73
+ build({ settings, environment }) {
74
+ const options = {
75
+ apiKey: settings.blaxelApiKey,
76
+ env: environment
77
+ };
78
+ if (settings.blaxelImage) options.image = settings.blaxelImage;
79
+ if (settings.blaxelRegion) options.region = settings.blaxelRegion;
80
+ if (settings.blaxelExposedPortPublic !== void 0) {
81
+ options.exposedPortPublic = settings.blaxelExposedPortPublic;
82
+ }
83
+ if (settings.blaxelExposedPortUrlTtlSeconds) {
84
+ options.exposedPortUrlTtlS = settings.blaxelExposedPortUrlTtlSeconds;
85
+ }
86
+ if (settings.blaxelMemoryMb) options.memory = settings.blaxelMemoryMb;
87
+ if (settings.blaxelTtl) options.ttl = settings.blaxelTtl;
88
+ return new BlaxelSandboxClient(options);
89
+ }
90
+ };
91
+
92
+ // src/sandbox/providers/cloudflare.ts
93
+ import { CloudflareSandboxClient } from "@openai/agents-extensions/sandbox/cloudflare";
94
+ var cloudflareProvider = {
95
+ backend: "cloudflare",
96
+ descriptor: CAPABILITY_DESCRIPTORS.cloudflare,
97
+ validateCredentials(settings) {
98
+ if (!settings.cloudflareWorkerUrl) {
99
+ throw new SandboxConfigError("cloudflare", "OPENGENI_CLOUDFLARE_WORKER_URL is required");
100
+ }
101
+ },
102
+ build({ settings, exposedPorts }) {
103
+ const options = {
104
+ workerUrl: settings.cloudflareWorkerUrl,
105
+ exposedPorts
106
+ };
107
+ if (settings.cloudflareApiKey) options.apiKey = settings.cloudflareApiKey;
108
+ return new CloudflareSandboxClient(options);
109
+ }
110
+ };
111
+
112
+ // src/sandbox/providers/daytona.ts
113
+ import { DaytonaSandboxClient } from "@openai/agents-extensions/sandbox/daytona";
114
+ var daytonaProvider = {
115
+ backend: "daytona",
116
+ descriptor: CAPABILITY_DESCRIPTORS.daytona,
117
+ validateCredentials(settings) {
118
+ if (!settings.daytonaApiKey) {
119
+ throw new SandboxConfigError("daytona", "OPENGENI_DAYTONA_API_KEY is required");
120
+ }
121
+ },
122
+ build({ settings, environment, exposedPorts }) {
123
+ const options = {
124
+ apiKey: settings.daytonaApiKey,
125
+ env: environment,
126
+ exposedPorts
127
+ };
128
+ if (settings.daytonaApiUrl) options.apiUrl = settings.daytonaApiUrl;
129
+ if (settings.daytonaTarget) options.target = settings.daytonaTarget;
130
+ if (settings.daytonaImage) options.image = settings.daytonaImage;
131
+ if (settings.daytonaSnapshotName) options.sandboxSnapshotName = settings.daytonaSnapshotName;
132
+ if (settings.daytonaAutoStopInterval !== void 0) {
133
+ options.autoStopInterval = settings.daytonaAutoStopInterval;
134
+ }
135
+ if (settings.daytonaTimeoutSeconds) options.timeoutSec = settings.daytonaTimeoutSeconds;
136
+ if (settings.daytonaExposedPortUrlTtlSeconds) {
137
+ options.exposedPortUrlTtlS = settings.daytonaExposedPortUrlTtlSeconds;
138
+ }
139
+ return new DaytonaSandboxClient(options);
140
+ }
141
+ };
142
+
143
+ // src/sandbox/providers/docker.ts
144
+ import { DockerSandboxClient } from "@openai/agents/sandbox/local";
145
+ var dockerProvider = {
146
+ backend: "docker",
147
+ descriptor: CAPABILITY_DESCRIPTORS.docker,
148
+ // Local dev container — no credentials. (The dockerNetwork decoration is
149
+ // applied by the factory, not here: it wraps the constructed client.)
150
+ validateCredentials() {
151
+ },
152
+ build({ settings, exposedPorts }) {
153
+ return new DockerSandboxClient({
154
+ image: settings.dockerImage,
155
+ exposedPorts
156
+ });
157
+ }
158
+ };
159
+
160
+ // src/sandbox/providers/e2b.ts
161
+ import { E2BSandboxClient } from "@openai/agents-extensions/sandbox/e2b";
162
+ var e2bProvider = {
163
+ backend: "e2b",
164
+ descriptor: CAPABILITY_DESCRIPTORS.e2b,
165
+ validateCredentials(settings) {
166
+ if (!settings.e2bApiKey) {
167
+ throw new SandboxConfigError("e2b", "OPENGENI_E2B_API_KEY is required");
168
+ }
169
+ },
170
+ build({ settings, environment, exposedPorts }) {
171
+ const options = {
172
+ env: environment,
173
+ exposedPorts
174
+ };
175
+ if (settings.e2bTemplate) options.template = settings.e2bTemplate;
176
+ if (settings.e2bTimeoutSeconds) options.timeout = settings.e2bTimeoutSeconds;
177
+ if (settings.e2bTimeoutAction) options.timeoutAction = settings.e2bTimeoutAction;
178
+ if (settings.e2bAllowInternetAccess !== void 0) {
179
+ options.allowInternetAccess = settings.e2bAllowInternetAccess;
180
+ }
181
+ if (settings.e2bAutoResume !== void 0) options.autoResume = settings.e2bAutoResume;
182
+ if (settings.e2bWorkspacePersistence) {
183
+ options.workspacePersistence = settings.e2bWorkspacePersistence;
184
+ }
185
+ return new E2BSandboxClient(options);
186
+ }
187
+ };
188
+
189
+ // src/sandbox/providers/local.ts
190
+ import { UnixLocalSandboxClient } from "@openai/agents/sandbox/local";
191
+ var localProvider = {
192
+ backend: "local",
193
+ descriptor: CAPABILITY_DESCRIPTORS.local,
194
+ // UnixLocalSandboxClient runs in-process — no credentials, no options.
195
+ validateCredentials() {
196
+ },
197
+ build() {
198
+ return new UnixLocalSandboxClient();
199
+ }
200
+ };
201
+
202
+ // src/sandbox/providers/modal.ts
203
+ import { ModalImageSelector, ModalSandboxClient } from "@openai/agents-extensions/sandbox/modal";
204
+ import { effectiveModalIdleTimeoutSeconds } from "@opengeni/config";
205
+ var modalProvider = {
206
+ backend: "modal",
207
+ descriptor: CAPABILITY_DESCRIPTORS.modal,
208
+ validateCredentials(settings) {
209
+ if (Boolean(settings.modalTokenId) !== Boolean(settings.modalTokenSecret)) {
210
+ throw new SandboxConfigError(
211
+ "modal",
212
+ "OPENGENI_MODAL_TOKEN_ID and OPENGENI_MODAL_TOKEN_SECRET must both be set or both omitted"
213
+ );
214
+ }
215
+ if (!settings.modalAppName) {
216
+ throw new SandboxConfigError("modal", "OPENGENI_MODAL_APP_NAME is required");
217
+ }
218
+ },
219
+ build({ settings, environment, exposedPorts }) {
220
+ const options = {
221
+ appName: settings.modalAppName,
222
+ timeoutMs: settings.modalTimeoutSeconds * 1e3,
223
+ exposedPorts,
224
+ env: environment
225
+ };
226
+ options.idleTimeoutMs = effectiveModalIdleTimeoutSeconds(settings) * 1e3;
227
+ if (settings.modalWorkspacePersistence) {
228
+ options.workspacePersistence = settings.modalWorkspacePersistence;
229
+ }
230
+ if (settings.modalImageRef) {
231
+ options.image = ModalImageSelector.fromTag(settings.modalImageRef);
232
+ }
233
+ if (settings.modalTokenId) {
234
+ options.tokenId = settings.modalTokenId;
235
+ }
236
+ if (settings.modalTokenSecret) {
237
+ options.tokenSecret = settings.modalTokenSecret;
238
+ }
239
+ if (settings.modalEnvironment) {
240
+ options.environment = settings.modalEnvironment;
241
+ }
242
+ return new ModalSandboxClient(options);
243
+ }
244
+ };
245
+
246
+ // src/sandbox/providers/none.ts
247
+ var noneProvider = {
248
+ backend: "none",
249
+ descriptor: CAPABILITY_DESCRIPTORS.none,
250
+ // No sandbox: nothing to validate, and build() returns undefined. The factory
251
+ // short-circuits on "none" before calling build, but we keep build honest.
252
+ validateCredentials() {
253
+ },
254
+ build() {
255
+ return void 0;
256
+ }
257
+ };
258
+
259
+ // src/sandbox/providers/runloop.ts
260
+ import { RunloopSandboxClient } from "@openai/agents-extensions/sandbox/runloop";
261
+ var runloopProvider = {
262
+ backend: "runloop",
263
+ descriptor: CAPABILITY_DESCRIPTORS.runloop,
264
+ validateCredentials(settings) {
265
+ if (!settings.runloopApiKey) {
266
+ throw new SandboxConfigError("runloop", "OPENGENI_RUNLOOP_API_KEY is required");
267
+ }
268
+ },
269
+ build({ settings, environment, exposedPorts }) {
270
+ const options = {
271
+ apiKey: settings.runloopApiKey,
272
+ env: environment,
273
+ exposedPorts,
274
+ // Tunnel v2: one tunnel for all ports. Defaults to true in our config.
275
+ tunnel: settings.runloopTunnel
276
+ };
277
+ if (settings.runloopBaseUrl) options.baseUrl = settings.runloopBaseUrl;
278
+ if (settings.runloopBlueprintName) options.blueprintName = settings.runloopBlueprintName;
279
+ if (settings.runloopBlueprintId) options.blueprintId = settings.runloopBlueprintId;
280
+ if (settings.runloopKeepAliveSeconds) {
281
+ options.timeouts = { keepAliveTimeoutMs: settings.runloopKeepAliveSeconds * 1e3 };
282
+ }
283
+ return new RunloopSandboxClient(options);
284
+ }
285
+ };
286
+
287
+ // src/sandbox/selfhosted/control-rpc.ts
288
+ import {
289
+ ControlRequest,
290
+ ControlResponse,
291
+ ErrorCode
292
+ } from "@opengeni/agent-proto";
293
+ function subjectFor(workspaceId, agentId) {
294
+ return `agent.${workspaceId}.${agentId}.rpc`;
295
+ }
296
+ var SelfhostedControlError = class extends Error {
297
+ name = "SelfhostedControlError";
298
+ code;
299
+ reason;
300
+ retryable;
301
+ fenced;
302
+ draining;
303
+ agentOffline;
304
+ osNotFound;
305
+ detail;
306
+ constructor(input) {
307
+ super(input.message);
308
+ this.code = input.code;
309
+ this.reason = input.reason;
310
+ this.retryable = input.retryable;
311
+ this.fenced = input.fenced ?? false;
312
+ this.draining = input.draining ?? false;
313
+ this.agentOffline = input.agentOffline ?? false;
314
+ this.osNotFound = input.osNotFound ?? false;
315
+ this.detail = input.detail ?? {};
316
+ }
317
+ };
318
+ function agentErrorToControlError(err) {
319
+ const message = err.message || `agent error (${err.code})`;
320
+ const detail = err.detail ?? {};
321
+ switch (err.code) {
322
+ case ErrorCode.ERROR_CODE_AGENT_OFFLINE:
323
+ return new SelfhostedControlError({
324
+ message: message || "the enrolled agent is offline",
325
+ code: err.code,
326
+ reason: "agent_offline",
327
+ retryable: false,
328
+ agentOffline: true,
329
+ detail
330
+ });
331
+ case ErrorCode.ERROR_CODE_TIMEOUT:
332
+ return new SelfhostedControlError({
333
+ message: message || "the enrolled agent did not respond in time",
334
+ code: err.code,
335
+ reason: "agent_reconnecting",
336
+ retryable: true,
337
+ detail
338
+ });
339
+ case ErrorCode.ERROR_CODE_CONSENT_REQUIRED:
340
+ return new SelfhostedControlError({
341
+ message: message || "the op requires consent that has not been granted",
342
+ code: err.code,
343
+ reason: "consent_required",
344
+ retryable: false,
345
+ detail
346
+ });
347
+ case ErrorCode.ERROR_CODE_DRAINING:
348
+ return new SelfhostedControlError({
349
+ message: message || "the agent is draining and cannot accept new work",
350
+ code: err.code,
351
+ reason: null,
352
+ retryable: true,
353
+ draining: true,
354
+ detail
355
+ });
356
+ case ErrorCode.ERROR_CODE_FENCED:
357
+ return new SelfhostedControlError({
358
+ message: message || "a stale op was fenced by the epoch guard; re-resolve and retry",
359
+ code: err.code,
360
+ reason: null,
361
+ retryable: true,
362
+ fenced: true,
363
+ detail
364
+ });
365
+ case ErrorCode.ERROR_CODE_NOT_FOUND:
366
+ return new SelfhostedControlError({
367
+ message: message || "the referenced path or ref does not exist",
368
+ code: err.code,
369
+ reason: null,
370
+ retryable: Boolean(err.retryable),
371
+ osNotFound: true,
372
+ detail
373
+ });
374
+ default:
375
+ return new SelfhostedControlError({
376
+ message,
377
+ code: err.code,
378
+ reason: null,
379
+ retryable: Boolean(err.retryable),
380
+ detail
381
+ });
382
+ }
383
+ }
384
+ function offlineAgentError(message = "no agent responded (offline)") {
385
+ return {
386
+ code: ErrorCode.ERROR_CODE_AGENT_OFFLINE,
387
+ message,
388
+ retryable: false,
389
+ detail: {}
390
+ };
391
+ }
392
+ function timeoutAgentError(message = "the agent did not respond in time") {
393
+ return {
394
+ code: ErrorCode.ERROR_CODE_TIMEOUT,
395
+ message,
396
+ retryable: true,
397
+ detail: {}
398
+ };
399
+ }
400
+ var NATS_NO_RESPONDERS_CODE = "503";
401
+ function isNoRespondersError(err) {
402
+ const code = err?.code;
403
+ if (typeof code === "string" && code === NATS_NO_RESPONDERS_CODE) {
404
+ return true;
405
+ }
406
+ const message = err instanceof Error ? err.message : String(err);
407
+ return /no responders|503/i.test(message);
408
+ }
409
+ function isRequestTimeoutError(err) {
410
+ const code = err?.code;
411
+ if (typeof code === "string" && /timeout/i.test(code)) {
412
+ return true;
413
+ }
414
+ const message = err instanceof Error ? err.message : String(err);
415
+ return /timeout|timed out/i.test(message);
416
+ }
417
+ var NatsControlRpc = class {
418
+ connect;
419
+ connection;
420
+ constructor(connect) {
421
+ this.connect = connect;
422
+ }
423
+ async resolveConnection() {
424
+ if (this.connection === void 0) {
425
+ try {
426
+ this.connection = await this.connect();
427
+ } catch {
428
+ this.connection = null;
429
+ return null;
430
+ }
431
+ }
432
+ return this.connection ?? null;
433
+ }
434
+ async request(subject, req, opts) {
435
+ const conn = await this.resolveConnection();
436
+ if (!conn) {
437
+ return offlineControlResponse(req.requestId);
438
+ }
439
+ const payload = ControlRequest.encode(req).finish();
440
+ try {
441
+ const reply = await conn.request(subject, payload, { timeout: opts.timeoutMs });
442
+ return ControlResponse.decode(reply.data);
443
+ } catch (err) {
444
+ if (isNoRespondersError(err)) {
445
+ return offlineControlResponse(req.requestId);
446
+ }
447
+ if (isRequestTimeoutError(err)) {
448
+ return timeoutControlResponse(req.requestId);
449
+ }
450
+ this.connection = void 0;
451
+ return offlineControlResponse(req.requestId);
452
+ }
453
+ }
454
+ };
455
+ function offlineControlResponse(requestId) {
456
+ return { requestId, error: offlineAgentError(), result: void 0 };
457
+ }
458
+ function timeoutControlResponse(requestId) {
459
+ return { requestId, error: timeoutAgentError(), result: void 0 };
460
+ }
461
+
462
+ // src/sandbox/selfhosted/session.ts
463
+ import {
464
+ FsEntryKind,
465
+ StreamKind
466
+ } from "@opengeni/agent-proto";
467
+ import { DESKTOP_STREAM_PORT as DESKTOP_STREAM_PORT2 } from "@opengeni/contracts";
468
+ import { Manifest } from "@openai/agents/sandbox";
469
+ var decoder = new TextDecoder();
470
+ var encoder = new TextEncoder();
471
+ var SELFHOSTED_VIRTUAL_ROOT = "/workspace";
472
+ function toMachinePath(p, workingDir) {
473
+ const base = workingDir.replace(/\/$/, "");
474
+ if (!p || p === SELFHOSTED_VIRTUAL_ROOT) return base;
475
+ if (p.startsWith(`${SELFHOSTED_VIRTUAL_ROOT}/`)) {
476
+ const rel = p.slice(SELFHOSTED_VIRTUAL_ROOT.length + 1);
477
+ return base ? `${base}/${rel}` : rel;
478
+ }
479
+ if (p.startsWith("/")) return p;
480
+ return base ? `${base}/${p}` : p;
481
+ }
482
+ var injectedApplyDiff;
483
+ function setSelfhostedApplyDiff(fn) {
484
+ injectedApplyDiff = fn;
485
+ }
486
+ var SELFHOSTED_DEFAULT_TIMEOUT_MS = 3e4;
487
+ var SELFHOSTED_RELAY_STREAM_PATH = "/stream";
488
+ var SelfhostedSession = class {
489
+ backendId = "selfhosted";
490
+ workspaceId;
491
+ agentId;
492
+ controlRpc;
493
+ relay;
494
+ epoch;
495
+ timeoutMs;
496
+ subject;
497
+ /** The session working directory — the path/cwd base every op is rooted under
498
+ * (see `toMachinePath`). "" by default ⇒ today's workspace_root behavior. */
499
+ workingDir;
500
+ /**
501
+ * The structural `state` slice consumers read. `agentId`/`instanceId` serve the
502
+ * channel-a `readInstanceId` + docker-network decoration (the agentId IS the
503
+ * identity). `manifest` is the slice the @openai/agents SDK reads AND writes per
504
+ * turn (serializeManifestEnvironment / validateProvidedSessionManifestUpdate read
505
+ * `manifest.root` + iterate `manifest.environment`; providedSessionManifest WRITES
506
+ * `state.manifest = next`). It must be a real, MUTABLE Manifest field — when the
507
+ * RoutingSandboxSession proxy resolves THIS as the active backend it returns
508
+ * `session.state` BY REFERENCE, so the SDK's read and write must both land on a
509
+ * well-formed Manifest here (defined `root`, object `environment`). Without it the
510
+ * SDK crashes with `undefined is not an object (evaluating 'current.root')`.
511
+ *
512
+ * `manifest` is intentionally a plain mutable field (not `readonly`) so the SDK's
513
+ * `state.manifest = next` write succeeds. It is NOT part of the persistable state
514
+ * (`serializeSessionState` round-trips `{agentId}` only).
515
+ *
516
+ * `environment` is the SDK `SandboxSessionState.environment` (a `Record<string,
517
+ * string>`). It MUST be present because the GROUP box's client serializes THIS
518
+ * (the active backend's) state at end-of-turn — the non-owned injected session is
519
+ * serialized via the CONFIGURED client (modal in prod), NOT the selfhosted client.
520
+ * Modal's `serializeRemoteSandboxSessionState` does `Object.entries(state.environment)`;
521
+ * an absent field crashes the post-turn RunState serialize with "Object.entries
522
+ * requires that input parameter not be null or undefined". It carries the run's
523
+ * threaded environment (or `{}`). The resulting modal-tagged envelope is inert for
524
+ * selfhosted (resume re-addresses the machine by agentId via the lease pointer,
525
+ * never from this SDK envelope), so its only job is to not crash the serialize.
526
+ */
527
+ state;
528
+ constructor(deps) {
529
+ this.workspaceId = deps.workspaceId;
530
+ this.agentId = deps.agentId;
531
+ this.controlRpc = deps.controlRpc;
532
+ this.relay = deps.relay;
533
+ this.epoch = deps.epoch ?? 0;
534
+ this.timeoutMs = deps.timeoutMs ?? SELFHOSTED_DEFAULT_TIMEOUT_MS;
535
+ this.subject = subjectFor(deps.workspaceId, deps.agentId);
536
+ this.workingDir = deps.workingDir ?? "";
537
+ this.state = {
538
+ agentId: deps.agentId,
539
+ instanceId: deps.agentId,
540
+ manifest: new Manifest({ root: "/workspace", entries: {}, environment: deps.environment ?? {} }),
541
+ // The SDK `SandboxSessionState.environment` — the run's threaded env (or `{}`).
542
+ // The group client's end-of-turn serialize reads `state.environment` directly
543
+ // (Object.entries), so it must be a defined object, not absent.
544
+ environment: deps.environment ?? {}
545
+ };
546
+ }
547
+ /** Issue a control op, decoding the agent's reply or throwing the mapped
548
+ * `SelfhostedControlError` on an AgentError (incl. a synthesized offline /
549
+ * timeout error from the transport). */
550
+ async call(op) {
551
+ const req = {
552
+ requestId: crypto.randomUUID(),
553
+ epoch: this.epoch,
554
+ op
555
+ };
556
+ const res = await this.controlRpc.request(this.subject, req, { timeoutMs: this.timeoutMs });
557
+ if (res.error) {
558
+ throw agentErrorToControlError(res.error);
559
+ }
560
+ if (!res.result) {
561
+ throw agentErrorToControlError({
562
+ code: 7,
563
+ // ERROR_CODE_PROTOCOL — an empty result is a protocol violation
564
+ message: "agent returned an empty control response",
565
+ retryable: false,
566
+ detail: {}
567
+ });
568
+ }
569
+ return res.result;
570
+ }
571
+ /** Channel-A `exec`: run a command on the machine and return its output. */
572
+ async exec(args) {
573
+ const execReq = {
574
+ // The agent does NOT shell-interpret unless `shell` — Channel-A passes a
575
+ // single shell command string, so run it through the platform shell.
576
+ command: [args.cmd],
577
+ shell: true,
578
+ // Rewrite a virtual-root cwd ("/workspace[/…]") onto the machine's frame —
579
+ // an absolute "/workspace" would ENOENT on a real machine (see
580
+ // SELFHOSTED_VIRTUAL_ROOT). Empty → the session workingDir (itself "" by
581
+ // default ⇒ the agent runs in its workspace_root).
582
+ cwd: toMachinePath(args.workdir, this.workingDir),
583
+ env: {},
584
+ stdin: new Uint8Array(0),
585
+ timeoutMs: 0
586
+ };
587
+ const result = await this.call({ $case: "exec", exec: execReq });
588
+ if (result.$case !== "exec") {
589
+ throw new Error(`selfhosted exec: unexpected result ${result.$case}`);
590
+ }
591
+ return execResultToChannelA(result.exec);
592
+ }
593
+ // ── The agent-turn provided-session contract (over the SAME NATS primitives) ──
594
+ // These are what the @openai/agents shell/filesystem/skills capabilities call on
595
+ // the ACTIVE session once the routing proxy resolves selfhosted. They reuse the
596
+ // exec/fs ops above; the machine owns its filesystem (materialization is a no-op).
597
+ /** SDK shell capability `execCommand`: run a command and return its stdout (the
598
+ * `exec_command` tool). Selfhosted exec is non-interactive (no PTY) — `tty` is
599
+ * ignored; `supportsPty()` is false so the SDK never offers a stdin session. */
600
+ async execCommand(args) {
601
+ const result = await this.exec({ cmd: args.cmd, workdir: args.workdir, runAs: args.runAs });
602
+ return result.output;
603
+ }
604
+ /** SDK shell capability never calls this (gated on `supportsPty()` which is
605
+ * false), but the surface advertises it. Selfhosted exec has no interactive PTY
606
+ * session over the structured RPC, so a stdin write is unsupported. */
607
+ supportsPty() {
608
+ return false;
609
+ }
610
+ /** SDK filesystem capability `view_image`: read the image bytes off the machine
611
+ * and wrap them in the tool-output image shape (magic-byte sniff + path fallback,
612
+ * mirroring the SDK's `imageOutputFromBytes`). */
613
+ async viewImage(args) {
614
+ const bytes = await this.readFile({ path: args.path, ...args.runAs ? { runAs: args.runAs } : {} });
615
+ const mediaType = sniffImageMediaType(bytes, args.path);
616
+ if (!mediaType) {
617
+ throw new Error(`selfhosted view_image: unsupported image format for ${args.path}`);
618
+ }
619
+ return { type: "image", image: { data: Uint8Array.from(bytes), mediaType } };
620
+ }
621
+ /** SDK skills/filesystem `pathExists`: whether a path exists on the machine. */
622
+ async pathExists(path, _runAs) {
623
+ const { exists } = await this.statFile({ path });
624
+ return exists;
625
+ }
626
+ /** SDK skills `listDir`: list a directory as `{name, path, type}[]`. */
627
+ async listDir(args) {
628
+ const result = await this.listFiles({ path: args.path });
629
+ return result.fsList.entries.map((entry) => ({
630
+ name: entry.name,
631
+ path: entry.path,
632
+ type: entry.kind === FsEntryKind.FS_ENTRY_KIND_DIRECTORY ? "dir" : entry.kind === FsEntryKind.FS_ENTRY_KIND_FILE ? "file" : "other"
633
+ }));
634
+ }
635
+ /** SDK manifest-delta `materializeEntry`: a NO-OP for selfhosted. Source
636
+ * materialization (cloning repos / staging files into the box) is how cloud
637
+ * providers prepare a fresh box; a bring-your-own machine already owns its
638
+ * filesystem and is prepared by the agent itself, so there is nothing to stage.
639
+ * Present (not absent) so the SDK's provided-session manifest apply path — which
640
+ * requires `applyManifest()` OR `materializeEntry()` when the agent declares
641
+ * entries — is satisfied without error. The selfhosted manifest declares no
642
+ * entries, so in practice this is never invoked with a real entry. */
643
+ async materializeEntry(_args) {
644
+ return;
645
+ }
646
+ /** SDK filesystem capability `createEditor`: the apply_patch host. Applies V4A
647
+ * diffs over the NATS fs ops (read → applyDiff → write). `applyDiff` is the SDK's
648
+ * own parser, injected by the runtime barrel (the leaf cannot import it). */
649
+ createEditor(runAs) {
650
+ const applyDiff = injectedApplyDiff;
651
+ if (!applyDiff) {
652
+ throw new Error(
653
+ "selfhosted createEditor: applyDiff not injected (the runtime barrel must call setSelfhostedApplyDiff before an agent turn binds the filesystem capability)"
654
+ );
655
+ }
656
+ const pathExists = (path) => this.pathExists(path, runAs);
657
+ const readText = async (path) => decoder.decode(await this.readFile({ path, ...runAs ? { runAs } : {} }));
658
+ const writeText = async (path, content) => {
659
+ await this.writeFile({ path, content, createParents: true });
660
+ };
661
+ const deletePath = async (path) => {
662
+ await this.exec({ cmd: `rm -rf -- ${shellQuote(toMachinePath(path, ""))}`, ...runAs ? { runAs } : {} });
663
+ };
664
+ return {
665
+ async createFile(operation) {
666
+ if (await pathExists(operation.path)) {
667
+ throw new Error(`selfhosted createFile: file already exists: ${operation.path}`);
668
+ }
669
+ await writeText(operation.path, applyDiff("", operation.diff, "create"));
670
+ return {};
671
+ },
672
+ async updateFile(operation) {
673
+ const current = await readText(operation.path);
674
+ const next = applyDiff(current, operation.diff);
675
+ const destination = operation.moveTo ?? operation.path;
676
+ await writeText(destination, next);
677
+ if (operation.moveTo && destination !== operation.path) {
678
+ await deletePath(operation.path);
679
+ }
680
+ return {};
681
+ },
682
+ async deleteFile(operation) {
683
+ await deletePath(operation.path);
684
+ return {};
685
+ }
686
+ };
687
+ }
688
+ /** Channel-A `readFile`: read a file off the machine (binary-safe). */
689
+ async readFile(args) {
690
+ const result = await this.call({
691
+ $case: "fsRead",
692
+ fsRead: {
693
+ path: toMachinePath(args.path, this.workingDir),
694
+ offset: "0",
695
+ length: args.maxBytes ? String(args.maxBytes) : "0"
696
+ }
697
+ });
698
+ if (result.$case !== "fsRead") {
699
+ throw new Error(`selfhosted readFile: unexpected result ${result.$case}`);
700
+ }
701
+ return result.fsRead.content;
702
+ }
703
+ /** Write a file onto the machine (the fs surface the descriptor advertises). */
704
+ async writeFile(args) {
705
+ const content = typeof args.content === "string" ? encoder.encode(args.content) : args.content;
706
+ const result = await this.call({
707
+ $case: "fsWrite",
708
+ fsWrite: {
709
+ path: toMachinePath(args.path, this.workingDir),
710
+ content,
711
+ createParents: args.createParents ?? true,
712
+ append: args.append ?? false,
713
+ mode: 0
714
+ }
715
+ });
716
+ if (result.$case !== "fsWrite") {
717
+ throw new Error(`selfhosted writeFile: unexpected result ${result.$case}`);
718
+ }
719
+ return Number(result.fsWrite.bytesWritten);
720
+ }
721
+ /** List a directory on the machine. */
722
+ async listFiles(args) {
723
+ const result = await this.call({
724
+ $case: "fsList",
725
+ fsList: { path: toMachinePath(args.path, this.workingDir), recursive: args.recursive ?? false }
726
+ });
727
+ if (result.$case !== "fsList") {
728
+ throw new Error(`selfhosted listFiles: unexpected result ${result.$case}`);
729
+ }
730
+ return result;
731
+ }
732
+ /** Stat a path on the machine. */
733
+ async statFile(args) {
734
+ const result = await this.call({ $case: "fsStat", fsStat: { path: toMachinePath(args.path, this.workingDir) } });
735
+ if (result.$case !== "fsStat") {
736
+ throw new Error(`selfhosted statFile: unexpected result ${result.$case}`);
737
+ }
738
+ return { exists: result.fsStat.exists };
739
+ }
740
+ // ── Computer-use control plane (the agent drives its OWN screen) ──────────────
741
+ // The CONTROL-PLANE twin of the relay DesktopInput/desktop stream: instead of a
742
+ // human viewer channel, the agent injects synthetic input into — and captures —
743
+ // its own display for the model's computer-use loop. Both route over the SAME
744
+ // `call()` primitive, so a consent/epoch rejection surfaces as the mapped
745
+ // `SelfhostedControlError` exactly like every other op. `NativeDesktopComputer`
746
+ // (sandbox-computer.ts) is the sole consumer.
747
+ /** Computer-use WRITE op: inject one synthetic desktop input event (pointer/key/
748
+ * scroll) on the machine's OWN display. The agent injects via CGEvent (macOS) /
749
+ * XTEST (Linux) and CONSENT-GATES it — an unconsented call never touches the OS
750
+ * and surfaces the mapped control error (ERROR_CODE_CONSENT_REQUIRED) via `call()`. */
751
+ async desktopInput(event) {
752
+ const result = await this.call({ $case: "desktopInput", desktopInput: { event } });
753
+ if (result.$case !== "desktopInput") {
754
+ throw new Error(`selfhosted desktopInput: unexpected result ${result.$case}`);
755
+ }
756
+ }
757
+ /** Computer-use VIEW op: capture a single PNG screenshot of the machine's desktop
758
+ * plus its geometry (via ScreenCaptureKit / x11). NOT consent-gated (a view op —
759
+ * the view/control decoupling), so it works with a display but no screen-control
760
+ * consent. Returns the raw encoded bytes + width/height. */
761
+ async screenshot() {
762
+ const result = await this.call({ $case: "desktopScreenshot", desktopScreenshot: {} });
763
+ if (result.$case !== "desktopScreenshot") {
764
+ throw new Error(`selfhosted screenshot: unexpected result ${result.$case}`);
765
+ }
766
+ return {
767
+ png: result.desktopScreenshot.png,
768
+ width: result.desktopScreenshot.width,
769
+ height: result.desktopScreenshot.height
770
+ };
771
+ }
772
+ /** A cheap liveness probe — request a Ping on the subject; returns true iff a
773
+ * responder answered (no AgentError). Used by `negotiateSelfhostedCapabilities`.
774
+ * The wire `nonce` is a uint64 (a numeric string), so the default is a random
775
+ * numeric value — NOT a UUID (which would fail proto uint64 encoding). */
776
+ async ping(nonce = randomNonce()) {
777
+ const req = {
778
+ requestId: crypto.randomUUID(),
779
+ epoch: this.epoch,
780
+ op: { $case: "ping", ping: { nonce } }
781
+ };
782
+ const res = await this.controlRpc.request(this.subject, req, { timeoutMs: this.timeoutMs });
783
+ return !res.error && res.result?.$case === "ping";
784
+ }
785
+ /**
786
+ * Resolve an exposed port to a relay stream endpoint (the viewer/pty plane).
787
+ * Returns the relay URL SHAPE — `{host:relay, port, tls, query:channel-key}` —
788
+ * after asking the agent to ensure a stream channel for the port. M8b wires the
789
+ * real relay tier (the byte pump) behind THIS seam.
790
+ *
791
+ * THE CHANNEL-KEY QUERY (the M8b relay-dial contract, dossier §10.5): the relay
792
+ * routes by `{workspaceId, agentId, port}` — the EXACT `ChannelKey::query` the
793
+ * agent's relay client (`opengeni-agent-stream`) appends when it registers the
794
+ * producer side: `ws=<workspaceId>&agent=<agentId>&port=<port>`. We append the
795
+ * agent-registered `channel=<channelId>` as a correlation hint. So the viewer
796
+ * dials `wss://<relay>/stream?ws=&agent=&port=&channel=` and presents the minted
797
+ * `ogs_` token in-band (NEVER as a URL param) — the relay pairs it with the
798
+ * producer by the routing key.
799
+ */
800
+ async resolveExposedPort(port) {
801
+ let channel;
802
+ if (port === DESKTOP_STREAM_PORT2) {
803
+ const result = await this.call({
804
+ $case: "desktopEnsure",
805
+ desktopEnsure: { width: 0, height: 0 }
806
+ });
807
+ if (result.$case !== "desktopEnsure") {
808
+ throw new Error(`selfhosted resolveExposedPort(${port}): unexpected result ${result.$case}`);
809
+ }
810
+ channel = result.desktopEnsure.channel;
811
+ } else {
812
+ const result = await this.call({
813
+ $case: "ptyOpen",
814
+ // Open the terminal in the session workingDir (default "" ⇒ the agent's
815
+ // workspace_root, byte-identical to before). A relative workingDir resolves
816
+ // under workspace_root; an absolute one is used as-is by the agent.
817
+ ptyOpen: { command: [], cwd: this.workingDir, env: {}, cols: 0, rows: 0, term: "xterm-256color" }
818
+ });
819
+ if (result.$case !== "ptyOpen") {
820
+ throw new Error(`selfhosted resolveExposedPort(${port}): unexpected result ${result.$case}`);
821
+ }
822
+ channel = result.ptyOpen.channel;
823
+ }
824
+ const channelId = channel?.channelId ?? channelKey(this.workspaceId, this.agentId, port);
825
+ const tls = this.relay.tls ?? true;
826
+ const routingQuery = `ws=${encodeURIComponent(this.workspaceId)}&agent=${encodeURIComponent(this.agentId)}&port=${port}&channel=${encodeURIComponent(channelId)}`;
827
+ return {
828
+ host: this.relay.host,
829
+ port: this.relay.port ?? (tls ? 443 : 80),
830
+ tls,
831
+ // The relay's wss route (`/stream`); buildStreamUrl honors `path`.
832
+ path: this.relay.path ?? SELFHOSTED_RELAY_STREAM_PATH,
833
+ query: routingQuery,
834
+ protocol: kindToProtocol(channel?.kind)
835
+ };
836
+ }
837
+ /** Round-trip the persistable state — `{agentId}` ONLY (resume = re-address). */
838
+ async serializeSessionState() {
839
+ return { agentId: this.agentId };
840
+ }
841
+ };
842
+ var SelfhostedSandboxClient = class {
843
+ backendId = "selfhosted";
844
+ supportsDefaultOptions = false;
845
+ workspaceId;
846
+ relay;
847
+ controlRpcFactory;
848
+ defaultAgentId;
849
+ epoch;
850
+ timeoutMs;
851
+ environment;
852
+ workingDir;
853
+ controlRpcMemo;
854
+ constructor(opts) {
855
+ this.workspaceId = opts.workspaceId;
856
+ this.relay = opts.relay;
857
+ this.controlRpcFactory = opts.controlRpcFactory;
858
+ this.defaultAgentId = opts.agentId;
859
+ this.epoch = opts.epoch;
860
+ this.timeoutMs = opts.timeoutMs;
861
+ this.environment = opts.environment;
862
+ this.workingDir = opts.workingDir;
863
+ }
864
+ controlRpc() {
865
+ if (!this.controlRpcMemo) {
866
+ this.controlRpcMemo = this.controlRpcFactory();
867
+ }
868
+ return this.controlRpcMemo;
869
+ }
870
+ bind(agentId) {
871
+ return new SelfhostedSession({
872
+ workspaceId: this.workspaceId,
873
+ agentId,
874
+ controlRpc: this.controlRpc(),
875
+ relay: this.relay,
876
+ ...this.epoch !== void 0 ? { epoch: this.epoch } : {},
877
+ ...this.timeoutMs !== void 0 ? { timeoutMs: this.timeoutMs } : {},
878
+ ...this.environment !== void 0 ? { environment: this.environment } : {},
879
+ ...this.workingDir !== void 0 ? { workingDir: this.workingDir } : {}
880
+ });
881
+ }
882
+ /** Bind a session to the live agent subject. There is no box to provision. */
883
+ async create(_manifest, _options) {
884
+ const agentId = this.requireAgentId();
885
+ return this.bind(agentId);
886
+ }
887
+ /** Resume = re-address the subject. Identical to create — no provider state. */
888
+ async resume(state, _options) {
889
+ const agentId = readAgentId(state) ?? this.requireAgentId();
890
+ return this.bind(agentId);
891
+ }
892
+ /** Serialize a live session's state → `{agentId}` ONLY. */
893
+ async serializeSessionState(state) {
894
+ const agentId = readAgentId(state) ?? this.requireAgentId();
895
+ return { agentId };
896
+ }
897
+ /** Deserialize `{agentId}` from the persisted envelope. */
898
+ async deserializeSessionState(state) {
899
+ const agentId = readAgentId(state) ?? this.requireAgentId();
900
+ return { agentId };
901
+ }
902
+ /** selfhosted is NOT persistable — there is no owned session state to preserve
903
+ * (the machine is the persistence). The lease never snapshots it. */
904
+ async canPersistOwnedSessionState() {
905
+ return false;
906
+ }
907
+ requireAgentId() {
908
+ if (!this.defaultAgentId) {
909
+ throw new Error("selfhosted sandbox client: no agentId bound (create()/resume() need a session state carrying agentId)");
910
+ }
911
+ return this.defaultAgentId;
912
+ }
913
+ };
914
+ async function buildSelfhostedBackendSession(deps) {
915
+ const client = new SelfhostedSandboxClient({
916
+ workspaceId: deps.workspaceId,
917
+ relay: deps.relay,
918
+ controlRpcFactory: deps.controlRpcFactory,
919
+ agentId: deps.agentId,
920
+ epoch: deps.epoch,
921
+ ...deps.timeoutMs !== void 0 ? { timeoutMs: deps.timeoutMs } : {},
922
+ ...deps.environment !== void 0 ? { environment: deps.environment } : {},
923
+ ...deps.workingDir ? { workingDir: deps.workingDir } : {}
924
+ });
925
+ const session = await client.resume({ agentId: deps.agentId });
926
+ return { client, session };
927
+ }
928
+ function readAgentId(state) {
929
+ if (state && typeof state === "object") {
930
+ const candidate = state.agentId ?? state.providerState?.agentId;
931
+ if (typeof candidate === "string" && candidate.length > 0) {
932
+ return candidate;
933
+ }
934
+ }
935
+ return void 0;
936
+ }
937
+ function execResultToChannelA(res) {
938
+ const stdout = decoder.decode(res.stdout);
939
+ const stderr = decoder.decode(res.stderr);
940
+ return {
941
+ output: stdout,
942
+ stdout,
943
+ stderr,
944
+ exitCode: res.exitCode
945
+ };
946
+ }
947
+ function channelKey(workspaceId, agentId, port) {
948
+ return `${workspaceId}:${agentId}:${port}`;
949
+ }
950
+ function shellQuote(value) {
951
+ return `'${value.replace(/'/g, `'\\''`)}'`;
952
+ }
953
+ function sniffImageMediaType(bytes, path) {
954
+ if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) return "image/png";
955
+ if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
956
+ if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 56) return "image/gif";
957
+ if (bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) return "image/webp";
958
+ if (bytes[0] === 66 && bytes[1] === 77) return "image/bmp";
959
+ if (bytes[0] === 73 && bytes[1] === 73 && bytes[2] === 42 && bytes[3] === 0 || bytes[0] === 77 && bytes[1] === 77 && bytes[2] === 0 && bytes[3] === 42) return "image/tiff";
960
+ if (looksLikeSvg(bytes)) return "image/svg+xml";
961
+ return mediaTypeFromPath(path);
962
+ }
963
+ function looksLikeSvg(bytes) {
964
+ const prefix = decoder.decode(bytes.subarray(0, Math.min(bytes.byteLength, 512))).trimStart().toLowerCase();
965
+ return prefix.startsWith("<svg") || /^<\?xml[\s\S]*<svg/u.test(prefix);
966
+ }
967
+ function mediaTypeFromPath(path) {
968
+ const p = path?.trim().toLowerCase() ?? "";
969
+ if (p.endsWith(".png")) return "image/png";
970
+ if (p.endsWith(".jpg") || p.endsWith(".jpeg")) return "image/jpeg";
971
+ if (p.endsWith(".gif")) return "image/gif";
972
+ if (p.endsWith(".webp")) return "image/webp";
973
+ if (p.endsWith(".bmp")) return "image/bmp";
974
+ if (p.endsWith(".tif") || p.endsWith(".tiff")) return "image/tiff";
975
+ if (p.endsWith(".svg") || p.endsWith(".svgz")) return "image/svg+xml";
976
+ return void 0;
977
+ }
978
+ function randomNonce() {
979
+ return String(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER));
980
+ }
981
+ function kindToProtocol(kind) {
982
+ switch (kind) {
983
+ case StreamKind.STREAM_KIND_PTY:
984
+ return "pty";
985
+ case StreamKind.STREAM_KIND_DESKTOP:
986
+ return "vnc";
987
+ default:
988
+ return "raw";
989
+ }
990
+ }
991
+ function isSelfhostedProviderNotFoundError(_error) {
992
+ return false;
993
+ }
994
+
995
+ // src/sandbox/providers/selfhosted.ts
996
+ function resolveRelayConfig(settings) {
997
+ const raw = settings.selfhostedRelayUrl?.trim();
998
+ if (!raw) {
999
+ return { host: "relay.opengeni.local", port: 443, tls: true, path: "/stream" };
1000
+ }
1001
+ try {
1002
+ const url = new URL(raw.includes("://") ? raw : `wss://${raw}`);
1003
+ const tls = url.protocol === "wss:" || url.protocol === "https:";
1004
+ const port = url.port ? Number(url.port) : tls ? 443 : 80;
1005
+ const path = url.pathname && url.pathname !== "/" ? url.pathname : "/stream";
1006
+ return { host: url.hostname, port, tls, path };
1007
+ } catch {
1008
+ return { host: raw, port: 443, tls: true, path: "/stream" };
1009
+ }
1010
+ }
1011
+ function defaultControlRpcFactory() {
1012
+ return new NatsControlRpc(async () => null);
1013
+ }
1014
+ var selfhostedProvider = {
1015
+ backend: "selfhosted",
1016
+ descriptor: CAPABILITY_DESCRIPTORS.selfhosted,
1017
+ /**
1018
+ * No per-box credentials: the machine is reached over the agent's own
1019
+ * enrollment. The enrollment-signing + relay-token secrets are deployment-level
1020
+ * config that lands with the connectivity/enrollment milestones (M4/M5) — and
1021
+ * the whole feature is gated by a `sandboxSelfhostedEnabled` flag (default off)
1022
+ * that does not yet exist in Settings. So validation is LENIENT (no-op) in M3:
1023
+ * boot must never break, and there is nothing per-box to validate. M4/M5 add
1024
+ * the (flag-gated) signing/relay presence checks here behind the same seam.
1025
+ */
1026
+ validateCredentials() {
1027
+ },
1028
+ /**
1029
+ * Build the registry client. `create()`/`resume()` bind a `SelfhostedSession`
1030
+ * to the agent subject; the per-request `{workspaceId, agentId, controlRpc}`
1031
+ * are supplied by the resume path (the lease's enrollment) — the registry
1032
+ * client carries the relay config + the default (offline-until-M4) ControlRpc
1033
+ * factory and a backendId-correct surface for `assertProviderRegistryInvariants`.
1034
+ */
1035
+ build({ settings }) {
1036
+ return new SelfhostedSandboxClient({
1037
+ // The workspaceId is bound per-request by the resume path (the API/worker
1038
+ // construct a request-scoped client with the lease's workspace + a live
1039
+ // ControlRpc). The registry-built client is the boot/assertion shape; an
1040
+ // empty workspaceId is fine until a session is bound with a real one.
1041
+ workspaceId: "",
1042
+ relay: resolveRelayConfig(settings),
1043
+ controlRpcFactory: defaultControlRpcFactory
1044
+ });
1045
+ }
1046
+ };
1047
+
1048
+ // src/sandbox/providers/vercel.ts
1049
+ import { VercelSandboxClient } from "@openai/agents-extensions/sandbox/vercel";
1050
+ var vercelProvider = {
1051
+ backend: "vercel",
1052
+ descriptor: CAPABILITY_DESCRIPTORS.vercel,
1053
+ validateCredentials(settings) {
1054
+ if (!settings.vercelToken) {
1055
+ throw new SandboxConfigError("vercel", "OPENGENI_VERCEL_TOKEN is required");
1056
+ }
1057
+ if (!settings.vercelProjectId) {
1058
+ throw new SandboxConfigError("vercel", "OPENGENI_VERCEL_PROJECT_ID is required");
1059
+ }
1060
+ },
1061
+ build({ settings, environment, exposedPorts }) {
1062
+ const options = {
1063
+ token: settings.vercelToken,
1064
+ projectId: settings.vercelProjectId,
1065
+ env: environment,
1066
+ exposedPorts
1067
+ };
1068
+ if (settings.vercelTeamId) options.teamId = settings.vercelTeamId;
1069
+ if (settings.vercelRuntime) options.runtime = settings.vercelRuntime;
1070
+ return new VercelSandboxClient(options);
1071
+ }
1072
+ };
1073
+
1074
+ // src/sandbox/providers/index.ts
1075
+ var PROVIDER_REGISTRY = {
1076
+ docker: dockerProvider,
1077
+ modal: modalProvider,
1078
+ local: localProvider,
1079
+ none: noneProvider,
1080
+ daytona: daytonaProvider,
1081
+ runloop: runloopProvider,
1082
+ e2b: e2bProvider,
1083
+ blaxel: blaxelProvider,
1084
+ cloudflare: cloudflareProvider,
1085
+ vercel: vercelProvider,
1086
+ selfhosted: selfhostedProvider
1087
+ };
1088
+ var ASSERTION_STUB_SETTINGS = {
1089
+ dockerImage: "opengeni-sandbox:local",
1090
+ modalAppName: "opengeni-sandbox",
1091
+ modalTimeoutSeconds: 900,
1092
+ daytonaApiKey: "stub",
1093
+ runloopApiKey: "stub",
1094
+ runloopTunnel: true,
1095
+ e2bApiKey: "stub",
1096
+ blaxelApiKey: "stub",
1097
+ cloudflareWorkerUrl: "https://stub.example.com",
1098
+ vercelToken: "stub",
1099
+ vercelProjectId: "stub"
1100
+ };
1101
+ function assertProviderRegistryInvariants() {
1102
+ assertDescriptorRegistryInvariants();
1103
+ for (const backend of SandboxBackend2.options) {
1104
+ const registration = PROVIDER_REGISTRY[backend];
1105
+ if (registration.backend !== backend) {
1106
+ throw new Error(`PROVIDER_REGISTRY["${backend}"].backend mismatch (got "${registration.backend}")`);
1107
+ }
1108
+ if (registration.descriptor.backend !== backend) {
1109
+ throw new Error(`PROVIDER_REGISTRY["${backend}"].descriptor.backend mismatch (got "${registration.descriptor.backend}")`);
1110
+ }
1111
+ if (backend === "none") {
1112
+ if (registration.descriptor.backendId !== "none") {
1113
+ throw new Error(`"none" descriptor.backendId must be "none" (got "${registration.descriptor.backendId}")`);
1114
+ }
1115
+ continue;
1116
+ }
1117
+ const client = registration.build({
1118
+ settings: ASSERTION_STUB_SETTINGS,
1119
+ environment: {},
1120
+ exposedPorts: []
1121
+ });
1122
+ const sdkBackendId = client?.backendId;
1123
+ if (typeof sdkBackendId !== "string") {
1124
+ throw new Error(`Provider "${backend}" SDK client has no string backendId`);
1125
+ }
1126
+ if (sdkBackendId !== registration.descriptor.backendId) {
1127
+ throw new Error(
1128
+ `Provider "${backend}" backendId mismatch: descriptor.backendId="${registration.descriptor.backendId}" but SDK client.backendId="${sdkBackendId}"`
1129
+ );
1130
+ }
1131
+ }
1132
+ }
1133
+ assertProviderRegistryInvariants();
1134
+
1135
+ // src/sandbox/index.ts
1136
+ import { collectSandboxEnvironment as collectSandboxEnvironment2, parseExposedPorts as parseExposedPorts2 } from "@opengeni/config";
1137
+
1138
+ // src/sandbox/select.ts
1139
+ import {
1140
+ CAPABILITY_DESCRIPTORS as CAPABILITY_DESCRIPTORS2
1141
+ } from "@opengeni/contracts";
1142
+ function selectBackend(backend) {
1143
+ const descriptor = CAPABILITY_DESCRIPTORS2[backend];
1144
+ if (!descriptor) {
1145
+ throw new Error(`Unknown sandbox backend "${backend}"`);
1146
+ }
1147
+ return descriptor;
1148
+ }
1149
+ function backendSupportsOs(descriptor, os) {
1150
+ return descriptor.os.supported.includes(os);
1151
+ }
1152
+ function desktopCapableBackend(backend) {
1153
+ const direct = CAPABILITY_DESCRIPTORS2[backend];
1154
+ if (direct) {
1155
+ return direct.capabilities.DesktopStream.available === true;
1156
+ }
1157
+ for (const descriptor of Object.values(CAPABILITY_DESCRIPTORS2)) {
1158
+ if (descriptor.backendId === backend) {
1159
+ return descriptor.capabilities.DesktopStream.available === true;
1160
+ }
1161
+ }
1162
+ return false;
1163
+ }
1164
+ function negotiateCapabilities(ctx) {
1165
+ const descriptor = selectBackend(ctx.backend);
1166
+ const osSupported = backendSupportsOs(descriptor, ctx.os);
1167
+ const negotiatedAt = (ctx.now ?? /* @__PURE__ */ new Date()).toISOString();
1168
+ const osReason = osSupported ? null : "os_unsupported";
1169
+ const fileSystem = (() => {
1170
+ if (osReason) {
1171
+ return { available: false, readOnly: true, root: descriptor.workspaceRoot, pathSep: "/", treeMode: "lazy", reason: osReason };
1172
+ }
1173
+ const cap = descriptor.capabilities.FileSystem;
1174
+ return {
1175
+ available: cap.available,
1176
+ readOnly: cap.readOnly,
1177
+ root: descriptor.workspaceRoot,
1178
+ pathSep: "/",
1179
+ treeMode: "lazy",
1180
+ reason: cap.available ? null : "backend_unsupported"
1181
+ };
1182
+ })();
1183
+ const terminal = (() => {
1184
+ const cap = descriptor.capabilities.Terminal;
1185
+ if (osReason) {
1186
+ return { transport: null, ptyCapable: false, shell: "/bin/bash", url: null, token: null, expiresAt: null, reason: osReason };
1187
+ }
1188
+ if (!cap.available) {
1189
+ return { transport: null, ptyCapable: false, shell: "/bin/bash", url: null, token: null, expiresAt: null, reason: "backend_unsupported" };
1190
+ }
1191
+ const ptyCapable = cap.pty;
1192
+ let transport = ptyCapable ? "pty-ws" : "sse-events";
1193
+ let reason = null;
1194
+ if (ptyCapable && ctx.terminalEnabled === false) {
1195
+ transport = "sse-events";
1196
+ reason = "disabled_by_policy";
1197
+ } else if (ptyCapable && ctx.liveness === "cold" && !ctx.terminalStream) {
1198
+ transport = "sse-events";
1199
+ reason = "lease_cold";
1200
+ }
1201
+ const minted = transport === "pty-ws" ? ctx.terminalStream : void 0;
1202
+ return {
1203
+ transport,
1204
+ ptyCapable,
1205
+ shell: "/bin/bash",
1206
+ url: minted?.url ?? null,
1207
+ token: minted?.token ?? null,
1208
+ expiresAt: minted?.expiresAt ?? null,
1209
+ reason
1210
+ };
1211
+ })();
1212
+ const git = (() => {
1213
+ const cap = descriptor.capabilities.Git;
1214
+ if (osReason) {
1215
+ return { available: false, repos: [], reason: osReason };
1216
+ }
1217
+ return { available: cap.available, repos: [], reason: cap.available ? null : "backend_unsupported" };
1218
+ })();
1219
+ const desktop = (() => {
1220
+ const cap = descriptor.capabilities.DesktopStream;
1221
+ let reason = null;
1222
+ let available = cap.available;
1223
+ if (osReason) {
1224
+ available = false;
1225
+ reason = osReason;
1226
+ } else if (!cap.available) {
1227
+ available = false;
1228
+ reason = descriptor.tier === "headless" ? "tier_headless" : "backend_unsupported";
1229
+ } else if (!ctx.desktopEnabled) {
1230
+ available = false;
1231
+ reason = "disabled_by_policy";
1232
+ } else if (ctx.streamTokenSecretAvailable === false) {
1233
+ available = false;
1234
+ reason = "disabled_by_policy";
1235
+ } else if (ctx.liveness === "cold" && !ctx.desktopStream) {
1236
+ available = false;
1237
+ reason = "lease_cold";
1238
+ }
1239
+ const shared = available ? Boolean(ctx.shared) : false;
1240
+ const acknowledged = available ? Boolean(ctx.desktopAcknowledged) : false;
1241
+ const minted = available && acknowledged ? ctx.desktopStream : void 0;
1242
+ const selfhostedFrames = ctx.backend === "selfhosted";
1243
+ const interactive = available && !selfhostedFrames && ctx.desktopInteractive !== false;
1244
+ const mode = interactive ? "interactive" : "read-only";
1245
+ return {
1246
+ transport: available ? selfhostedFrames ? "relay-frames" : cap.transport : null,
1247
+ client: available ? selfhostedFrames ? "frames" : "novnc" : null,
1248
+ mode,
1249
+ url: minted?.url ?? null,
1250
+ token: minted?.token ?? null,
1251
+ expiresAt: minted?.expiresAt ?? null,
1252
+ resolution: minted?.resolution ?? [1024, 768],
1253
+ // Desktop pixels are ALWAYS un-redacted when present (the literal
1254
+ // framebuffer); the acknowledgment gate rests on this.
1255
+ unredacted: true,
1256
+ requiresAcknowledgment: available,
1257
+ acknowledged: available ? Boolean(ctx.desktopAcknowledged) : false,
1258
+ // Shared-exposure disclosure (addendum E.1): `shared` when the group has
1259
+ // >1 session; `sharedSessionIds` is the OTHER sessions' ids ONLY (never
1260
+ // their conversation/metadata). Empty/false for a solo box or when the
1261
+ // desktop cell is unavailable.
1262
+ shared,
1263
+ sharedSessionIds: shared ? ctx.sharedSessionIds ?? [] : [],
1264
+ reason
1265
+ };
1266
+ })();
1267
+ const recording = (() => {
1268
+ const cap = descriptor.capabilities.Recording;
1269
+ if (osReason) {
1270
+ return { available: false, modes: [], codecs: [], reason: osReason };
1271
+ }
1272
+ if (!cap.available) {
1273
+ return { available: false, modes: [], codecs: [], reason: descriptor.tier === "headless" ? "tier_headless" : "backend_unsupported" };
1274
+ }
1275
+ if (!ctx.desktopEnabled) {
1276
+ return { available: false, modes: [], codecs: [], reason: "disabled_by_policy" };
1277
+ }
1278
+ return {
1279
+ available: true,
1280
+ modes: ["manual", "on-turn", "on-verify"],
1281
+ codecs: ["h264-mp4", "vp9-webm"],
1282
+ reason: null
1283
+ };
1284
+ })();
1285
+ const computerUse = (() => {
1286
+ const desktopCapable = descriptor.capabilities.DesktopStream.available;
1287
+ const readOnly = ctx.computerUseReadOnly ?? false;
1288
+ if (osReason) {
1289
+ return { available: false, readOnly, reason: osReason };
1290
+ }
1291
+ if (!desktopCapable) {
1292
+ return { available: false, readOnly, reason: descriptor.tier === "headless" ? "tier_headless" : "backend_unsupported" };
1293
+ }
1294
+ if (!ctx.desktopEnabled || ctx.computerUseEnabled === false) {
1295
+ return { available: false, readOnly, reason: "disabled_by_policy" };
1296
+ }
1297
+ return { available: true, readOnly, reason: null };
1298
+ })();
1299
+ return {
1300
+ sessionId: ctx.sessionId,
1301
+ backend: ctx.backend,
1302
+ os: ctx.os,
1303
+ liveness: ctx.liveness,
1304
+ leaseEpoch: ctx.leaseEpoch,
1305
+ viewerHeartbeatIntervalMs: 3e4,
1306
+ FileSystem: fileSystem,
1307
+ Terminal: terminal,
1308
+ Git: git,
1309
+ DesktopStream: desktop,
1310
+ Recording: recording,
1311
+ ComputerUse: computerUse,
1312
+ negotiatedAt
1313
+ };
1314
+ }
1315
+
1316
+ // src/sandbox/stream-token.ts
1317
+ import {
1318
+ DESKTOP_STREAM_PORT as DESKTOP_STREAM_PORT3,
1319
+ StreamTokenPayload,
1320
+ signStreamToken,
1321
+ verifyStreamToken as verifyStreamTokenEnvelope
1322
+ } from "@opengeni/contracts";
1323
+ import { StreamTokenPayload as StreamTokenPayload2 } from "@opengeni/contracts";
1324
+ var STREAM_TOKEN_DEFAULT_TTL_SECONDS = 120;
1325
+ async function mintStreamToken(secret, input) {
1326
+ const nowSeconds = input.nowSeconds ?? Math.floor(Date.now() / 1e3);
1327
+ const ttlSeconds = input.ttlSeconds ?? STREAM_TOKEN_DEFAULT_TTL_SECONDS;
1328
+ const payload = StreamTokenPayload.parse({
1329
+ workspaceId: input.workspaceId,
1330
+ sessionId: input.sessionId,
1331
+ viewerId: input.viewerId,
1332
+ leaseEpoch: input.leaseEpoch,
1333
+ mode: input.mode ?? "view",
1334
+ port: input.port ?? DESKTOP_STREAM_PORT3,
1335
+ exp: nowSeconds + ttlSeconds
1336
+ });
1337
+ return signStreamToken(secret, payload);
1338
+ }
1339
+ async function verifyStreamToken(secret, token, nowSeconds = Math.floor(Date.now() / 1e3)) {
1340
+ return verifyStreamTokenEnvelope(secret, token, nowSeconds);
1341
+ }
1342
+
1343
+ // src/sandbox/display-stack.ts
1344
+ import { DESKTOP_STREAM_PORT as DESKTOP_STREAM_PORT4 } from "@opengeni/contracts";
1345
+ var STREAM_PORT = DESKTOP_STREAM_PORT4;
1346
+ var DISPLAY_STACK_TIMEOUT_MS = 6e4;
1347
+ var DEFAULT_DESKTOP_GEOMETRY = { width: 1280, height: 800, dpi: 96 };
1348
+ var DisplayStackError = class extends Error {
1349
+ exitCode;
1350
+ stage;
1351
+ constructor(exitCode, output) {
1352
+ const stage = exitCode === 11 ? "xvfb" : exitCode === 12 ? "x11vnc" : exitCode === 13 ? "websockify" : "unknown";
1353
+ super(`desktop display stack failed at stage "${stage}" (exit ${exitCode})${output ? `:
1354
+ ${output}` : ""}`);
1355
+ this.name = "DisplayStackError";
1356
+ this.exitCode = exitCode;
1357
+ this.stage = stage;
1358
+ }
1359
+ };
1360
+ var DisplayStackUnsupportedError = class extends Error {
1361
+ constructor(message) {
1362
+ super(message);
1363
+ this.name = "DisplayStackUnsupportedError";
1364
+ }
1365
+ };
1366
+ function buildDisplayStackScript(options = {}) {
1367
+ const geometry = options.geometry ?? DEFAULT_DESKTOP_GEOMETRY;
1368
+ const port = options.port ?? DESKTOP_STREAM_PORT4;
1369
+ const env = `DESKTOP_W=${geometry.width} DESKTOP_H=${geometry.height} DESKTOP_DPI=${geometry.dpi} STREAM_PORT=${port}`;
1370
+ return `if nc -z 127.0.0.1 ${port} >/dev/null 2>&1 && nc -z 127.0.0.1 5900 >/dev/null 2>&1; then echo "OPENGENI_DESKTOP_UP port=${port} geometry=${geometry.width}x${geometry.height} dpi=${geometry.dpi} (precheck)"; else mkdir -p /tmp/opengeni-desktop && flock -w 45 /tmp/opengeni-desktop/up.outer.lock env ${env} opengeni-desktop-up; fi`;
1371
+ }
1372
+ function execResultOutput(result) {
1373
+ if (typeof result === "string") {
1374
+ return result;
1375
+ }
1376
+ return [result.output, result.stderr, result.stdout].filter((v) => typeof v === "string" && v.length > 0).join("\n");
1377
+ }
1378
+ function execResultExitCode(result) {
1379
+ if (typeof result === "string") {
1380
+ return null;
1381
+ }
1382
+ return typeof result.exitCode === "number" ? result.exitCode : null;
1383
+ }
1384
+ function inferExitFromOutput(output) {
1385
+ if (/OPENGENI_DESKTOP_UP\b/.test(output)) {
1386
+ return 0;
1387
+ }
1388
+ if (/Xvfb failed to come up/.test(output)) {
1389
+ return 11;
1390
+ }
1391
+ if (/x11vnc failed on/.test(output)) {
1392
+ return 12;
1393
+ }
1394
+ if (/websockify failed on/.test(output)) {
1395
+ return 13;
1396
+ }
1397
+ return -1;
1398
+ }
1399
+ async function ensureDisplayStack(session, options = {}) {
1400
+ const s = session;
1401
+ if (typeof s?.exec !== "function" && typeof s?.execCommand !== "function") {
1402
+ throw new DisplayStackUnsupportedError(
1403
+ "provider session cannot run commands (no exec/execCommand) \u2014 desktop tier unavailable"
1404
+ );
1405
+ }
1406
+ const geometry = options.geometry ?? DEFAULT_DESKTOP_GEOMETRY;
1407
+ const port = options.port ?? DESKTOP_STREAM_PORT4;
1408
+ const timeoutMs = options.timeoutMs ?? DISPLAY_STACK_TIMEOUT_MS;
1409
+ const cmd = buildDisplayStackScript({ geometry, port });
1410
+ const result = typeof s.exec === "function" ? await s.exec({ cmd, yieldTimeMs: timeoutMs, maxOutputTokens: 2e4 }) : await s.execCommand({ cmd, yieldTimeMs: timeoutMs, maxOutputTokens: 2e4 });
1411
+ const output = execResultOutput(result);
1412
+ const exitCode = execResultExitCode(result) ?? inferExitFromOutput(output);
1413
+ if (exitCode !== 0) {
1414
+ throw new DisplayStackError(exitCode, output);
1415
+ }
1416
+ const marker = (output.match(/OPENGENI_DESKTOP_UP[^\n]*/) ?? [""])[0];
1417
+ return { port, geometry, marker };
1418
+ }
1419
+ async function tearDownDisplayStack(session) {
1420
+ const s = session;
1421
+ if (typeof s?.exec === "function") {
1422
+ await s.exec({ cmd: "opengeni-desktop-down", yieldTimeMs: 1e4, maxOutputTokens: 4e3 });
1423
+ return;
1424
+ }
1425
+ if (typeof s?.execCommand === "function") {
1426
+ await s.execCommand({ cmd: "opengeni-desktop-down", yieldTimeMs: 1e4, maxOutputTokens: 4e3 });
1427
+ }
1428
+ }
1429
+
1430
+ // src/sandbox/terminal-server.ts
1431
+ import { TERMINAL_STREAM_PORT } from "@opengeni/contracts";
1432
+ var TERMINAL_SERVER_TIMEOUT_MS = 6e4;
1433
+ var TerminalServerError = class extends Error {
1434
+ exitCode;
1435
+ stage;
1436
+ constructor(exitCode, output) {
1437
+ const stage = exitCode === 14 ? "ttyd" : "unknown";
1438
+ super(`terminal server failed at stage "${stage}" (exit ${exitCode})${output ? `:
1439
+ ${output}` : ""}`);
1440
+ this.name = "TerminalServerError";
1441
+ this.exitCode = exitCode;
1442
+ this.stage = stage;
1443
+ }
1444
+ };
1445
+ var TerminalServerUnsupportedError = class extends Error {
1446
+ constructor(message) {
1447
+ super(message);
1448
+ this.name = "TerminalServerUnsupportedError";
1449
+ }
1450
+ };
1451
+ function buildTerminalServerScript(options = {}) {
1452
+ const port = options.port ?? TERMINAL_STREAM_PORT;
1453
+ return `mkdir -p /tmp/opengeni-terminal && flock -w 30 /tmp/opengeni-terminal/up.outer.lock env TERMINAL_PORT=${port} opengeni-terminal-up`;
1454
+ }
1455
+ function execResultOutput2(result) {
1456
+ if (typeof result === "string") {
1457
+ return result;
1458
+ }
1459
+ return [result.output, result.stderr, result.stdout].filter((v) => typeof v === "string" && v.length > 0).join("\n");
1460
+ }
1461
+ function execResultExitCode2(result) {
1462
+ if (typeof result === "string") {
1463
+ return null;
1464
+ }
1465
+ return typeof result.exitCode === "number" ? result.exitCode : null;
1466
+ }
1467
+ function inferExitFromOutput2(output) {
1468
+ if (/OPENGENI_TERMINAL_UP\b/.test(output)) {
1469
+ return 0;
1470
+ }
1471
+ if (/ttyd failed to come up/.test(output)) {
1472
+ return 14;
1473
+ }
1474
+ return -1;
1475
+ }
1476
+ async function ensureTerminalServer(session, options = {}) {
1477
+ const s = session;
1478
+ if (typeof s?.exec !== "function" && typeof s?.execCommand !== "function") {
1479
+ throw new TerminalServerUnsupportedError(
1480
+ "provider session cannot run commands (no exec/execCommand) \u2014 terminal pty-ws unavailable"
1481
+ );
1482
+ }
1483
+ const port = options.port ?? TERMINAL_STREAM_PORT;
1484
+ const timeoutMs = options.timeoutMs ?? TERMINAL_SERVER_TIMEOUT_MS;
1485
+ const cmd = buildTerminalServerScript({ port });
1486
+ const result = typeof s.exec === "function" ? await s.exec({ cmd, yieldTimeMs: timeoutMs, maxOutputTokens: 2e4 }) : await s.execCommand({ cmd, yieldTimeMs: timeoutMs, maxOutputTokens: 2e4 });
1487
+ const output = execResultOutput2(result);
1488
+ const exitCode = execResultExitCode2(result) ?? inferExitFromOutput2(output);
1489
+ if (exitCode !== 0) {
1490
+ throw new TerminalServerError(exitCode, output);
1491
+ }
1492
+ const marker = (output.match(/OPENGENI_TERMINAL_UP[^\n]*/) ?? [""])[0];
1493
+ return { port, marker };
1494
+ }
1495
+ async function tearDownTerminalServer(session) {
1496
+ const s = session;
1497
+ if (typeof s?.exec === "function") {
1498
+ await s.exec({ cmd: "opengeni-terminal-down", yieldTimeMs: 1e4, maxOutputTokens: 4e3 });
1499
+ return;
1500
+ }
1501
+ if (typeof s?.execCommand === "function") {
1502
+ await s.execCommand({ cmd: "opengeni-terminal-down", yieldTimeMs: 1e4, maxOutputTokens: 4e3 });
1503
+ }
1504
+ }
1505
+
1506
+ // src/sandbox/stream-port.ts
1507
+ import { DESKTOP_STREAM_PORT as DESKTOP_STREAM_PORT5 } from "@opengeni/contracts";
1508
+ var StreamPortUnavailableError = class extends Error {
1509
+ constructor(message, cause) {
1510
+ super(message);
1511
+ this.cause = cause;
1512
+ this.name = "StreamPortUnavailableError";
1513
+ }
1514
+ cause;
1515
+ };
1516
+ var DEFAULT_RESOLUTION = [1280, 800];
1517
+ function buildStreamUrl(endpoint) {
1518
+ if (typeof endpoint.host !== "string" || endpoint.host.length === 0 || typeof endpoint.port !== "number") {
1519
+ throw new StreamPortUnavailableError(
1520
+ `provider returned a malformed exposed-port endpoint (host=${String(endpoint.host)}, port=${String(endpoint.port)})`
1521
+ );
1522
+ }
1523
+ const tls = endpoint.tls ?? false;
1524
+ const scheme = tls ? "wss" : "ws";
1525
+ const defaultPort = tls ? 443 : 80;
1526
+ const host = endpoint.host.includes(":") && !endpoint.host.startsWith("[") ? `[${endpoint.host}]` : endpoint.host;
1527
+ const rawPath = typeof endpoint.path === "string" && endpoint.path.length > 0 ? endpoint.path : "/";
1528
+ const path = rawPath.startsWith("/") ? rawPath : `/${rawPath}`;
1529
+ const origin = endpoint.port === defaultPort ? `${scheme}://${host}` : `${scheme}://${host}:${endpoint.port}`;
1530
+ const authority = `${origin}${path}`;
1531
+ const query = endpoint.query ?? "";
1532
+ return query ? `${authority}?${query}` : authority;
1533
+ }
1534
+ async function exposeStreamPort(session, input) {
1535
+ const s = session;
1536
+ const port = input.port ?? DESKTOP_STREAM_PORT5;
1537
+ if (typeof s?.resolveExposedPort !== "function") {
1538
+ throw new StreamPortUnavailableError(
1539
+ "provider session cannot resolve exposed ports (no resolveExposedPort) \u2014 desktop stream unavailable"
1540
+ );
1541
+ }
1542
+ let endpoint;
1543
+ try {
1544
+ endpoint = await s.resolveExposedPort(port);
1545
+ } catch (error) {
1546
+ throw new StreamPortUnavailableError(
1547
+ `provider failed to resolve the stream port ${port}: ${error instanceof Error ? error.message : String(error)}`,
1548
+ error
1549
+ );
1550
+ }
1551
+ const url = buildStreamUrl(endpoint);
1552
+ const ttlSeconds = input.ttlSeconds ?? STREAM_TOKEN_DEFAULT_TTL_SECONDS;
1553
+ const nowSeconds = input.nowSeconds ?? Math.floor(Date.now() / 1e3);
1554
+ const token = await mintStreamToken(input.streamTokenSecret, {
1555
+ workspaceId: input.workspaceId,
1556
+ sessionId: input.sessionId,
1557
+ viewerId: input.viewerId,
1558
+ leaseEpoch: input.leaseEpoch,
1559
+ mode: "view",
1560
+ port,
1561
+ ttlSeconds,
1562
+ nowSeconds
1563
+ });
1564
+ return {
1565
+ url,
1566
+ token,
1567
+ expiresAt: new Date((nowSeconds + ttlSeconds) * 1e3).toISOString(),
1568
+ transport: "vnc-ws",
1569
+ client: "novnc",
1570
+ resolution: input.resolution ?? DEFAULT_RESOLUTION,
1571
+ leaseEpoch: input.leaseEpoch
1572
+ };
1573
+ }
1574
+
1575
+ // src/sandbox/recording.ts
1576
+ import { DESKTOP_STREAM_PORT as DESKTOP_STREAM_PORT6 } from "@opengeni/contracts";
1577
+ var DEFAULT_MAX_SECONDS = 600;
1578
+ var DEFAULT_FRAMERATE = 15;
1579
+ var DEFAULT_MAX_BYTES = 268435456;
1580
+ var DEFAULT_DIMENSIONS = [1280, 800];
1581
+ var STOP_YIELD_MS = 2e4;
1582
+ var EXEC_YIELD_MS = 15e3;
1583
+ function contentTypeForCodec(codec) {
1584
+ return codec === "vp9-webm" ? "video/webm" : "video/mp4";
1585
+ }
1586
+ function extForCodec(codec) {
1587
+ return codec === "vp9-webm" ? "webm" : "mp4";
1588
+ }
1589
+ var RecordingUnavailableError = class extends Error {
1590
+ constructor(message) {
1591
+ super(message);
1592
+ this.name = "RecordingUnavailableError";
1593
+ }
1594
+ };
1595
+ var RecordingError = class extends Error {
1596
+ constructor(message, reason) {
1597
+ super(message);
1598
+ this.reason = reason;
1599
+ this.name = "RecordingError";
1600
+ }
1601
+ reason;
1602
+ };
1603
+ function shq(s) {
1604
+ return `'${s.replace(/'/g, `'\\''`)}'`;
1605
+ }
1606
+ function resultOutput(result) {
1607
+ if (typeof result === "string") return result;
1608
+ return [result.output, result.stderr, result.stdout].filter((v) => typeof v === "string" && v.length > 0).join("\n");
1609
+ }
1610
+ var DEFAULT_MAX_OUTPUT_TOKENS = 4e3;
1611
+ async function run(session, cmd, runAs, yieldTimeMs = EXEC_YIELD_MS, maxOutputTokens = DEFAULT_MAX_OUTPUT_TOKENS) {
1612
+ const args = { cmd, ...runAs ? { runAs } : {}, yieldTimeMs, maxOutputTokens };
1613
+ if (typeof session.exec === "function") {
1614
+ return resultOutput(await session.exec(args));
1615
+ }
1616
+ if (typeof session.execCommand === "function") {
1617
+ return resultOutput(await session.execCommand(args));
1618
+ }
1619
+ throw new RecordingUnavailableError("session cannot run commands (no exec/execCommand) \u2014 recording unavailable");
1620
+ }
1621
+ function stripExecBanner(raw) {
1622
+ const marker = raw.lastIndexOf("\nOutput:\n");
1623
+ if (marker >= 0) return raw.slice(marker + "\nOutput:\n".length);
1624
+ if (raw.startsWith("Output:\n")) return raw.slice("Output:\n".length);
1625
+ return raw;
1626
+ }
1627
+ async function startRecording(session, input) {
1628
+ const s = session;
1629
+ const codec = input.codec ?? "h264-mp4";
1630
+ const dimensions = input.dimensions ?? DEFAULT_DIMENSIONS;
1631
+ const framerate = input.framerate ?? DEFAULT_FRAMERATE;
1632
+ const maxSeconds = input.maxSeconds ?? DEFAULT_MAX_SECONDS;
1633
+ const display = input.display ?? ":0";
1634
+ const tmp = input.tmpDir ?? "/tmp";
1635
+ const ext = extForCodec(codec);
1636
+ const boxPath = `${tmp}/og-rec-${input.recordingId}.${ext}`;
1637
+ const pidFile = `${tmp}/og-rec-${input.recordingId}.pid`;
1638
+ const logFile = `${tmp}/og-rec-${input.recordingId}.log`;
1639
+ const [w, h] = dimensions;
1640
+ const enc = codec === "vp9-webm" ? `-c:v libvpx-vp9 -b:v 0 -crf 32 -row-mt 1` : `-c:v libx264 -preset veryfast -pix_fmt yuv420p -movflags +faststart`;
1641
+ const ffmpeg = `nohup ffmpeg -hide_banner -loglevel error -f x11grab -draw_mouse 1 -framerate ${framerate} -video_size ${w}x${h} -i ${display}.0 -t ${maxSeconds} ${enc} ${boxPath} </dev/null >${logFile} 2>&1 & echo $! > ${pidFile}`;
1642
+ await run(s, `bash -lc ${shq(ffmpeg)}`, input.runAs);
1643
+ return {
1644
+ recordingId: input.recordingId,
1645
+ codec,
1646
+ boxPath,
1647
+ pidFile,
1648
+ dimensions,
1649
+ framerate,
1650
+ startedAt: Date.now(),
1651
+ display,
1652
+ ...input.runAs ? { runAs: input.runAs } : {}
1653
+ };
1654
+ }
1655
+ async function stopRecording(session, proc) {
1656
+ const s = session;
1657
+ const wait = `kill -INT "$(cat ${proc.pidFile})" 2>/dev/null; for i in $(seq 1 80); do kill -0 "$(cat ${proc.pidFile})" 2>/dev/null || break; sleep 0.1; done`;
1658
+ await run(s, `bash -lc ${shq(wait)}`, proc.runAs, STOP_YIELD_MS).catch(() => void 0);
1659
+ }
1660
+ async function readRecordingBytes(session, proc, maxBytes = DEFAULT_MAX_BYTES) {
1661
+ const s = session;
1662
+ if (typeof s.exec !== "function" && typeof s.execCommand !== "function") {
1663
+ throw new RecordingUnavailableError("session cannot run commands (no exec/execCommand) \u2014 recording finalize unavailable");
1664
+ }
1665
+ const sizeOut = (await run(s, `bash -lc ${shq(`stat -c %s ${proc.boxPath} 2>/dev/null || echo MISSING`)}`, proc.runAs)).trim();
1666
+ const sizeLine = sizeOut.split("\n").map((l) => l.trim()).filter(Boolean).pop() ?? "MISSING";
1667
+ if (sizeLine === "MISSING" || sizeLine === "") {
1668
+ throw new RecordingError(`recording file missing on box: ${proc.boxPath}`, "box-death");
1669
+ }
1670
+ const size = Number(sizeLine);
1671
+ if (!Number.isFinite(size) || size <= 0) {
1672
+ throw new RecordingError(`recording file empty on box: ${proc.boxPath}`, "ffmpeg-error");
1673
+ }
1674
+ if (size > maxBytes) {
1675
+ throw new RecordingError(`recording ${size}B exceeds max ${maxBytes}B`, "max-bytes-exceeded");
1676
+ }
1677
+ const STOP_YIELD = 6e4;
1678
+ const encoded = stripExecBanner(
1679
+ await run(s, `bash -lc ${shq(`base64 ${proc.boxPath}`)}`, proc.runAs, STOP_YIELD, null)
1680
+ );
1681
+ const base64 = encoded.replace(/\s+/g, "");
1682
+ if (base64.length === 0) {
1683
+ throw new RecordingError(`recording read returned 0 bytes: ${proc.boxPath}`, "ffmpeg-error");
1684
+ }
1685
+ let bytes;
1686
+ try {
1687
+ bytes = Uint8Array.from(Buffer.from(base64, "base64"));
1688
+ } catch (error) {
1689
+ throw new RecordingError(`recording base64 decode failed: ${error instanceof Error ? error.message : String(error)}`, "ffmpeg-error");
1690
+ }
1691
+ if (bytes.length === 0) {
1692
+ throw new RecordingError(`recording read returned 0 bytes: ${proc.boxPath}`, "ffmpeg-error");
1693
+ }
1694
+ return {
1695
+ bytes,
1696
+ contentType: contentTypeForCodec(proc.codec),
1697
+ sizeBytes: bytes.length,
1698
+ durationSeconds: Math.max(0, Math.round((Date.now() - proc.startedAt) / 1e3))
1699
+ };
1700
+ }
1701
+ async function deleteRecordingArtifacts(session, proc) {
1702
+ const s = session;
1703
+ const logFile = proc.boxPath.replace(/\.(mp4|webm)$/, ".log");
1704
+ await run(s, `rm -f ${proc.boxPath} ${proc.pidFile} ${logFile}`, proc.runAs).catch(() => void 0);
1705
+ }
1706
+ function recordingStorageKey(workspaceId, sessionId, recordingId, codec) {
1707
+ return `recordings/${workspaceId}/${sessionId}/${recordingId}.${extForCodec(codec)}`;
1708
+ }
1709
+
1710
+ // src/sandbox/channel-a.ts
1711
+ var ChannelAValidationError = class extends Error {
1712
+ constructor(message) {
1713
+ super(message);
1714
+ this.name = "ChannelAValidationError";
1715
+ }
1716
+ };
1717
+ var ChannelAConflictError = class extends Error {
1718
+ constructor(message) {
1719
+ super(message);
1720
+ this.name = "ChannelAConflictError";
1721
+ }
1722
+ };
1723
+ var ChannelANotFoundError = class extends Error {
1724
+ constructor(message) {
1725
+ super(message);
1726
+ this.name = "ChannelANotFoundError";
1727
+ }
1728
+ };
1729
+ var ChannelAUnsupportedError = class extends Error {
1730
+ constructor(message) {
1731
+ super(message);
1732
+ this.name = "ChannelAUnsupportedError";
1733
+ }
1734
+ };
1735
+ var NUL = String.fromCharCode(0);
1736
+ var US = String.fromCharCode(31);
1737
+ var RS = String.fromCharCode(30);
1738
+ var SandboxChannelAService = class {
1739
+ session;
1740
+ workspaceRoot;
1741
+ leaseEpoch;
1742
+ revision;
1743
+ emit;
1744
+ runAs;
1745
+ constructor(opts) {
1746
+ this.session = opts.session;
1747
+ this.workspaceRoot = opts.workspaceRoot ?? "";
1748
+ this.leaseEpoch = opts.leaseEpoch ?? 0;
1749
+ this.revision = opts.revision ?? 0;
1750
+ this.emit = opts.emit;
1751
+ this.runAs = opts.runAs;
1752
+ }
1753
+ /** Capability probe — the compact Channel-A projection. */
1754
+ capabilities(repos = []) {
1755
+ const s = this.session;
1756
+ const hasExec = Boolean(s.exec || s.execCommand);
1757
+ const hasFs = Boolean(s.readFile && (s.exec || s.execCommand || s.createEditor));
1758
+ return {
1759
+ FileSystem: { available: hasFs, readOnly: !(s.exec || s.createEditor), root: this.workspaceRoot },
1760
+ Terminal: {
1761
+ events: hasExec,
1762
+ exec: hasExec,
1763
+ pty: { available: Boolean(s.supportsPty?.() && s.writeStdin) }
1764
+ },
1765
+ Git: { available: hasExec, repos }
1766
+ };
1767
+ }
1768
+ // ════════════════════════════ exec primitive ══════════════════════════════
1769
+ // RAW exec — returns {stdout, stderr, exitCode}. Uses session.exec when present
1770
+ // (the local/docker sessions return raw output); falls back to execCommand +
1771
+ // a banner strip (last resort; banner-truncation can mangle, so exec is always
1772
+ // preferred). Throws ChannelAUnsupportedError when neither exists.
1773
+ async run(args) {
1774
+ const withRunAs = this.runAs ? { ...args, runAs: this.runAs } : args;
1775
+ if (this.session.exec) {
1776
+ const r = await this.session.exec(withRunAs);
1777
+ return {
1778
+ stdout: r.stdout ?? r.output ?? "",
1779
+ stderr: r.stderr ?? "",
1780
+ exitCode: r.exitCode ?? null,
1781
+ ...typeof r.sessionId === "number" ? { sessionId: r.sessionId } : {},
1782
+ wallTimeSeconds: r.wallTimeSeconds ?? 0
1783
+ };
1784
+ }
1785
+ if (this.session.execCommand) {
1786
+ const raw = await this.session.execCommand(withRunAs);
1787
+ const sessionId = parseExecBannerSessionId(raw);
1788
+ return {
1789
+ stdout: stripExecBanner2(raw),
1790
+ stderr: "",
1791
+ exitCode: null,
1792
+ ...sessionId !== null ? { sessionId } : {},
1793
+ wallTimeSeconds: 0
1794
+ };
1795
+ }
1796
+ throw new ChannelAUnsupportedError("the box does not support command execution");
1797
+ }
1798
+ // ════════════════════════════ FileSystem (A2) ═════════════════════════════
1799
+ async fsList(req) {
1800
+ const root = normalizeRelPath(req.path);
1801
+ const findRoot = root === "" ? "." : shellQuote2(root);
1802
+ const depthArg = Math.max(1, req.depth);
1803
+ const hidden = req.includeHidden ? "" : ` -not -path '*/.*'`;
1804
+ const gnuFind = `find ${findRoot} -mindepth 1 -maxdepth ${depthArg}${hidden} -printf '%y\\t%s\\t%T@\\t%m\\t%p\\0' 2>/dev/null`;
1805
+ let { stdout } = await this.run({ cmd: `bash -lc ${shellQuote2(gnuFind)}`, workdir: this.workspaceRoot || void 0 });
1806
+ if (!stdout) {
1807
+ const portableFind = [
1808
+ `find ${findRoot} -mindepth 1 -maxdepth ${depthArg}${hidden} -print0 2>/dev/null | while IFS= read -r -d '' p; do`,
1809
+ `if [ -d "$p" ]; then t=d; size=0; elif [ -f "$p" ]; then t=f; size=$(wc -c < "$p" | tr -d ' '); elif [ -L "$p" ]; then t=l; size=0; else t=o; size=0; fi;`,
1810
+ `mtime=$(date -r "$p" +%s 2>/dev/null || stat -c %Y "$p" 2>/dev/null || echo 0);`,
1811
+ `mode=$(stat -f %Lp "$p" 2>/dev/null || stat -c %a "$p" 2>/dev/null || echo 0);`,
1812
+ `printf '%s\\t%s\\t%s\\t%s\\t%s\\0' "$t" "$size" "$mtime" "$mode" "$p";`,
1813
+ `done`
1814
+ ].join(" ");
1815
+ ({ stdout } = await this.run({ cmd: `bash -lc ${shellQuote2(portableFind)}`, workdir: this.workspaceRoot || void 0 }));
1816
+ }
1817
+ const entries = stdout.split(NUL).filter((s) => s.length > 0);
1818
+ const rootNode = {
1819
+ name: basename(root) || (root === "" ? "" : root),
1820
+ path: root,
1821
+ type: "dir",
1822
+ sizeBytes: null,
1823
+ mtimeMs: null,
1824
+ mode: null,
1825
+ children: [],
1826
+ truncated: false
1827
+ };
1828
+ const byPath = /* @__PURE__ */ new Map();
1829
+ byPath.set(root, rootNode);
1830
+ let count = 0;
1831
+ let truncated = false;
1832
+ for (const entry of entries) {
1833
+ if (count >= req.maxEntries) {
1834
+ truncated = true;
1835
+ break;
1836
+ }
1837
+ const parts = entry.split(" ");
1838
+ if (parts.length < 5) continue;
1839
+ const [typeChar, sizeStr, mtimeStr, modeStr, ...pathParts] = parts;
1840
+ const rawPath = pathParts.join(" ");
1841
+ const relPath = stripDotSlash(rawPath, root);
1842
+ const node = {
1843
+ name: basename(relPath),
1844
+ path: relPath,
1845
+ type: findTypeToNode(typeChar ?? ""),
1846
+ sizeBytes: typeChar === "d" ? null : safeInt(sizeStr),
1847
+ mtimeMs: mtimeToMs(mtimeStr),
1848
+ mode: safeOctal(modeStr),
1849
+ ...typeChar === "d" ? { children: [] } : {},
1850
+ truncated: false
1851
+ };
1852
+ byPath.set(relPath, node);
1853
+ count++;
1854
+ }
1855
+ for (const [path, node] of byPath) {
1856
+ if (path === root) continue;
1857
+ const parentPath = dirnameRel(path, root);
1858
+ const parent = byPath.get(parentPath) ?? rootNode;
1859
+ (parent.children ??= []).push(node);
1860
+ }
1861
+ sortTree(rootNode);
1862
+ return { root: rootNode, revision: this.revision, truncated };
1863
+ }
1864
+ async fsRead(req) {
1865
+ const path = assertSafeRelPath(req.path);
1866
+ if (!this.session.readFile) {
1867
+ return await this.fsReadViaExec(path, req);
1868
+ }
1869
+ let raw;
1870
+ try {
1871
+ raw = await this.session.readFile({ path: this.joinRoot(path), maxBytes: req.maxBytes, ...this.runAs ? { runAs: this.runAs } : {} });
1872
+ } catch (error) {
1873
+ if (isWorkspaceEscapeError(error)) {
1874
+ return await this.fsReadViaExec(path, req);
1875
+ }
1876
+ throw new ChannelANotFoundError(`file not found: ${path} (${error instanceof Error ? error.message : String(error)})`);
1877
+ }
1878
+ const bytes = typeof raw === "string" ? Buffer.from(raw, "utf8") : Buffer.from(raw);
1879
+ return this.shapeRead(path, bytes, req);
1880
+ }
1881
+ /** Read a file by base64-ing it through exec. Binary-safe and — crucially —
1882
+ * NOT subject to the provider's native-readFile workspace-escape validation,
1883
+ * so it can render a symlink whose target lives outside /workspace (the link
1884
+ * node itself is in-workspace). `base64 <path>` follows the symlink. */
1885
+ async fsReadViaExec(path, req) {
1886
+ const abs = this.joinRoot(path);
1887
+ const { stdout, exitCode } = await this.run({ cmd: `base64 ${shellQuote2(abs)} 2>/dev/null | head -c ${Math.ceil(req.maxBytes * 1.4)}` });
1888
+ if (exitCode !== null && exitCode !== 0 && stdout === "") {
1889
+ throw new ChannelANotFoundError(`file not found: ${path}`);
1890
+ }
1891
+ const bytes = Buffer.from(stdout.replace(/\n/g, ""), "base64");
1892
+ return this.shapeRead(path, bytes, req);
1893
+ }
1894
+ shapeRead(path, bytes, req) {
1895
+ const truncated = bytes.byteLength >= req.maxBytes;
1896
+ const isBinary = sniffBinary(bytes);
1897
+ const encoding = req.encoding === "base64" || isBinary ? "base64" : "utf8";
1898
+ const content = encoding === "base64" ? bytes.toString("base64") : bytes.toString("utf8");
1899
+ return {
1900
+ path,
1901
+ encoding,
1902
+ content,
1903
+ sizeBytes: bytes.byteLength,
1904
+ truncated,
1905
+ isBinary,
1906
+ revision: this.revision
1907
+ };
1908
+ }
1909
+ async fsWrite(req) {
1910
+ const path = assertSafeRelPath(req.path);
1911
+ const abs = this.joinRoot(path);
1912
+ const bytes = req.encoding === "base64" ? Buffer.from(req.content, "base64") : Buffer.from(req.content, "utf8");
1913
+ if (!req.overwrite) {
1914
+ const { exitCode: exitCode2 } = await this.run({ cmd: `test -e ${shellQuote2(abs)}` });
1915
+ if (exitCode2 === 0) {
1916
+ throw new ChannelAConflictError(`path exists and overwrite is false: ${path}`);
1917
+ }
1918
+ }
1919
+ if (req.createParents) {
1920
+ const dir = dirnameAbs(abs);
1921
+ if (dir) await this.run({ cmd: `mkdir -p ${shellQuote2(dir)}` });
1922
+ }
1923
+ const b64 = bytes.toString("base64");
1924
+ const { exitCode, stderr } = await this.run({
1925
+ cmd: `printf %s ${shellQuote2(b64)} | base64 -d > ${shellQuote2(abs)}`
1926
+ });
1927
+ if (exitCode !== null && exitCode !== 0) {
1928
+ if (req.encoding !== "base64" && this.session.createEditor) {
1929
+ const ok2 = await this.tryEditorWrite(abs, req.content);
1930
+ if (!ok2) throw new ChannelAValidationError(`failed to write ${path}: ${stderr || `exit ${exitCode}`}`);
1931
+ } else {
1932
+ throw new ChannelAValidationError(`failed to write ${path}: ${stderr || `exit ${exitCode}`}`);
1933
+ }
1934
+ }
1935
+ this.revision++;
1936
+ await this.emitFsChanged([{ path, kind: "modified", isDir: false, sizeBytes: bytes.byteLength }], "write");
1937
+ return { path, sizeBytes: bytes.byteLength, revision: this.revision };
1938
+ }
1939
+ async tryEditorWrite(absPath, content) {
1940
+ const editor = this.session.createEditor?.(this.runAs);
1941
+ if (!editor?.createFile) return false;
1942
+ try {
1943
+ const diff = content.split("\n").map((line) => `+${line}`).join("\n");
1944
+ await editor.createFile({ type: "create_file", path: absPath, diff });
1945
+ return true;
1946
+ } catch {
1947
+ return false;
1948
+ }
1949
+ }
1950
+ async fsDelete(req) {
1951
+ const path = assertSafeRelPath(req.path);
1952
+ const abs = this.joinRoot(path);
1953
+ const flag = req.recursive ? "-rf" : "-f";
1954
+ const { exitCode, stderr } = await this.run({ cmd: `rm ${flag} ${shellQuote2(abs)}` });
1955
+ if (exitCode !== null && exitCode !== 0) {
1956
+ throw new ChannelAValidationError(`failed to delete ${path}: ${stderr || `exit ${exitCode}`}`);
1957
+ }
1958
+ this.revision++;
1959
+ await this.emitFsChanged([{ path, kind: "deleted", isDir: false, sizeBytes: null }], "write");
1960
+ return { revision: this.revision };
1961
+ }
1962
+ async fsMove(req) {
1963
+ const path = assertSafeRelPath(req.path);
1964
+ const newPath = assertSafeRelPath(req.newPath);
1965
+ const abs = this.joinRoot(path);
1966
+ const newAbs = this.joinRoot(newPath);
1967
+ if (!req.overwrite) {
1968
+ const { exitCode: exitCode2 } = await this.run({ cmd: `test -e ${shellQuote2(newAbs)}` });
1969
+ if (exitCode2 === 0) {
1970
+ throw new ChannelAConflictError(`destination exists and overwrite is false: ${newPath}`);
1971
+ }
1972
+ }
1973
+ if (req.createParents) {
1974
+ const dir = dirnameAbs(newAbs);
1975
+ if (dir) await this.run({ cmd: `mkdir -p ${shellQuote2(dir)}` });
1976
+ }
1977
+ const flag = req.overwrite ? "-f " : "";
1978
+ const { exitCode, stderr } = await this.run({
1979
+ cmd: `mv ${flag}${shellQuote2(abs)} ${shellQuote2(newAbs)}`
1980
+ });
1981
+ if (exitCode !== null && exitCode !== 0) {
1982
+ throw new ChannelAValidationError(`failed to move ${path} -> ${newPath}: ${stderr || `exit ${exitCode}`}`);
1983
+ }
1984
+ this.revision++;
1985
+ await this.emitFsChanged(
1986
+ [
1987
+ { path, kind: "deleted", isDir: false, sizeBytes: null },
1988
+ { path: newPath, kind: "created", isDir: false, sizeBytes: null }
1989
+ ],
1990
+ "write"
1991
+ );
1992
+ return { path, newPath, revision: this.revision };
1993
+ }
1994
+ async fsMkdir(req) {
1995
+ const path = assertSafeRelPath(req.path);
1996
+ const abs = this.joinRoot(path);
1997
+ const flag = req.recursive ? "-p " : "";
1998
+ const { exitCode, stderr } = await this.run({ cmd: `mkdir ${flag}${shellQuote2(abs)}` });
1999
+ if (exitCode !== null && exitCode !== 0) {
2000
+ throw new ChannelAValidationError(`failed to mkdir ${path}: ${stderr || `exit ${exitCode}`}`);
2001
+ }
2002
+ this.revision++;
2003
+ await this.emitFsChanged([{ path, kind: "created", isDir: true, sizeBytes: null }], "write");
2004
+ return { path, revision: this.revision };
2005
+ }
2006
+ // ════════════════════════════ Git (A2, read-only) ═════════════════════════
2007
+ async gitStatus(req) {
2008
+ const repo = this.repoWorkdir(req.path);
2009
+ const inside = await this.run({ cmd: "git rev-parse --is-inside-work-tree 2>/dev/null", workdir: repo });
2010
+ if (inside.stdout.trim() !== "true") {
2011
+ return { isRepo: false, head: null, detached: false, upstream: null, ahead: 0, behind: 0, files: [], revision: this.revision };
2012
+ }
2013
+ const { stdout } = await this.run({ cmd: "git status --porcelain=v2 --branch -z", workdir: repo });
2014
+ return { ...parsePorcelainV2(stdout), revision: this.revision };
2015
+ }
2016
+ async gitDiff(req) {
2017
+ const repo = this.repoWorkdir(req.path);
2018
+ const ctx = req.contextLines;
2019
+ let range = "";
2020
+ if (req.fromRef && req.toRef) range = `${shellQuote2(req.fromRef)} ${shellQuote2(req.toRef)}`;
2021
+ else if (req.fromRef) range = `${shellQuote2(req.fromRef)}`;
2022
+ else if (req.staged) range = "--cached";
2023
+ const pathspec = req.pathspec.length ? ` -- ${req.pathspec.map(shellQuote2).join(" ")}` : "";
2024
+ const numstat = await this.run({ cmd: `git -c core.quotePath=false diff --no-color -z --numstat ${range}${pathspec}`.trim(), workdir: repo });
2025
+ const stats = parseNumstatZ(numstat.stdout);
2026
+ const files = [];
2027
+ for (const stat of stats) {
2028
+ const target = stat.newPath;
2029
+ const fileStatus = stat.binary ? "modified" : "modified";
2030
+ if (stat.binary) {
2031
+ files.push({
2032
+ path: target,
2033
+ oldPath: stat.oldPath,
2034
+ status: fileStatus,
2035
+ isBinary: true,
2036
+ isImage: isImagePath(target),
2037
+ additions: 0,
2038
+ deletions: 0,
2039
+ hunks: [],
2040
+ truncated: false
2041
+ });
2042
+ continue;
2043
+ }
2044
+ const patch = await this.run({
2045
+ cmd: `git -c core.quotePath=false diff --no-color -U${ctx} ${range} -- ${shellQuote2(target)}`.trim(),
2046
+ workdir: repo
2047
+ });
2048
+ const oversized = Buffer.byteLength(patch.stdout, "utf8") > req.maxBytesPerFile;
2049
+ const parsed = oversized ? { hunks: [], status: "modified" } : parseUnifiedPatch(patch.stdout);
2050
+ files.push({
2051
+ path: target,
2052
+ oldPath: stat.oldPath,
2053
+ status: parsed.status,
2054
+ isBinary: false,
2055
+ isImage: isImagePath(target),
2056
+ additions: stat.additions,
2057
+ deletions: stat.deletions,
2058
+ hunks: parsed.hunks,
2059
+ truncated: oversized
2060
+ });
2061
+ }
2062
+ return { files, revision: this.revision };
2063
+ }
2064
+ async gitLog(req) {
2065
+ const repo = this.repoWorkdir(req.path);
2066
+ const fmt = `%H${US}%h${US}%P${US}%an${US}%ae${US}%at${US}%cn${US}%ce${US}%ct${US}%s${US}%b${RS}`;
2067
+ const pathspec = req.pathspec.length ? ` -- ${req.pathspec.map(shellQuote2).join(" ")}` : "";
2068
+ const { stdout, exitCode } = await this.run({
2069
+ cmd: `git log --format=${shellQuote2(fmt)} -n${req.maxCount + 1} --skip=${req.skip} ${shellQuote2(req.ref)}${pathspec}`,
2070
+ workdir: repo
2071
+ });
2072
+ if (exitCode !== null && exitCode !== 0) {
2073
+ return { commits: [], hasMore: false };
2074
+ }
2075
+ const records = stdout.split(RS).map((r) => r.replace(/^\n/, "")).filter((r) => r.trim().length > 0);
2076
+ const commits = [];
2077
+ for (const rec of records.slice(0, req.maxCount)) {
2078
+ const f = rec.split(US);
2079
+ if (f.length < 11) continue;
2080
+ commits.push({
2081
+ sha: f[0],
2082
+ shortSha: f[1],
2083
+ parents: (f[2] ?? "").trim() ? f[2].trim().split(" ") : [],
2084
+ author: { name: f[3], email: f[4], timestamp: safeInt(f[5]) ?? 0 },
2085
+ committer: { name: f[6], email: f[7], timestamp: safeInt(f[8]) ?? 0 },
2086
+ subject: f[9],
2087
+ body: f.slice(10).join(US),
2088
+ refs: []
2089
+ });
2090
+ }
2091
+ return { commits, hasMore: records.length > req.maxCount };
2092
+ }
2093
+ async gitShow(req) {
2094
+ const repo = this.repoWorkdir(req.path);
2095
+ if (req.filePath) {
2096
+ const { stdout, exitCode } = await this.run({
2097
+ cmd: `git cat-file blob ${shellQuote2(`${req.ref}:${req.filePath}`)} 2>/dev/null | base64`,
2098
+ workdir: repo
2099
+ });
2100
+ if (exitCode !== null && exitCode !== 0 && stdout.trim() === "") {
2101
+ throw new ChannelANotFoundError(`blob not found: ${req.ref}:${req.filePath}`);
2102
+ }
2103
+ const bytes = Buffer.from(stdout.replace(/\n/g, ""), "base64");
2104
+ const truncated = bytes.byteLength > req.maxBytesPerFile;
2105
+ const clamped = truncated ? bytes.subarray(0, req.maxBytesPerFile) : bytes;
2106
+ const isBinary = sniffBinary(clamped);
2107
+ const encoding = req.encoding === "base64" || isBinary ? "base64" : "utf8";
2108
+ return {
2109
+ commit: null,
2110
+ files: [],
2111
+ blob: { content: encoding === "base64" ? clamped.toString("base64") : clamped.toString("utf8"), encoding, sizeBytes: clamped.byteLength, truncated },
2112
+ revision: this.revision
2113
+ };
2114
+ }
2115
+ const log = await this.gitLog({ path: req.path, ref: req.ref, maxCount: 1, skip: 0, pathspec: [] });
2116
+ const commit = log.commits[0] ?? null;
2117
+ const diff = await this.gitDiff({ path: req.path, staged: false, fromRef: `${req.ref}^`, toRef: req.ref, pathspec: [], contextLines: 3, maxBytesPerFile: req.maxBytesPerFile });
2118
+ return { commit, files: diff.files, blob: null, revision: this.revision };
2119
+ }
2120
+ /** Detect repo roots within the workspace (for the Git.repos capability). */
2121
+ async detectRepos() {
2122
+ try {
2123
+ const { stdout } = await this.run({ cmd: `find . -maxdepth 3 -name .git -type d 2>/dev/null`, workdir: this.workspaceRoot || void 0 });
2124
+ return stdout.split("\n").map((l) => l.trim()).filter(Boolean).map((g) => dirnameAbs(stripDotSlash(g, "")) || "");
2125
+ } catch {
2126
+ return [];
2127
+ }
2128
+ }
2129
+ // ════════════════════════ Terminal exec + PTY (A2) ════════════════════════
2130
+ /** Run a bounded command, return buffered stdout/stderr + exit code inline. The
2131
+ * long-running tail (when the process hasn't exited within timeoutMs) keeps
2132
+ * running in-box; if emitStream is set the buffered output is also published as
2133
+ * the agent firehose so other viewers see it. */
2134
+ async terminalExec(req) {
2135
+ const r = await this.run({
2136
+ cmd: req.command,
2137
+ workdir: this.repoWorkdir(req.cwd),
2138
+ yieldTimeMs: req.timeoutMs
2139
+ });
2140
+ const running = r.exitCode === null && typeof r.sessionId === "number";
2141
+ if (req.emitStream && (r.stdout || r.stderr)) {
2142
+ const events = [];
2143
+ const commandId = crypto.randomUUID();
2144
+ if (r.stdout) events.push({ type: "sandbox.command.output.delta", payload: { stream: "stdout", chunk: r.stdout, commandId, seq: 0 } });
2145
+ if (r.stderr) events.push({ type: "sandbox.command.output.delta", payload: { stream: "stderr", chunk: r.stderr, commandId, seq: 1 } });
2146
+ await this.emitEvents(events);
2147
+ }
2148
+ return {
2149
+ stdout: r.stdout,
2150
+ stderr: r.stderr,
2151
+ exitCode: r.exitCode,
2152
+ running,
2153
+ wallTimeSeconds: r.wallTimeSeconds
2154
+ };
2155
+ }
2156
+ /** Open an interactive PTY: exec the shell with tty:true, yielding the numeric
2157
+ * exec-session id the caller persists (ptyId<->execSessionId) so subsequent
2158
+ * writeStdin can drive it. Returns the supportsInput gate (false when the
2159
+ * backend has no writeStdin). The caller emits terminal.pty.started after it
2160
+ * persists the row. */
2161
+ async ptyOpen(req, ptyId) {
2162
+ const supportsInput = Boolean(this.session.supportsPty?.() && this.session.writeStdin);
2163
+ const shell = req.shell ?? "/bin/bash";
2164
+ const r = await this.run({
2165
+ cmd: shell,
2166
+ workdir: this.repoWorkdir(req.cwd),
2167
+ tty: true,
2168
+ login: true,
2169
+ yieldTimeMs: 250
2170
+ });
2171
+ return {
2172
+ response: { ptyId, streamVia: "sse-events", supportsInput },
2173
+ execSessionId: typeof r.sessionId === "number" ? r.sessionId : null,
2174
+ shell,
2175
+ initialOutput: r.stdout
2176
+ };
2177
+ }
2178
+ /** Drive an open PTY's stdin. Returns the drained output (the caller publishes
2179
+ * it as terminal.pty.output.delta). Throws ChannelAUnsupportedError when the
2180
+ * backend has no writeStdin. */
2181
+ async ptyWrite(_req, execSessionId, data) {
2182
+ if (!this.session.writeStdin) {
2183
+ throw new ChannelAUnsupportedError("interactive terminal unsupported on this backend");
2184
+ }
2185
+ const out = await this.session.writeStdin({ sessionId: execSessionId, chars: data, yieldTimeMs: 250 });
2186
+ if (isExecSessionLostBanner(out, execSessionId)) {
2187
+ throw new ChannelAConflictError("pty session lost on the live box; reopen the terminal");
2188
+ }
2189
+ return stripExecBanner2(out);
2190
+ }
2191
+ /** Resize an open PTY (SIGWINCH via stty against the exec-session). The SDK has
2192
+ * no resize method; stty in the same tty session updates the geometry. */
2193
+ async ptyResize(req, execSessionId) {
2194
+ if (!this.session.writeStdin) return;
2195
+ await this.session.writeStdin({ sessionId: execSessionId, chars: `stty cols ${req.cols} rows ${req.rows}
2196
+ `, yieldTimeMs: 50 });
2197
+ }
2198
+ /** Close an open PTY: write exit/EOF. The caller marks the row closed + emits
2199
+ * terminal.pty.exited. */
2200
+ async ptyClose(_req, execSessionId) {
2201
+ if (execSessionId !== null && this.session.writeStdin) {
2202
+ try {
2203
+ await this.session.writeStdin({ sessionId: execSessionId, chars: "", yieldTimeMs: 50 });
2204
+ } catch {
2205
+ }
2206
+ }
2207
+ }
2208
+ // ──────────────────────────── helpers ──────────────────────────────────────
2209
+ /** The current FS revision (for the caller to persist/seed). */
2210
+ currentRevision() {
2211
+ return this.revision;
2212
+ }
2213
+ joinRoot(rel) {
2214
+ if (!this.workspaceRoot) return rel === "" ? "." : rel;
2215
+ return rel === "" ? this.workspaceRoot : `${this.workspaceRoot}/${rel}`;
2216
+ }
2217
+ repoWorkdir(rel) {
2218
+ const safe = normalizeRelPath(rel);
2219
+ const joined = this.joinRoot(safe);
2220
+ return joined === "." ? this.workspaceRoot || void 0 : joined;
2221
+ }
2222
+ async emitEvents(events) {
2223
+ if (!this.emit || events.length === 0) return;
2224
+ try {
2225
+ await this.emit(events);
2226
+ } catch {
2227
+ }
2228
+ }
2229
+ async emitFsChanged(changes, source) {
2230
+ const payload = { changes, source, revision: this.revision, leaseEpoch: this.leaseEpoch };
2231
+ await this.emitEvents([{ type: "fs.changed", payload }]);
2232
+ }
2233
+ /** Re-probe git after a mutation and emit git.changed (best-effort, used by the
2234
+ * worker agent-turn side after FS-mutating tools). */
2235
+ async emitGitChanged(repoPath, reason) {
2236
+ try {
2237
+ const status = await this.gitStatus({ path: repoPath });
2238
+ const payload = {
2239
+ head: status.head,
2240
+ dirty: status.files.length > 0,
2241
+ ahead: status.ahead,
2242
+ behind: status.behind,
2243
+ changedFileCount: status.files.length,
2244
+ reason,
2245
+ revision: this.revision,
2246
+ leaseEpoch: this.leaseEpoch
2247
+ };
2248
+ await this.emitEvents([{ type: "git.changed", payload }]);
2249
+ } catch {
2250
+ }
2251
+ }
2252
+ };
2253
+ function stripExecBanner2(raw) {
2254
+ const marker = raw.indexOf("\nOutput:\n");
2255
+ if (marker >= 0) return raw.slice(marker + "\nOutput:\n".length);
2256
+ if (raw.startsWith("Output:\n")) return raw.slice("Output:\n".length);
2257
+ return raw;
2258
+ }
2259
+ function isWorkspaceEscapeError(error) {
2260
+ const msg = error instanceof Error ? error.message : String(error ?? "");
2261
+ const lower = msg.toLowerCase();
2262
+ return lower.includes("workspace escape") || lower.includes("remote validation") && lower.includes("escape");
2263
+ }
2264
+ function isExecSessionLostBanner(out, execSessionId) {
2265
+ if (!out) return false;
2266
+ const lower = out.toLowerCase();
2267
+ if (!lower.includes("session not found")) return false;
2268
+ return lower.includes(`session not found: ${execSessionId}`) || !/session not found:\s*\d+/.test(lower);
2269
+ }
2270
+ function parseExecBannerSessionId(raw) {
2271
+ const outputIdx = raw.indexOf("\nOutput:\n");
2272
+ const banner = outputIdx >= 0 ? raw.slice(0, outputIdx) : raw.startsWith("Output:\n") ? "" : raw;
2273
+ const match = banner.match(/Process running with session ID (\d+)/);
2274
+ if (!match) return null;
2275
+ const n = Number.parseInt(match[1], 10);
2276
+ return Number.isFinite(n) ? n : null;
2277
+ }
2278
+ function sniffBinary(bytes) {
2279
+ const n = Math.min(bytes.byteLength, 8192);
2280
+ for (let i = 0; i < n; i++) if (bytes[i] === 0) return true;
2281
+ return false;
2282
+ }
2283
+ function normalizeRelPath(p) {
2284
+ const trimmed = (p ?? "").replace(/^\/+/, "").replace(/\/+$/, "");
2285
+ return trimmed;
2286
+ }
2287
+ function assertSafeRelPath(p) {
2288
+ const norm = normalizeRelPath(p);
2289
+ if (norm === "") throw new ChannelAValidationError("path is required");
2290
+ if (p.startsWith("/")) throw new ChannelAValidationError(`absolute paths are not allowed: ${p}`);
2291
+ if (norm.split("/").some((seg) => seg === "..")) throw new ChannelAValidationError(`path traversal is not allowed: ${p}`);
2292
+ return norm;
2293
+ }
2294
+ function shellQuote2(s) {
2295
+ return `'${s.replace(/'/g, `'\\''`)}'`;
2296
+ }
2297
+ function basename(p) {
2298
+ const parts = p.split("/").filter(Boolean);
2299
+ return parts.length ? parts[parts.length - 1] : "";
2300
+ }
2301
+ function dirnameAbs(p) {
2302
+ const idx = p.lastIndexOf("/");
2303
+ return idx > 0 ? p.slice(0, idx) : "";
2304
+ }
2305
+ function dirnameRel(p, root) {
2306
+ const idx = p.lastIndexOf("/");
2307
+ if (idx < 0) return root;
2308
+ return p.slice(0, idx);
2309
+ }
2310
+ function stripDotSlash(rawPath, root) {
2311
+ let p = rawPath.startsWith("./") ? rawPath.slice(2) : rawPath;
2312
+ p = p.replace(/^\/+/, "");
2313
+ if (root && !p.startsWith(`${root}/`) && p !== root) {
2314
+ return root ? `${root}/${p}` : p;
2315
+ }
2316
+ return p;
2317
+ }
2318
+ function findTypeToNode(t) {
2319
+ if (t === "d") return "dir";
2320
+ if (t === "f") return "file";
2321
+ if (t === "l") return "symlink";
2322
+ return "other";
2323
+ }
2324
+ function safeInt(s) {
2325
+ if (s === void 0) return null;
2326
+ const n = Number.parseInt(s, 10);
2327
+ return Number.isFinite(n) ? n : null;
2328
+ }
2329
+ function safeOctal(s) {
2330
+ if (s === void 0) return null;
2331
+ const n = Number.parseInt(s, 8);
2332
+ return Number.isFinite(n) ? n : null;
2333
+ }
2334
+ function mtimeToMs(s) {
2335
+ if (s === void 0) return null;
2336
+ const f = Number.parseFloat(s);
2337
+ return Number.isFinite(f) ? Math.round(f * 1e3) : null;
2338
+ }
2339
+ function sortTree(node) {
2340
+ if (!node.children) return;
2341
+ node.children.sort((a, b) => {
2342
+ if (a.type === "dir" && b.type !== "dir") return -1;
2343
+ if (a.type !== "dir" && b.type === "dir") return 1;
2344
+ return a.name.localeCompare(b.name);
2345
+ });
2346
+ for (const child of node.children) sortTree(child);
2347
+ }
2348
+ function isImagePath(p) {
2349
+ return /\.(png|jpe?g|gif|webp|bmp|ico|svg|tiff?)$/i.test(p);
2350
+ }
2351
+ function parsePorcelainV2(z) {
2352
+ const records = z.split(NUL);
2353
+ let head = null;
2354
+ let upstream = null;
2355
+ let detached = false;
2356
+ let ahead = 0;
2357
+ let behind = 0;
2358
+ const files = [];
2359
+ for (let i = 0; i < records.length; i++) {
2360
+ const rec = records[i];
2361
+ if (rec === "") continue;
2362
+ if (rec.startsWith("# branch.head ")) {
2363
+ const v = rec.slice("# branch.head ".length);
2364
+ if (v === "(detached)") {
2365
+ detached = true;
2366
+ head = null;
2367
+ } else head = v;
2368
+ } else if (rec.startsWith("# branch.upstream ")) {
2369
+ upstream = rec.slice("# branch.upstream ".length);
2370
+ } else if (rec.startsWith("# branch.ab ")) {
2371
+ const m = rec.slice("# branch.ab ".length).match(/\+(\d+)\s+-(\d+)/);
2372
+ if (m) {
2373
+ ahead = Number(m[1]);
2374
+ behind = Number(m[2]);
2375
+ }
2376
+ } else if (rec.startsWith("1 ")) {
2377
+ const fields = rec.split(" ");
2378
+ const xy = fields[1] ?? "..";
2379
+ const path = fields.slice(8).join(" ");
2380
+ files.push(statusFromXY(xy, path, null));
2381
+ } else if (rec.startsWith("2 ")) {
2382
+ const fields = rec.split(" ");
2383
+ const xy = fields[1] ?? "..";
2384
+ const path = fields.slice(9).join(" ");
2385
+ const oldPath = records[i + 1] ?? null;
2386
+ i++;
2387
+ files.push(statusFromXY(xy, path, oldPath));
2388
+ } else if (rec.startsWith("u ")) {
2389
+ const fields = rec.split(" ");
2390
+ const path = fields.slice(10).join(" ");
2391
+ files.push({ path, oldPath: null, index: "conflicted", worktree: "conflicted", isConflicted: true });
2392
+ } else if (rec.startsWith("? ")) {
2393
+ files.push({ path: rec.slice(2), oldPath: null, index: null, worktree: "untracked", isConflicted: false });
2394
+ } else if (rec.startsWith("! ")) {
2395
+ files.push({ path: rec.slice(2), oldPath: null, index: null, worktree: "ignored", isConflicted: false });
2396
+ }
2397
+ }
2398
+ return { isRepo: true, head, detached, upstream, ahead, behind, files };
2399
+ }
2400
+ function xyCode(c) {
2401
+ switch (c) {
2402
+ case "A":
2403
+ return "added";
2404
+ case "M":
2405
+ return "modified";
2406
+ case "D":
2407
+ return "deleted";
2408
+ case "R":
2409
+ return "renamed";
2410
+ case "C":
2411
+ return "copied";
2412
+ case "T":
2413
+ return "typechange";
2414
+ case "U":
2415
+ return "conflicted";
2416
+ case ".":
2417
+ return null;
2418
+ default:
2419
+ return null;
2420
+ }
2421
+ }
2422
+ function statusFromXY(xy, path, oldPath) {
2423
+ const x = xy[0] ?? ".";
2424
+ const y = xy[1] ?? ".";
2425
+ return {
2426
+ path,
2427
+ oldPath,
2428
+ index: xyCode(x),
2429
+ worktree: xyCode(y),
2430
+ isConflicted: x === "U" || y === "U"
2431
+ };
2432
+ }
2433
+ function parseNumstatZ(z) {
2434
+ const fields = z.split(NUL);
2435
+ const out = [];
2436
+ let i = 0;
2437
+ while (i < fields.length) {
2438
+ const head = fields[i];
2439
+ if (head === "") {
2440
+ i++;
2441
+ continue;
2442
+ }
2443
+ const m = head.match(/^(\d+|-)\t(\d+|-)\t(.*)$/s);
2444
+ if (!m) {
2445
+ i++;
2446
+ continue;
2447
+ }
2448
+ const addStr = m[1];
2449
+ const delStr = m[2];
2450
+ const pathPart = m[3];
2451
+ const binary = addStr === "-" && delStr === "-";
2452
+ if (pathPart === "") {
2453
+ const oldPath = fields[i + 1] ?? null;
2454
+ const newPath = fields[i + 2] ?? "";
2455
+ out.push({ additions: binary ? 0 : Number(addStr), deletions: binary ? 0 : Number(delStr), binary, oldPath, newPath });
2456
+ i += 3;
2457
+ } else {
2458
+ out.push({ additions: binary ? 0 : Number(addStr), deletions: binary ? 0 : Number(delStr), binary, oldPath: null, newPath: pathPart });
2459
+ i++;
2460
+ }
2461
+ }
2462
+ return out;
2463
+ }
2464
+ function parseUnifiedPatch(patch) {
2465
+ const lines = patch.split("\n");
2466
+ const hunks = [];
2467
+ let status = "modified";
2468
+ let current = null;
2469
+ let oldNo = 0;
2470
+ let newNo = 0;
2471
+ for (const line of lines) {
2472
+ if (line.startsWith("new file mode")) status = "added";
2473
+ else if (line.startsWith("deleted file mode")) status = "deleted";
2474
+ else if (line.startsWith("rename from") || line.startsWith("rename to")) status = "renamed";
2475
+ if (line.startsWith("@@")) {
2476
+ const m = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/);
2477
+ if (m) {
2478
+ const oldStart = Number(m[1]);
2479
+ const oldLines = m[2] !== void 0 ? Number(m[2]) : 1;
2480
+ const newStart = Number(m[3]);
2481
+ const newLines = m[4] !== void 0 ? Number(m[4]) : 1;
2482
+ current = { oldStart, oldLines, newStart, newLines, header: (m[5] ?? "").trim(), lines: [] };
2483
+ hunks.push(current);
2484
+ oldNo = oldStart;
2485
+ newNo = newStart;
2486
+ }
2487
+ continue;
2488
+ }
2489
+ if (!current) continue;
2490
+ if (line.startsWith("\\")) continue;
2491
+ const marker = line[0];
2492
+ const text = line.slice(1);
2493
+ if (marker === "+") {
2494
+ current.lines.push({ type: "add", oldNo: null, newNo, text });
2495
+ newNo++;
2496
+ } else if (marker === "-") {
2497
+ current.lines.push({ type: "del", oldNo, newNo: null, text });
2498
+ oldNo++;
2499
+ } else if (marker === " ") {
2500
+ current.lines.push({ type: "context", oldNo, newNo, text });
2501
+ oldNo++;
2502
+ newNo++;
2503
+ }
2504
+ }
2505
+ return { hunks, status };
2506
+ }
2507
+
2508
+ // src/sandbox/selfhosted/capabilities.ts
2509
+ var SELFHOSTED_RECONNECT_WINDOW_MS = 3e4;
2510
+ function selfhostedLiveness(input) {
2511
+ const { enrollment } = input;
2512
+ if (!enrollment || enrollment.status !== "active") {
2513
+ return { state: "offline", consented: false, hasDisplay: false };
2514
+ }
2515
+ const consented = enrollment.exposure === "whole-machine" && enrollment.allowScreenControl;
2516
+ const hasDisplay = enrollment.hasDisplay;
2517
+ if (input.probeResponded) {
2518
+ return { state: "online", consented, hasDisplay };
2519
+ }
2520
+ const now = (input.now ?? /* @__PURE__ */ new Date()).getTime();
2521
+ const lastSeen = enrollment.lastSeenAt ? new Date(enrollment.lastSeenAt).getTime() : null;
2522
+ if (lastSeen !== null && now - lastSeen <= SELFHOSTED_RECONNECT_WINDOW_MS) {
2523
+ return { state: "reconnecting", consented, hasDisplay };
2524
+ }
2525
+ return { state: "offline", consented, hasDisplay };
2526
+ }
2527
+ async function negotiateSelfhostedCapabilities(input) {
2528
+ const probeResponded = input.probeResponded ?? (input.session ? await input.session.ping() : false);
2529
+ const liveness = selfhostedLiveness({
2530
+ enrollment: input.enrollment,
2531
+ probeResponded,
2532
+ ...input.now ? { now: input.now } : {}
2533
+ });
2534
+ const baseLiveness = liveness.state === "online" ? "warm" : "cold";
2535
+ const base = {
2536
+ sessionId: input.sessionId,
2537
+ backend: "selfhosted",
2538
+ os: input.os ?? "linux",
2539
+ liveness: baseLiveness,
2540
+ leaseEpoch: input.leaseEpoch,
2541
+ desktopEnabled: input.desktopEnabled ?? true,
2542
+ terminalEnabled: input.terminalEnabled ?? true,
2543
+ computerUseEnabled: input.computerUseEnabled ?? true,
2544
+ ...input.desktopAcknowledged !== void 0 ? { desktopAcknowledged: input.desktopAcknowledged } : {},
2545
+ ...input.shared !== void 0 ? { shared: input.shared } : {},
2546
+ ...input.sharedSessionIds !== void 0 ? { sharedSessionIds: input.sharedSessionIds } : {},
2547
+ ...input.now ? { now: input.now } : {}
2548
+ };
2549
+ const caps = negotiateCapabilities(base);
2550
+ if (liveness.state !== "online") {
2551
+ const reason = liveness.state === "offline" ? "agent_offline" : "agent_reconnecting";
2552
+ return {
2553
+ ...caps,
2554
+ FileSystem: { ...caps.FileSystem, available: false, readOnly: true, reason },
2555
+ Terminal: { ...caps.Terminal, transport: null, url: null, token: null, expiresAt: null, reason },
2556
+ Git: { ...caps.Git, available: false, reason },
2557
+ DesktopStream: {
2558
+ ...caps.DesktopStream,
2559
+ transport: null,
2560
+ client: null,
2561
+ mode: "read-only",
2562
+ url: null,
2563
+ token: null,
2564
+ expiresAt: null,
2565
+ requiresAcknowledgment: false,
2566
+ acknowledged: false,
2567
+ shared: false,
2568
+ sharedSessionIds: [],
2569
+ reason
2570
+ },
2571
+ Recording: { ...caps.Recording, available: false, modes: [], codecs: [], reason },
2572
+ ComputerUse: { ...caps.ComputerUse, available: false, reason }
2573
+ };
2574
+ }
2575
+ if (!liveness.hasDisplay) {
2576
+ const reason = "display_unavailable";
2577
+ return {
2578
+ ...caps,
2579
+ DesktopStream: caps.DesktopStream.transport !== null ? {
2580
+ ...caps.DesktopStream,
2581
+ transport: null,
2582
+ client: null,
2583
+ mode: "read-only",
2584
+ url: null,
2585
+ token: null,
2586
+ expiresAt: null,
2587
+ requiresAcknowledgment: false,
2588
+ acknowledged: false,
2589
+ shared: false,
2590
+ sharedSessionIds: [],
2591
+ reason
2592
+ } : caps.DesktopStream,
2593
+ Recording: caps.Recording.available ? { ...caps.Recording, available: false, modes: [], codecs: [], reason } : caps.Recording,
2594
+ ComputerUse: caps.ComputerUse.available ? { ...caps.ComputerUse, available: false, reason } : caps.ComputerUse
2595
+ };
2596
+ }
2597
+ if (!liveness.consented) {
2598
+ return {
2599
+ ...caps,
2600
+ DesktopStream: caps.DesktopStream.transport !== null ? { ...caps.DesktopStream, mode: "read-only" } : caps.DesktopStream,
2601
+ ComputerUse: caps.ComputerUse.available ? { ...caps.ComputerUse, available: false, reason: "consent_required" } : caps.ComputerUse
2602
+ };
2603
+ }
2604
+ return caps;
2605
+ }
2606
+
2607
+ // src/sandbox/selfhosted/testing.ts
2608
+ import {
2609
+ ErrorCode as ErrorCode2,
2610
+ FsEntryKind as FsEntryKind2
2611
+ } from "@opengeni/agent-proto";
2612
+ var encoder2 = new TextEncoder();
2613
+ var decoder2 = new TextDecoder();
2614
+ var MockAgentResponder = class {
2615
+ online;
2616
+ consented;
2617
+ draining;
2618
+ files = /* @__PURE__ */ new Map();
2619
+ execHandler;
2620
+ hostname;
2621
+ /** Every request seen, for assertion (subject + decoded ControlRequest). */
2622
+ requests = [];
2623
+ constructor(opts = {}) {
2624
+ this.online = opts.online ?? true;
2625
+ this.consented = opts.consented ?? true;
2626
+ this.draining = opts.draining ?? false;
2627
+ this.hostname = opts.hostname ?? "mock-machine";
2628
+ this.execHandler = opts.exec ?? ((req) => defaultEcho(req, this.hostname));
2629
+ for (const [path, content] of Object.entries(opts.files ?? {})) {
2630
+ this.files.set(normalize(path), typeof content === "string" ? encoder2.encode(content) : content);
2631
+ }
2632
+ }
2633
+ /** Flip the responder offline mid-test (a deliberate stop / blip). */
2634
+ setOnline(online) {
2635
+ this.online = online;
2636
+ }
2637
+ /** Read a file the session wrote (test assertion helper). */
2638
+ fileText(path) {
2639
+ const bytes = this.files.get(normalize(path));
2640
+ return bytes ? decoder2.decode(bytes) : void 0;
2641
+ }
2642
+ async request(subject, req, _opts) {
2643
+ this.requests.push({ subject, req });
2644
+ if (!this.online) {
2645
+ return errorResponse(req.requestId, ErrorCode2.ERROR_CODE_AGENT_OFFLINE, "the enrolled agent is offline", false);
2646
+ }
2647
+ if (this.draining) {
2648
+ return errorResponse(req.requestId, ErrorCode2.ERROR_CODE_DRAINING, "the agent is draining", true);
2649
+ }
2650
+ const op = req.op;
2651
+ if (!op) {
2652
+ return errorResponse(req.requestId, ErrorCode2.ERROR_CODE_PROTOCOL, "empty op", false);
2653
+ }
2654
+ switch (op.$case) {
2655
+ case "ping":
2656
+ return ok(req.requestId, { $case: "ping", ping: { nonce: op.ping.nonce, agentMonotonicMs: "0" } });
2657
+ case "exec": {
2658
+ const res = await this.execHandler(op.exec);
2659
+ return ok(req.requestId, { $case: "exec", exec: res });
2660
+ }
2661
+ case "fsRead": {
2662
+ const bytes = this.files.get(normalize(op.fsRead.path));
2663
+ if (!bytes) {
2664
+ return errorResponse(req.requestId, ErrorCode2.ERROR_CODE_NOT_FOUND, `no such file: ${op.fsRead.path}`, false);
2665
+ }
2666
+ const res = { content: bytes, totalSize: String(bytes.length) };
2667
+ return ok(req.requestId, { $case: "fsRead", fsRead: res });
2668
+ }
2669
+ case "fsWrite": {
2670
+ const path = normalize(op.fsWrite.path);
2671
+ const next = op.fsWrite.append ? concat(this.files.get(path) ?? new Uint8Array(0), op.fsWrite.content) : op.fsWrite.content;
2672
+ this.files.set(path, next);
2673
+ const res = { bytesWritten: String(op.fsWrite.content.length) };
2674
+ return ok(req.requestId, { $case: "fsWrite", fsWrite: res });
2675
+ }
2676
+ case "fsList": {
2677
+ const prefix = normalize(op.fsList.path).replace(/\/?$/, "/");
2678
+ const res = {
2679
+ entries: [...this.files.keys()].filter((p) => p.startsWith(prefix)).map((p) => {
2680
+ const bytes = this.files.get(p);
2681
+ const rel = p.slice(prefix.length);
2682
+ return {
2683
+ name: rel.split("/").pop() ?? rel,
2684
+ path: rel,
2685
+ kind: FsEntryKind2.FS_ENTRY_KIND_FILE,
2686
+ size: String(bytes.length),
2687
+ modifiedMs: "0",
2688
+ mode: 420
2689
+ };
2690
+ })
2691
+ };
2692
+ return ok(req.requestId, { $case: "fsList", fsList: res });
2693
+ }
2694
+ case "fsStat": {
2695
+ const bytes = this.files.get(normalize(op.fsStat.path));
2696
+ const res = bytes ? {
2697
+ exists: true,
2698
+ entry: {
2699
+ name: normalize(op.fsStat.path).split("/").pop() ?? "",
2700
+ path: op.fsStat.path,
2701
+ kind: FsEntryKind2.FS_ENTRY_KIND_FILE,
2702
+ size: String(bytes.length),
2703
+ modifiedMs: "0",
2704
+ mode: 420
2705
+ }
2706
+ } : { exists: false, entry: void 0 };
2707
+ return ok(req.requestId, { $case: "fsStat", fsStat: res });
2708
+ }
2709
+ case "desktopEnsure": {
2710
+ return ok(req.requestId, {
2711
+ $case: "desktopEnsure",
2712
+ desktopEnsure: {
2713
+ channel: { channelId: "mock-desktop", workspaceId: "", agentId: "", kind: 1, port: 6080 },
2714
+ display: { id: ":99", width: 1024, height: 768, virtual: true }
2715
+ }
2716
+ });
2717
+ }
2718
+ case "ptyOpen": {
2719
+ return ok(req.requestId, {
2720
+ $case: "ptyOpen",
2721
+ ptyOpen: {
2722
+ ptyId: "mock-pty",
2723
+ channel: { channelId: "mock-pty", workspaceId: "", agentId: "", kind: 1, port: 7681 }
2724
+ }
2725
+ });
2726
+ }
2727
+ default:
2728
+ return errorResponse(req.requestId, ErrorCode2.ERROR_CODE_UNSUPPORTED, `mock does not implement ${op.$case}`, false);
2729
+ }
2730
+ }
2731
+ };
2732
+ function defaultEcho(req, hostname) {
2733
+ const joined = req.command.join(" ");
2734
+ const stdout = /hostname|HOSTNAME/.test(joined) ? hostname : joined;
2735
+ return {
2736
+ exitCode: 0,
2737
+ stdout: encoder2.encode(`${stdout}
2738
+ `),
2739
+ stderr: new Uint8Array(0),
2740
+ timedOut: false,
2741
+ durationMs: "1"
2742
+ };
2743
+ }
2744
+ function ok(requestId, result) {
2745
+ return { requestId, error: void 0, result };
2746
+ }
2747
+ function errorResponse(requestId, code, message, retryable) {
2748
+ const error = { code, message, retryable, detail: {} };
2749
+ return { requestId, error, result: void 0 };
2750
+ }
2751
+ function normalize(path) {
2752
+ const trimmed = path.replace(/\/+$/, "");
2753
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
2754
+ }
2755
+ function concat(a, b) {
2756
+ const out = new Uint8Array(a.length + b.length);
2757
+ out.set(a, 0);
2758
+ out.set(b, a.length);
2759
+ return out;
2760
+ }
2761
+
2762
+ // src/sandbox/routing/routing-session.ts
2763
+ var RoutingUnsupportedError = class extends Error {
2764
+ name = "RoutingUnsupportedError";
2765
+ constructor(op, kind) {
2766
+ super(`the active sandbox (${kind}) does not support "${op}"`);
2767
+ }
2768
+ };
2769
+ function isFenceError(error) {
2770
+ if (!error || typeof error !== "object") {
2771
+ return false;
2772
+ }
2773
+ if (error.fenced === true) {
2774
+ return true;
2775
+ }
2776
+ const name = typeof error.name === "string" ? error.name : "";
2777
+ const message = error instanceof Error ? error.message : String(error.message ?? "");
2778
+ const haystack = `${name} ${message}`.toLowerCase();
2779
+ return haystack.includes("fenced") || haystack.includes("epoch") && haystack.includes("super");
2780
+ }
2781
+ var RoutingSandboxSession = class {
2782
+ deps;
2783
+ maxFenceRetries;
2784
+ // The per-epoch resolved-backend cache. Keyed by activeEpoch: a swap bumps the
2785
+ // epoch, invalidating the cache so the NEXT op re-resolves the new backend.
2786
+ cachedEpoch;
2787
+ cached;
2788
+ // The last-resolved backend, exposed via the `state` getter (a method-free read
2789
+ // of the active backend's `state`). Updated on every resolve.
2790
+ lastResolved;
2791
+ // The native-desktop control-plane ops (self-hosted / macOS). Declared as OPTIONAL
2792
+ // INSTANCE fields — NOT prototype methods — because their PRESENCE is the selection
2793
+ // signal `isNativeDesktopSession` (sandbox-computer.ts) uses to pick the native vs
2794
+ // exec-shelling Computer. If they were unconditional prototype methods, this proxy
2795
+ // would ALWAYS duck-type as native — misclassifying a Modal-fronting proxy (whose
2796
+ // real backend has no native surface) and driving CGEvent/screenshot ops at a box
2797
+ // that cannot serve them. So the constructor assigns them ONLY when the
2798
+ // construction-time default backend actually implements the native surface (below).
2799
+ desktopInput;
2800
+ screenshot;
2801
+ constructor(deps) {
2802
+ this.deps = deps;
2803
+ this.maxFenceRetries = deps.maxFenceRetries ?? 3;
2804
+ const def = deps.defaultResolved?.session;
2805
+ if (typeof def?.desktopInput === "function" && typeof def?.screenshot === "function") {
2806
+ this.desktopInput = (event) => this.dispatch("desktopInput", async (s) => {
2807
+ if (!s.desktopInput) {
2808
+ throw new RoutingUnsupportedError("desktopInput", this.cached?.kind ?? "unknown");
2809
+ }
2810
+ return s.desktopInput(event);
2811
+ });
2812
+ this.screenshot = () => this.dispatch("screenshot", async (s) => {
2813
+ if (!s.screenshot) {
2814
+ throw new RoutingUnsupportedError("screenshot", this.cached?.kind ?? "unknown");
2815
+ }
2816
+ return s.screenshot();
2817
+ });
2818
+ }
2819
+ }
2820
+ /**
2821
+ * A method-free read of the active backend's `state` (best-effort: the last
2822
+ * resolved backend, falling back to the default backend resolved at construction
2823
+ * so this is non-empty BEFORE the first op). Consumers that read `session.state`
2824
+ * (instanceId/decoration) get the active backend's state.
2825
+ *
2826
+ * CRITICAL: this returns the underlying backend's `state` OBJECT BY REFERENCE
2827
+ * (never a fresh `{}` when a backend exists). The @openai/agents SDK both READS
2828
+ * `session.state.manifest` and WRITES `session.state.manifest = nextManifest`
2829
+ * (providedSessionManifest); returning the live object by reference means those
2830
+ * property writes land on the real backend state and persist. Only when NO
2831
+ * backend has been resolved yet (no default seeded, no op dispatched) do we
2832
+ * return an empty object — and that path no longer occurs in the turn wiring,
2833
+ * which always seeds `defaultResolved`.
2834
+ */
2835
+ get state() {
2836
+ const backendState = (this.lastResolved ?? this.deps.defaultResolved)?.session.state;
2837
+ return backendState ?? {};
2838
+ }
2839
+ /**
2840
+ * Re-read the pointer and resolve the active backend, using the per-epoch cache.
2841
+ * The cache is keyed by `activeEpoch`: if the epoch is unchanged we return the
2842
+ * cached backend; if it moved (a swap) we re-resolve and update the cache. This
2843
+ * is THE per-call re-read that makes a mid-turn swap land on the next op.
2844
+ */
2845
+ async resolve() {
2846
+ const pointer = await this.deps.readPointer();
2847
+ if (this.cachedEpoch === pointer.activeEpoch && this.cached) {
2848
+ return this.cached;
2849
+ }
2850
+ const fromEpoch = this.cachedEpoch ?? pointer.activeEpoch;
2851
+ const resolved = await this.deps.resolveActiveBackend(pointer);
2852
+ this.cachedEpoch = pointer.activeEpoch;
2853
+ this.cached = resolved;
2854
+ this.lastResolved = resolved;
2855
+ this.deps.onTransition?.({
2856
+ type: this.cachedEpoch !== void 0 && fromEpoch !== pointer.activeEpoch ? "epoch-changed" : "resolved",
2857
+ fromEpoch,
2858
+ toEpoch: pointer.activeEpoch,
2859
+ sandboxId: resolved.sandboxId,
2860
+ kind: resolved.kind
2861
+ });
2862
+ return resolved;
2863
+ }
2864
+ /**
2865
+ * Dispatch an op to the currently-active backend, retrying on a stale-epoch
2866
+ * fence. The sequence per attempt:
2867
+ * 1. re-read the pointer + resolve the active backend (cached by epoch),
2868
+ * 2. run `fn(activeSession)`,
2869
+ * 3. on a FENCE error (the pointer moved under us / the backend rejected a
2870
+ * stale epoch), INVALIDATE the cache and retry against the re-resolved
2871
+ * active sandbox — up to `maxFenceRetries`.
2872
+ * A non-fence error propagates immediately (it is a real op failure, not a swap
2873
+ * race).
2874
+ */
2875
+ async dispatch(op, fn) {
2876
+ let attempt = 0;
2877
+ let lastError;
2878
+ while (attempt <= this.maxFenceRetries) {
2879
+ const backend = await this.resolve();
2880
+ try {
2881
+ return await fn(backend.session);
2882
+ } catch (error) {
2883
+ if (!isFenceError(error)) {
2884
+ throw error;
2885
+ }
2886
+ lastError = error;
2887
+ this.cachedEpoch = void 0;
2888
+ this.cached = void 0;
2889
+ this.deps.onTransition?.({
2890
+ type: "fenced-retry",
2891
+ fromEpoch: backend.sandboxId === null ? 0 : 0,
2892
+ toEpoch: 0,
2893
+ sandboxId: backend.sandboxId,
2894
+ kind: backend.kind
2895
+ });
2896
+ attempt += 1;
2897
+ }
2898
+ }
2899
+ throw lastError ?? new Error(`routing op "${op}" exhausted fence retries`);
2900
+ }
2901
+ // ── The forwarded structural surface ──────────────────────────────────────
2902
+ // Every method is PRESENT on the proxy (the SDK binds presence once) and
2903
+ // dispatches to the active backend at call-time. A missing backend method
2904
+ // degrades via the natural fallback or RoutingUnsupportedError.
2905
+ async exec(args) {
2906
+ return this.dispatch("exec", async (s) => {
2907
+ if (s.exec) {
2908
+ return s.exec(args);
2909
+ }
2910
+ if (s.execCommand) {
2911
+ return s.execCommand(args);
2912
+ }
2913
+ throw new RoutingUnsupportedError("exec", this.cached?.kind ?? "unknown");
2914
+ });
2915
+ }
2916
+ async execCommand(args) {
2917
+ return this.dispatch("execCommand", async (s) => {
2918
+ if (s.execCommand) {
2919
+ return s.execCommand(args);
2920
+ }
2921
+ if (s.exec) {
2922
+ const r = await s.exec(args);
2923
+ return r.stdout ?? r.output ?? "";
2924
+ }
2925
+ throw new RoutingUnsupportedError("execCommand", this.cached?.kind ?? "unknown");
2926
+ });
2927
+ }
2928
+ async writeStdin(args) {
2929
+ return this.dispatch("writeStdin", async (s) => {
2930
+ if (!s.writeStdin) {
2931
+ throw new RoutingUnsupportedError("writeStdin", this.cached?.kind ?? "unknown");
2932
+ }
2933
+ return s.writeStdin(args);
2934
+ });
2935
+ }
2936
+ async readFile(args) {
2937
+ return this.dispatch("readFile", async (s) => {
2938
+ if (!s.readFile) {
2939
+ throw new RoutingUnsupportedError("readFile", this.cached?.kind ?? "unknown");
2940
+ }
2941
+ return s.readFile(args);
2942
+ });
2943
+ }
2944
+ async writeFile(args) {
2945
+ return this.dispatch("writeFile", async (s) => {
2946
+ if (!s.writeFile) {
2947
+ throw new RoutingUnsupportedError("writeFile", this.cached?.kind ?? "unknown");
2948
+ }
2949
+ return s.writeFile(args);
2950
+ });
2951
+ }
2952
+ async listDir(args) {
2953
+ return this.dispatch("listDir", async (s) => {
2954
+ if (!s.listDir) {
2955
+ throw new RoutingUnsupportedError("listDir", this.cached?.kind ?? "unknown");
2956
+ }
2957
+ return s.listDir(args);
2958
+ });
2959
+ }
2960
+ async pathExists(path, runAs) {
2961
+ return this.dispatch("pathExists", async (s) => {
2962
+ if (!s.pathExists) {
2963
+ throw new RoutingUnsupportedError("pathExists", this.cached?.kind ?? "unknown");
2964
+ }
2965
+ return s.pathExists(path, runAs);
2966
+ });
2967
+ }
2968
+ async viewImage(args) {
2969
+ return this.dispatch("viewImage", async (s) => {
2970
+ if (!s.viewImage) {
2971
+ throw new RoutingUnsupportedError("viewImage", this.cached?.kind ?? "unknown");
2972
+ }
2973
+ return s.viewImage(args);
2974
+ });
2975
+ }
2976
+ async materializeEntry(args) {
2977
+ return this.dispatch("materializeEntry", async (s) => {
2978
+ if (!s.materializeEntry) {
2979
+ throw new RoutingUnsupportedError("materializeEntry", this.cached?.kind ?? "unknown");
2980
+ }
2981
+ return s.materializeEntry(args);
2982
+ });
2983
+ }
2984
+ /** PTY support reflects the LAST-resolved backend (a synchronous probe; the SDK
2985
+ * reads it to decide if the terminal is interactive). It cannot re-read the
2986
+ * pointer (synchronous), so it answers from the last resolve — coherent with
2987
+ * the resolve the surrounding op already performed. Defaults false before the
2988
+ * first resolve. */
2989
+ supportsPty() {
2990
+ const s = (this.lastResolved ?? this.deps.defaultResolved)?.session;
2991
+ return Boolean(s?.supportsPty?.());
2992
+ }
2993
+ /** createEditor is a synchronous factory in the SDK surface; it binds to the
2994
+ * last-resolved backend's editor (or the default backend before the first op).
2995
+ * Returns undefined when the active backend has no editor (channel-a falls back
2996
+ * to its exec-based write path). */
2997
+ createEditor(runAs) {
2998
+ return (this.lastResolved ?? this.deps.defaultResolved)?.session.createEditor?.(runAs);
2999
+ }
3000
+ async resolveExposedPort(port) {
3001
+ return this.dispatch("resolveExposedPort", async (s) => {
3002
+ if (!s.resolveExposedPort) {
3003
+ throw new RoutingUnsupportedError("resolveExposedPort", this.cached?.kind ?? "unknown");
3004
+ }
3005
+ return s.resolveExposedPort(port);
3006
+ });
3007
+ }
3008
+ /** Serialize the active backend's session state. Used by the resume-by-id seam
3009
+ * to fold the live box onto the lease. Dispatches to the active backend. */
3010
+ async serializeSessionState() {
3011
+ return this.dispatch("serializeSessionState", async (s) => {
3012
+ if (!s.serializeSessionState) {
3013
+ return void 0;
3014
+ }
3015
+ return s.serializeSessionState();
3016
+ });
3017
+ }
3018
+ /** Force a resolve (priming the proxy before the first op so `state`/`supportsPty`
3019
+ * read a real backend). Optional — every op resolves lazily anyway. */
3020
+ async prime() {
3021
+ return this.resolve();
3022
+ }
3023
+ };
3024
+
3025
+ // src/sandbox/routing/backend-resolver.ts
3026
+ var ActiveBackendUnresolvableError = class extends Error {
3027
+ name = "ActiveBackendUnresolvableError";
3028
+ constructor(message) {
3029
+ super(message);
3030
+ }
3031
+ };
3032
+ function makeActiveBackendResolver(deps) {
3033
+ return async (pointer) => {
3034
+ if (pointer.activeSandboxId === null) {
3035
+ return { session: deps.defaultBackend, sandboxId: null, kind: deps.defaultKind };
3036
+ }
3037
+ if (deps.pinnedSelfhosted && pointer.activeSandboxId === deps.pinnedSelfhosted.sandboxId && pointer.activeEpoch === deps.pinnedSelfhosted.epoch) {
3038
+ return { session: deps.pinnedSelfhosted.session, sandboxId: pointer.activeSandboxId, kind: "selfhosted" };
3039
+ }
3040
+ const sandbox = await deps.getSandbox(pointer.activeSandboxId);
3041
+ if (!sandbox) {
3042
+ throw new ActiveBackendUnresolvableError(
3043
+ `active sandbox ${pointer.activeSandboxId} not found in workspace ${deps.workspaceId}`
3044
+ );
3045
+ }
3046
+ if (sandbox.kind === "selfhosted") {
3047
+ if (!sandbox.enrollmentId) {
3048
+ throw new ActiveBackendUnresolvableError(
3049
+ `selfhosted sandbox ${sandbox.id} has no enrollment (agent id) to address`
3050
+ );
3051
+ }
3052
+ const { session } = await buildSelfhostedBackendSession({
3053
+ workspaceId: deps.workspaceId,
3054
+ relay: deps.relay,
3055
+ controlRpcFactory: deps.controlRpcFactory,
3056
+ agentId: sandbox.enrollmentId,
3057
+ epoch: pointer.activeEpoch,
3058
+ ...deps.selfhostedTimeoutMs !== void 0 ? { timeoutMs: deps.selfhostedTimeoutMs } : {},
3059
+ // The turn's declared environment → the session's manifest.environment, so
3060
+ // the SDK's per-turn manifest-env delta is empty (no "cannot change manifest
3061
+ // environment variables" throw on a pin-to-vm turn).
3062
+ ...deps.environment !== void 0 ? { environment: deps.environment } : {},
3063
+ // The session's working directory (per-session pointer) → the path/cwd base
3064
+ // for this selfhosted backend. Absent/empty ⇒ the default workspace_root.
3065
+ ...pointer.workingDir ? { workingDir: pointer.workingDir } : {}
3066
+ });
3067
+ return { session, sandboxId: sandbox.id, kind: "selfhosted" };
3068
+ }
3069
+ if (sandbox.kind === "modal") {
3070
+ if (!deps.establishModalTarget) {
3071
+ throw new ActiveBackendUnresolvableError(
3072
+ `modal swap target ${sandbox.id} cannot be established in this context (no establisher wired)`
3073
+ );
3074
+ }
3075
+ const session = await deps.establishModalTarget(sandbox);
3076
+ return { session, sandboxId: sandbox.id, kind: "modal" };
3077
+ }
3078
+ throw new ActiveBackendUnresolvableError(
3079
+ `unsupported swap target kind "${sandbox.kind}" for sandbox ${sandbox.id}`
3080
+ );
3081
+ };
3082
+ }
3083
+
3084
+ // src/sandbox/index.ts
3085
+ function createSandboxClient(settings, environment = collectSandboxEnvironment(settings)) {
3086
+ return createSandboxClientForBackend(settings.sandboxBackend, settings, environment);
3087
+ }
3088
+ function createSandboxClientForBackend(backend, settings, environment = collectSandboxEnvironment(settings)) {
3089
+ const registration = PROVIDER_REGISTRY[backend];
3090
+ if (!registration) {
3091
+ throw new SandboxConfigError(backend, `Unknown sandbox backend "${backend}"`);
3092
+ }
3093
+ if (registration.backend === "none") {
3094
+ return void 0;
3095
+ }
3096
+ registration.validateCredentials(settings);
3097
+ const exposedPorts = parseExposedPorts(settings.dockerExposedPorts);
3098
+ const desktop = registration.descriptor.capabilities.DesktopStream;
3099
+ if (desktop.available && settings.sandboxDesktopEnabled && !registration.descriptor.portExposure.supportsOnDemandPorts && !exposedPorts.includes(DESKTOP_STREAM_PORT7)) {
3100
+ exposedPorts.push(DESKTOP_STREAM_PORT7);
3101
+ }
3102
+ if (desktop.available && settings.sandboxDesktopEnabled && !registration.descriptor.portExposure.supportsOnDemandPorts && !exposedPorts.includes(TERMINAL_STREAM_PORT2)) {
3103
+ exposedPorts.push(TERMINAL_STREAM_PORT2);
3104
+ }
3105
+ const raw = registration.build({ settings, environment, exposedPorts });
3106
+ return registration.backend === "docker" ? withDockerNetwork(raw, settings.dockerNetwork) : raw;
3107
+ }
3108
+ function withDockerNetwork(client, network) {
3109
+ const trimmed = network?.trim();
3110
+ if (!trimmed) {
3111
+ return client;
3112
+ }
3113
+ const wrapSession = async (session) => {
3114
+ const containerId = session.state?.containerId;
3115
+ if (typeof containerId === "string" && containerId.length > 0) {
3116
+ await connectDockerNetwork(trimmed, containerId);
3117
+ }
3118
+ return session;
3119
+ };
3120
+ return {
3121
+ backendId: client.backendId,
3122
+ ...client.supportsDefaultOptions !== void 0 ? { supportsDefaultOptions: client.supportsDefaultOptions } : {},
3123
+ ...client.create ? { create: async (...args) => await wrapSession(await client.create(...args)) } : {},
3124
+ ...client.resume ? { resume: async (state) => await wrapSession(await client.resume(state)) } : {},
3125
+ ...client.delete ? { delete: async (state) => await client.delete(state) } : {},
3126
+ ...client.serializeSessionState ? { serializeSessionState: async (state, options) => await client.serializeSessionState(state, options) } : {},
3127
+ ...client.canPersistOwnedSessionState ? { canPersistOwnedSessionState: async (state) => await client.canPersistOwnedSessionState(state) } : {},
3128
+ ...client.canReusePreservedOwnedSession ? { canReusePreservedOwnedSession: async (state) => await client.canReusePreservedOwnedSession(state) } : {},
3129
+ ...client.deserializeSessionState ? { deserializeSessionState: async (state) => await client.deserializeSessionState(state) } : {}
3130
+ };
3131
+ }
3132
+ async function connectDockerNetwork(network, containerId) {
3133
+ const result = Bun.spawnSync(["docker", "network", "connect", network, containerId], {
3134
+ stdout: "pipe",
3135
+ stderr: "pipe"
3136
+ });
3137
+ if (result.exitCode === 0) {
3138
+ return;
3139
+ }
3140
+ const stderr = new TextDecoder().decode(result.stderr);
3141
+ if (stderr.includes("already exists")) {
3142
+ return;
3143
+ }
3144
+ throw new Error(`Failed to connect Docker sandbox container to network ${network}: ${stderr.trim()}`);
3145
+ }
3146
+ function sandboxStateEntryFromRunState(state) {
3147
+ const sandboxState = state?._sandbox;
3148
+ if (!sandboxState) {
3149
+ return null;
3150
+ }
3151
+ const entry = sandboxState.sessionsByAgent?.[sandboxState.currentAgentKey] ?? (sandboxState.currentAgentKey && sandboxState.sessionState ? {
3152
+ backendId: sandboxState.backendId,
3153
+ currentAgentKey: sandboxState.currentAgentKey,
3154
+ currentAgentName: sandboxState.currentAgentName,
3155
+ sessionState: sandboxState.sessionState
3156
+ } : null);
3157
+ if (!entry || !entry.sessionState) {
3158
+ return null;
3159
+ }
3160
+ return entry;
3161
+ }
3162
+ async function restoredSandboxSessionStateFromEntry(entry, client) {
3163
+ if (!client || !entry || typeof entry !== "object" || !("sessionState" in entry)) {
3164
+ return void 0;
3165
+ }
3166
+ if (entry.backendId && client.backendId !== entry.backendId) {
3167
+ throw new Error("Stored sandbox envelope backend does not match the configured sandbox client");
3168
+ }
3169
+ return await deserializeSandboxSessionStateEnvelope(client, entry.sessionState);
3170
+ }
3171
+ function readWorkspaceArchiveFromEnvelopeSessionState(sessionState) {
3172
+ if (!sessionState || typeof sessionState !== "object") {
3173
+ return void 0;
3174
+ }
3175
+ const b64 = sessionState.workspaceArchive;
3176
+ if (typeof b64 !== "string" || b64.length === 0) {
3177
+ return void 0;
3178
+ }
3179
+ try {
3180
+ return Uint8Array.from(Buffer.from(b64, "base64"));
3181
+ } catch {
3182
+ return void 0;
3183
+ }
3184
+ }
3185
+ var MODAL_SNAPSHOT_REF_PREFIXES = [
3186
+ "MODAL_SANDBOX_FS_SNAPSHOT_V1\n",
3187
+ "MODAL_SANDBOX_DIR_SNAPSHOT_V1\n"
3188
+ ];
3189
+ function decodeModalSnapshotId(archive) {
3190
+ let text;
3191
+ try {
3192
+ text = new TextDecoder().decode(archive);
3193
+ } catch {
3194
+ return void 0;
3195
+ }
3196
+ for (const prefix of MODAL_SNAPSHOT_REF_PREFIXES) {
3197
+ if (!text.startsWith(prefix)) {
3198
+ continue;
3199
+ }
3200
+ try {
3201
+ const payload = JSON.parse(text.slice(prefix.length));
3202
+ return typeof payload.snapshot_id === "string" && payload.snapshot_id.length > 0 ? payload.snapshot_id : void 0;
3203
+ } catch {
3204
+ return void 0;
3205
+ }
3206
+ }
3207
+ return void 0;
3208
+ }
3209
+ async function deletePriorPersistedSnapshot(session, priorArchiveBase64) {
3210
+ if (!priorArchiveBase64) {
3211
+ return void 0;
3212
+ }
3213
+ let bytes;
3214
+ try {
3215
+ bytes = Uint8Array.from(Buffer.from(priorArchiveBase64, "base64"));
3216
+ } catch {
3217
+ return void 0;
3218
+ }
3219
+ const snapshotId = decodeModalSnapshotId(bytes);
3220
+ if (!snapshotId) {
3221
+ return void 0;
3222
+ }
3223
+ const modal = session.modal;
3224
+ const del = modal?.images?.delete;
3225
+ if (typeof del !== "function") {
3226
+ return void 0;
3227
+ }
3228
+ try {
3229
+ await del.call(modal.images, snapshotId);
3230
+ return snapshotId;
3231
+ } catch {
3232
+ return void 0;
3233
+ }
3234
+ }
3235
+ async function deserializeSandboxSessionStateEnvelope(client, envelope) {
3236
+ if (!envelope || typeof envelope !== "object") {
3237
+ return void 0;
3238
+ }
3239
+ if (!client.deserializeSessionState) {
3240
+ throw new Error("Sandbox client must implement deserializeSessionState() to resume RunState sandbox state");
3241
+ }
3242
+ const state = envelope;
3243
+ return await client.deserializeSessionState({
3244
+ ...state.providerState ?? {},
3245
+ manifest: state.manifest,
3246
+ ...state.snapshot !== void 0 ? { snapshot: state.snapshot } : {},
3247
+ ...state.snapshotFingerprint !== void 0 ? { snapshotFingerprint: state.snapshotFingerprint } : {},
3248
+ ...state.snapshotFingerprintVersion !== void 0 ? { snapshotFingerprintVersion: state.snapshotFingerprintVersion } : {},
3249
+ workspaceReady: state.workspaceReady,
3250
+ ...state.exposedPorts ? { exposedPorts: structuredClone(state.exposedPorts) } : {}
3251
+ });
3252
+ }
3253
+ function isProviderSandboxNotFoundError(backendId, error) {
3254
+ if (backendId === "selfhosted") {
3255
+ return isSelfhostedProviderNotFoundError(error);
3256
+ }
3257
+ if (!error) {
3258
+ return false;
3259
+ }
3260
+ const status = error.status ?? error.statusCode;
3261
+ if (status === 404) {
3262
+ return true;
3263
+ }
3264
+ const name = typeof error.name === "string" ? error.name : "";
3265
+ const code = typeof error.code === "string" ? error.code : "";
3266
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : String(error?.message ?? "");
3267
+ const haystack = `${name} ${code} ${message}`.toLowerCase();
3268
+ const goneMarkers = [
3269
+ "not found",
3270
+ "no longer running",
3271
+ "no longer exists",
3272
+ "does not exist",
3273
+ "doesn't exist",
3274
+ "has been terminated",
3275
+ "was terminated",
3276
+ "is terminated",
3277
+ "sandbox terminated",
3278
+ "notfound",
3279
+ "sandbox_not_found",
3280
+ "box no longer running"
3281
+ ];
3282
+ if (haystack.includes("already running") || haystack.includes("still running") || haystack.includes("already exists")) {
3283
+ return false;
3284
+ }
3285
+ return goneMarkers.some((marker) => haystack.includes(marker));
3286
+ }
3287
+ function readInstanceId(session) {
3288
+ const state = session.state ?? {};
3289
+ const candidate = state.sandboxId ?? state.instanceId ?? state.id ?? state.hostId ?? state.containerId;
3290
+ return typeof candidate === "string" && candidate.length > 0 ? candidate : "";
3291
+ }
3292
+ async function establishSandboxSessionFromEnvelope(settings, envelope, opts) {
3293
+ const envelopeBackend = typeof envelope?.backendId === "string" ? envelope.backendId : void 0;
3294
+ const backend = opts.backendOverride ?? envelopeBackend ?? settings.sandboxBackend;
3295
+ const environment = opts.environment ?? collectSandboxEnvironment(settings);
3296
+ const client = createSandboxClientForBackend(backend, settings, environment);
3297
+ if (!client) {
3298
+ throw new SandboxConfigError(backend, `Cannot establish a sandbox session for backend "${backend}" (no client; sandboxBackend=none?)`);
3299
+ }
3300
+ if (!client.create) {
3301
+ throw new SandboxConfigError(backend, `Sandbox backend "${backend}" does not support create()`);
3302
+ }
3303
+ const createManifest = { environment };
3304
+ const envelopeSessionState = envelope && typeof envelope === "object" ? envelope.sessionState : void 0;
3305
+ const workspaceArchive = readWorkspaceArchiveFromEnvelopeSessionState(envelopeSessionState);
3306
+ const coldRestore = async (resumeFallbackState) => {
3307
+ const restored = await client.create({ manifest: createManifest });
3308
+ if (workspaceArchive) {
3309
+ const hydrate = restored.hydrateWorkspace;
3310
+ if (typeof hydrate === "function") {
3311
+ try {
3312
+ await hydrate.call(restored, workspaceArchive);
3313
+ } catch (hydrateError) {
3314
+ const restoredState2 = restored.state;
3315
+ const clientWithDelete = client;
3316
+ if (typeof clientWithDelete.delete === "function" && restoredState2 !== void 0) {
3317
+ try {
3318
+ await clientWithDelete.delete(restoredState2);
3319
+ } catch {
3320
+ }
3321
+ } else {
3322
+ const sess = restored;
3323
+ try {
3324
+ await (sess.terminate ?? sess.close)?.();
3325
+ } catch {
3326
+ }
3327
+ }
3328
+ throw hydrateError;
3329
+ }
3330
+ }
3331
+ }
3332
+ const restoredState = restored.state;
3333
+ return { client, session: restored, sessionState: restoredState ?? resumeFallbackState, instanceId: readInstanceId(restored), backendId: client.backendId };
3334
+ };
3335
+ const envelopeProviderState = envelopeSessionState && typeof envelopeSessionState === "object" ? envelopeSessionState.providerState : void 0;
3336
+ const hasResumableInstance = Boolean(
3337
+ envelopeProviderState && typeof envelopeProviderState === "object" && (envelopeProviderState.sandboxId || envelopeProviderState.instanceId || envelopeProviderState.id || envelopeProviderState.containerId)
3338
+ );
3339
+ if (hasResumableInstance && envelopeSessionState && client.resume && client.deserializeSessionState) {
3340
+ let resumedState;
3341
+ try {
3342
+ resumedState = await deserializeSandboxSessionStateEnvelope(client, envelopeSessionState);
3343
+ } catch (error) {
3344
+ throw new SandboxConfigError(backend, `Failed to deserialize sandbox resume envelope for backend "${backend}": ${error instanceof Error ? error.message : String(error)}`);
3345
+ }
3346
+ if (resumedState !== void 0) {
3347
+ try {
3348
+ const session = await client.resume(resumedState);
3349
+ return { client, session, sessionState: resumedState, instanceId: readInstanceId(session), backendId: client.backendId };
3350
+ } catch (error) {
3351
+ if (!isProviderSandboxNotFoundError(client.backendId, error)) {
3352
+ throw error;
3353
+ }
3354
+ return await coldRestore(resumedState);
3355
+ }
3356
+ }
3357
+ }
3358
+ return await coldRestore();
3359
+ }
3360
+ async function serializeEstablishedSandboxEnvelope(established) {
3361
+ const client = established.client;
3362
+ if (!client || typeof client.serializeSessionState !== "function") {
3363
+ return null;
3364
+ }
3365
+ if (established.sessionState === void 0 || established.sessionState === null) {
3366
+ return null;
3367
+ }
3368
+ try {
3369
+ const serialized = await client.serializeSessionState(established.sessionState);
3370
+ const flat = serialized;
3371
+ const manifest = flat.manifest;
3372
+ const exposedPorts = flat.configuredExposedPorts ?? flat.exposedPorts;
3373
+ const sessionState = {
3374
+ providerState: flat,
3375
+ ...manifest !== void 0 ? { manifest } : {},
3376
+ ...exposedPorts !== void 0 ? { exposedPorts } : {},
3377
+ workspaceReady: true
3378
+ };
3379
+ return { backendId: established.backendId, sessionState };
3380
+ } catch {
3381
+ return null;
3382
+ }
3383
+ }
3384
+
3385
+ export {
3386
+ CAPABILITY_DESCRIPTORS,
3387
+ DESKTOP_STREAM_PORT,
3388
+ assertDescriptorRegistryInvariants,
3389
+ SandboxConfigError,
3390
+ SandboxProviderUnavailableError,
3391
+ subjectFor,
3392
+ SelfhostedControlError,
3393
+ agentErrorToControlError,
3394
+ offlineAgentError,
3395
+ timeoutAgentError,
3396
+ NatsControlRpc,
3397
+ offlineControlResponse,
3398
+ timeoutControlResponse,
3399
+ setSelfhostedApplyDiff,
3400
+ SELFHOSTED_DEFAULT_TIMEOUT_MS,
3401
+ SELFHOSTED_RELAY_STREAM_PATH,
3402
+ SelfhostedSession,
3403
+ SelfhostedSandboxClient,
3404
+ buildSelfhostedBackendSession,
3405
+ isSelfhostedProviderNotFoundError,
3406
+ PROVIDER_REGISTRY,
3407
+ assertProviderRegistryInvariants,
3408
+ selectBackend,
3409
+ backendSupportsOs,
3410
+ desktopCapableBackend,
3411
+ negotiateCapabilities,
3412
+ STREAM_TOKEN_DEFAULT_TTL_SECONDS,
3413
+ mintStreamToken,
3414
+ verifyStreamToken,
3415
+ StreamTokenPayload2 as StreamTokenPayload,
3416
+ STREAM_PORT,
3417
+ DISPLAY_STACK_TIMEOUT_MS,
3418
+ DEFAULT_DESKTOP_GEOMETRY,
3419
+ DisplayStackError,
3420
+ DisplayStackUnsupportedError,
3421
+ buildDisplayStackScript,
3422
+ ensureDisplayStack,
3423
+ tearDownDisplayStack,
3424
+ TERMINAL_STREAM_PORT,
3425
+ TERMINAL_SERVER_TIMEOUT_MS,
3426
+ TerminalServerError,
3427
+ TerminalServerUnsupportedError,
3428
+ buildTerminalServerScript,
3429
+ ensureTerminalServer,
3430
+ tearDownTerminalServer,
3431
+ StreamPortUnavailableError,
3432
+ buildStreamUrl,
3433
+ exposeStreamPort,
3434
+ contentTypeForCodec,
3435
+ extForCodec,
3436
+ RecordingUnavailableError,
3437
+ RecordingError,
3438
+ startRecording,
3439
+ stopRecording,
3440
+ readRecordingBytes,
3441
+ deleteRecordingArtifacts,
3442
+ recordingStorageKey,
3443
+ ChannelAValidationError,
3444
+ ChannelAConflictError,
3445
+ ChannelANotFoundError,
3446
+ ChannelAUnsupportedError,
3447
+ SandboxChannelAService,
3448
+ stripExecBanner2 as stripExecBanner,
3449
+ isWorkspaceEscapeError,
3450
+ isExecSessionLostBanner,
3451
+ parseExecBannerSessionId,
3452
+ assertSafeRelPath,
3453
+ parsePorcelainV2,
3454
+ parseNumstatZ,
3455
+ parseUnifiedPatch,
3456
+ SELFHOSTED_RECONNECT_WINDOW_MS,
3457
+ selfhostedLiveness,
3458
+ negotiateSelfhostedCapabilities,
3459
+ MockAgentResponder,
3460
+ RoutingUnsupportedError,
3461
+ RoutingSandboxSession,
3462
+ ActiveBackendUnresolvableError,
3463
+ makeActiveBackendResolver,
3464
+ createSandboxClient,
3465
+ createSandboxClientForBackend,
3466
+ sandboxStateEntryFromRunState,
3467
+ restoredSandboxSessionStateFromEntry,
3468
+ readWorkspaceArchiveFromEnvelopeSessionState,
3469
+ decodeModalSnapshotId,
3470
+ deletePriorPersistedSnapshot,
3471
+ deserializeSandboxSessionStateEnvelope,
3472
+ isProviderSandboxNotFoundError,
3473
+ establishSandboxSessionFromEnvelope,
3474
+ serializeEstablishedSandboxEnvelope,
3475
+ collectSandboxEnvironment2 as collectSandboxEnvironment,
3476
+ parseExposedPorts2 as parseExposedPorts
3477
+ };
3478
+ //# sourceMappingURL=chunk-2PO56VAL.js.map