@opengeni/runtime 0.2.2 → 0.3.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.
@@ -202,6 +202,23 @@ var localProvider = {
202
202
  // src/sandbox/providers/modal.ts
203
203
  import { ModalImageSelector, ModalSandboxClient } from "@openai/agents-extensions/sandbox/modal";
204
204
  import { effectiveModalIdleTimeoutSeconds } from "@opengeni/config";
205
+ var MODAL_ORPHAN_SWEEP_LIMIT = 50;
206
+ var MODAL_UNATTRIBUTED_ORPHAN_GRACE_MS = 30 * 6e4;
207
+ function modalSandboxAttributionEnvironment(input) {
208
+ return {
209
+ OPENGENI_SANDBOX_LEASE_ID: input.leaseId,
210
+ OPENGENI_SANDBOX_GROUP_ID: input.sandboxGroupId,
211
+ OPENGENI_WORKSPACE_ID: input.workspaceId
212
+ };
213
+ }
214
+ function modalSandboxAttributionTags(input) {
215
+ return {
216
+ opengeni: "true",
217
+ opengeni_lease_id: input.leaseId,
218
+ opengeni_workspace_id: input.workspaceId,
219
+ opengeni_sandbox_group_id: input.sandboxGroupId
220
+ };
221
+ }
205
222
  var modalProvider = {
206
223
  backend: "modal",
207
224
  descriptor: CAPABILITY_DESCRIPTORS.modal,
@@ -220,8 +237,13 @@ var modalProvider = {
220
237
  const options = {
221
238
  appName: settings.modalAppName,
222
239
  timeoutMs: settings.modalTimeoutSeconds * 1e3,
240
+ sandboxCreateTimeoutS: Math.ceil(settings.sandboxWarmingTimeoutMs / 1e3),
223
241
  exposedPorts,
224
- env: environment
242
+ env: environment,
243
+ // The Modal JS SDK's sandbox default command already sleeps until timeout
244
+ // or explicit termination. Do not let the Agents extension stamp a separate
245
+ // hardcoded sleep command; OPENGENI_MODAL_TIMEOUT_SECONDS owns lifetime.
246
+ useSleepCmd: false
225
247
  };
226
248
  options.idleTimeoutMs = effectiveModalIdleTimeoutSeconds(settings) * 1e3;
227
249
  if (settings.modalWorkspacePersistence) {
@@ -242,6 +264,137 @@ var modalProvider = {
242
264
  return new ModalSandboxClient(options);
243
265
  }
244
266
  };
267
+ function modalClientOptions(settings) {
268
+ return {
269
+ ...settings.modalTokenId ? { tokenId: settings.modalTokenId } : {},
270
+ ...settings.modalTokenSecret ? { tokenSecret: settings.modalTokenSecret } : {},
271
+ ...settings.modalEnvironment ? { environment: settings.modalEnvironment } : {},
272
+ ...settings.modalTimeoutSeconds ? { timeoutMs: settings.modalTimeoutSeconds * 1e3 } : {}
273
+ };
274
+ }
275
+ async function createModalClient(settings) {
276
+ const modal = await import("modal");
277
+ return new modal.ModalClient(modalClientOptions(settings));
278
+ }
279
+ async function tagModalSandbox(settings, sandboxId, attribution) {
280
+ if (!sandboxId) {
281
+ return false;
282
+ }
283
+ const modal = await createModalClient(settings);
284
+ try {
285
+ const sandbox = await modal.sandboxes.fromId(sandboxId);
286
+ await sandbox.setTags(modalSandboxAttributionTags(attribution));
287
+ return true;
288
+ } finally {
289
+ modal.close();
290
+ }
291
+ }
292
+ async function terminateModalSandboxById(settings, sandboxId) {
293
+ if (!sandboxId) {
294
+ return true;
295
+ }
296
+ const modal = await createModalClient(settings);
297
+ try {
298
+ const sandbox = await modal.sandboxes.fromId(sandboxId);
299
+ await sandbox.terminate();
300
+ return true;
301
+ } finally {
302
+ modal.close();
303
+ }
304
+ }
305
+ function tagsFromInfo(info) {
306
+ const tags = {};
307
+ for (const tag of info.tags ?? []) {
308
+ if (typeof tag.tagName === "string" && typeof tag.tagValue === "string") {
309
+ tags[tag.tagName] = tag.tagValue;
310
+ }
311
+ }
312
+ return tags;
313
+ }
314
+ function sandboxCreatedAtMs(info) {
315
+ if (typeof info.createdAt !== "number" || !Number.isFinite(info.createdAt) || info.createdAt <= 0) {
316
+ return null;
317
+ }
318
+ return info.createdAt < 1e10 ? Math.floor(info.createdAt * 1e3) : Math.floor(info.createdAt);
319
+ }
320
+ function attributionKey(input) {
321
+ return `${input.workspaceId}:${input.sandboxGroupId}:${input.leaseId}`;
322
+ }
323
+ async function sweepModalOrphanSandboxes(settings, liveLeases, options = {}) {
324
+ const nowMs = options.now?.getTime() ?? Date.now();
325
+ const maxTerminations = options.maxTerminations ?? MODAL_ORPHAN_SWEEP_LIMIT;
326
+ const unattributedGraceMs = options.unattributedGraceMs ?? MODAL_UNATTRIBUTED_ORPHAN_GRACE_MS;
327
+ const liveByAttribution = new Map(liveLeases.map((lease) => [attributionKey(lease), lease]));
328
+ const ownedClient = options.client ? null : await createModalClient(settings);
329
+ const modal = options.client ?? ownedClient;
330
+ try {
331
+ const app = await modal.apps.fromName(settings.modalAppName, {
332
+ createIfMissing: false,
333
+ ...settings.modalEnvironment ? { environment: settings.modalEnvironment } : {}
334
+ });
335
+ const appId = app.appId;
336
+ if (!appId) {
337
+ return { examined: 0, terminated: [], skipped: 0 };
338
+ }
339
+ let examined = 0;
340
+ let skipped = 0;
341
+ const terminated = [];
342
+ let beforeTimestamp;
343
+ while (terminated.length < maxTerminations) {
344
+ const response = await modal.cpClient.sandboxList({
345
+ appId,
346
+ ...beforeTimestamp !== void 0 ? { beforeTimestamp } : {},
347
+ includeFinished: false,
348
+ ...settings.modalEnvironment ? { environmentName: settings.modalEnvironment } : {},
349
+ tags: []
350
+ });
351
+ const sandboxes = response.sandboxes ?? [];
352
+ if (sandboxes.length === 0) {
353
+ break;
354
+ }
355
+ for (const info of sandboxes) {
356
+ examined += 1;
357
+ const tags = tagsFromInfo(info);
358
+ const leaseId = tags.opengeni_lease_id;
359
+ const workspaceId = tags.opengeni_workspace_id;
360
+ const sandboxGroupId = tags.opengeni_sandbox_group_id;
361
+ let reason = null;
362
+ if (leaseId && workspaceId && sandboxGroupId) {
363
+ const live = liveByAttribution.get(attributionKey({ leaseId, workspaceId, sandboxGroupId }));
364
+ if (!live || live.instanceId && live.instanceId !== info.id) {
365
+ reason = "stale_attribution";
366
+ }
367
+ } else {
368
+ const createdAtMs = sandboxCreatedAtMs(info);
369
+ if (createdAtMs !== null && nowMs - createdAtMs >= unattributedGraceMs) {
370
+ reason = "unattributed";
371
+ }
372
+ }
373
+ if (!reason) {
374
+ skipped += 1;
375
+ continue;
376
+ }
377
+ try {
378
+ const sandbox = await modal.sandboxes.fromId(info.id);
379
+ await sandbox.terminate();
380
+ terminated.push({ sandboxId: info.id, reason, tags });
381
+ } catch {
382
+ skipped += 1;
383
+ }
384
+ if (terminated.length >= maxTerminations) {
385
+ break;
386
+ }
387
+ }
388
+ beforeTimestamp = sandboxes[sandboxes.length - 1]?.createdAt;
389
+ if (beforeTimestamp === void 0) {
390
+ break;
391
+ }
392
+ }
393
+ return { examined, terminated, skipped };
394
+ } finally {
395
+ ownedClient?.close();
396
+ }
397
+ }
245
398
 
246
399
  // src/sandbox/providers/none.ts
247
400
  var noneProvider = {
@@ -757,16 +910,24 @@ var SelfhostedSession = class {
757
910
  /** Computer-use VIEW op: capture a single PNG screenshot of the machine's desktop
758
911
  * plus its geometry (via ScreenCaptureKit / x11). NOT consent-gated (a view op —
759
912
  * the view/control decoupling), so it works with a display but no screen-control
760
- * consent. Returns the raw encoded bytes + width/height. */
913
+ * consent. Returns the raw encoded bytes + the ENCODED width/height, plus the
914
+ * NATIVE (pre-downscale) geometry: when the agent had to downscale the PNG to fit
915
+ * the transport's max payload, `nativeWidth`/`nativeHeight` carry the original
916
+ * capture size so the computer-use layer can scale model clicks (in encoded-pixel
917
+ * space) back to native pixels. An older agent leaves them 0 → read as "same as
918
+ * width/height" (no downscale). */
761
919
  async screenshot() {
762
920
  const result = await this.call({ $case: "desktopScreenshot", desktopScreenshot: {} });
763
921
  if (result.$case !== "desktopScreenshot") {
764
922
  throw new Error(`selfhosted screenshot: unexpected result ${result.$case}`);
765
923
  }
924
+ const s = result.desktopScreenshot;
766
925
  return {
767
- png: result.desktopScreenshot.png,
768
- width: result.desktopScreenshot.width,
769
- height: result.desktopScreenshot.height
926
+ png: s.png,
927
+ width: s.width,
928
+ height: s.height,
929
+ nativeWidth: s.nativeWidth || s.width,
930
+ nativeHeight: s.nativeHeight || s.height
770
931
  };
771
932
  }
772
933
  /** A cheap liveness probe — request a Ping on the subject; returns true iff a
@@ -1343,13 +1504,16 @@ async function verifyStreamToken(secret, token, nowSeconds = Math.floor(Date.now
1343
1504
  // src/sandbox/display-stack.ts
1344
1505
  import { DESKTOP_STREAM_PORT as DESKTOP_STREAM_PORT4 } from "@opengeni/contracts";
1345
1506
  var STREAM_PORT = DESKTOP_STREAM_PORT4;
1346
- var DISPLAY_STACK_TIMEOUT_MS = 6e4;
1507
+ var DISPLAY_STACK_TIMEOUT_MS = 9e4;
1508
+ var PAINT_PROBE_ATTEMPTS = 150;
1509
+ var PAINT_PROBE_INTERVAL_S = 0.2;
1510
+ var PAINT_MIN_BYTES = 6e4;
1347
1511
  var DEFAULT_DESKTOP_GEOMETRY = { width: 1280, height: 800, dpi: 96 };
1348
1512
  var DisplayStackError = class extends Error {
1349
1513
  exitCode;
1350
1514
  stage;
1351
1515
  constructor(exitCode, output) {
1352
- const stage = exitCode === 11 ? "xvfb" : exitCode === 12 ? "x11vnc" : exitCode === 13 ? "websockify" : "unknown";
1516
+ const stage = exitCode === 11 ? "xvfb" : exitCode === 12 ? "x11vnc" : exitCode === 13 ? "websockify" : exitCode === 14 ? "paint" : "unknown";
1353
1517
  super(`desktop display stack failed at stage "${stage}" (exit ${exitCode})${output ? `:
1354
1518
  ${output}` : ""}`);
1355
1519
  this.name = "DisplayStackError";
@@ -1367,7 +1531,9 @@ function buildDisplayStackScript(options = {}) {
1367
1531
  const geometry = options.geometry ?? DEFAULT_DESKTOP_GEOMETRY;
1368
1532
  const port = options.port ?? DESKTOP_STREAM_PORT4;
1369
1533
  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`;
1534
+ const bringUp = `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`;
1535
+ const paintProbe = `p=/tmp/opengeni-desktop/paint-probe.png; for i in $(seq 1 ${PAINT_PROBE_ATTEMPTS}); do if DISPLAY=:0 scrot -o "$p" >/dev/null 2>&1; then sz=$(wc -c < "$p" 2>/dev/null || echo 0); else sz=0; fi; rm -f "$p"; if [ "$sz" -ge ${PAINT_MIN_BYTES} ]; then break; fi; if [ "$i" = "${PAINT_PROBE_ATTEMPTS}" ]; then echo "OPENGENI_DESKTOP_NOT_PAINTING scrot below ${PAINT_MIN_BYTES}B after warmup (last=$sz)"; exit 14; fi; sleep ${PAINT_PROBE_INTERVAL_S}; done`;
1536
+ return `mkdir -p /tmp/opengeni-desktop; { ${bringUp} ; } && { ${paintProbe} ; }`;
1371
1537
  }
1372
1538
  function execResultOutput(result) {
1373
1539
  if (typeof result === "string") {
@@ -1382,6 +1548,9 @@ function execResultExitCode(result) {
1382
1548
  return typeof result.exitCode === "number" ? result.exitCode : null;
1383
1549
  }
1384
1550
  function inferExitFromOutput(output) {
1551
+ if (/OPENGENI_DESKTOP_NOT_PAINTING/.test(output)) {
1552
+ return 14;
1553
+ }
1385
1554
  if (/OPENGENI_DESKTOP_UP\b/.test(output)) {
1386
1555
  return 0;
1387
1556
  }
@@ -3289,6 +3458,21 @@ function readInstanceId(session) {
3289
3458
  const candidate = state.sandboxId ?? state.instanceId ?? state.id ?? state.hostId ?? state.containerId;
3290
3459
  return typeof candidate === "string" && candidate.length > 0 ? candidate : "";
3291
3460
  }
3461
+ async function terminateCreatedSandbox(client, session, sessionState) {
3462
+ const clientWithDelete = client;
3463
+ if (typeof clientWithDelete.delete === "function" && sessionState !== void 0) {
3464
+ try {
3465
+ await clientWithDelete.delete(sessionState);
3466
+ } catch {
3467
+ }
3468
+ return;
3469
+ }
3470
+ const sess = session;
3471
+ try {
3472
+ await (sess.terminate ?? sess.kill ?? sess.close)?.();
3473
+ } catch {
3474
+ }
3475
+ }
3292
3476
  async function establishSandboxSessionFromEnvelope(settings, envelope, opts) {
3293
3477
  const envelopeBackend = typeof envelope?.backendId === "string" ? envelope.backendId : void 0;
3294
3478
  const backend = opts.backendOverride ?? envelopeBackend ?? settings.sandboxBackend;
@@ -3305,32 +3489,60 @@ async function establishSandboxSessionFromEnvelope(settings, envelope, opts) {
3305
3489
  const workspaceArchive = readWorkspaceArchiveFromEnvelopeSessionState(envelopeSessionState);
3306
3490
  const coldRestore = async (resumeFallbackState) => {
3307
3491
  const restored = await client.create({ manifest: createManifest });
3492
+ let restoredState = restored.state;
3493
+ let established = {
3494
+ client,
3495
+ session: restored,
3496
+ sessionState: restoredState ?? resumeFallbackState,
3497
+ instanceId: readInstanceId(restored),
3498
+ backendId: client.backendId
3499
+ };
3500
+ if (opts.onSandboxCreated) {
3501
+ try {
3502
+ await opts.onSandboxCreated(established);
3503
+ } catch (createCallbackError) {
3504
+ await terminateCreatedSandbox(client, restored, restoredState);
3505
+ throw createCallbackError;
3506
+ }
3507
+ }
3308
3508
  if (workspaceArchive) {
3309
3509
  const hydrate = restored.hydrateWorkspace;
3310
3510
  if (typeof hydrate === "function") {
3311
3511
  try {
3312
3512
  await hydrate.call(restored, workspaceArchive);
3313
3513
  } 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;
3514
+ await terminateCreatedSandbox(client, restored, restoredState);
3515
+ throw hydrateError;
3516
+ }
3517
+ const hydratedState = restored.state;
3518
+ const hydratedInstanceId = readInstanceId(restored);
3519
+ if (hydratedInstanceId && hydratedInstanceId !== established.instanceId) {
3520
+ established = {
3521
+ client,
3522
+ session: restored,
3523
+ sessionState: hydratedState ?? resumeFallbackState,
3524
+ instanceId: hydratedInstanceId,
3525
+ backendId: client.backendId
3526
+ };
3527
+ if (opts.onSandboxCreated) {
3323
3528
  try {
3324
- await (sess.terminate ?? sess.close)?.();
3325
- } catch {
3529
+ await opts.onSandboxCreated(established);
3530
+ } catch (createCallbackError) {
3531
+ await terminateCreatedSandbox(client, restored, hydratedState);
3532
+ throw createCallbackError;
3326
3533
  }
3327
3534
  }
3328
- throw hydrateError;
3329
3535
  }
3330
3536
  }
3331
3537
  }
3332
- const restoredState = restored.state;
3333
- return { client, session: restored, sessionState: restoredState ?? resumeFallbackState, instanceId: readInstanceId(restored), backendId: client.backendId };
3538
+ restoredState = restored.state;
3539
+ return {
3540
+ client,
3541
+ session: restored,
3542
+ sessionState: restoredState ?? resumeFallbackState,
3543
+ instanceId: readInstanceId(restored),
3544
+ backendId: client.backendId
3545
+ };
3334
3546
  };
3335
3547
  const envelopeProviderState = envelopeSessionState && typeof envelopeSessionState === "object" ? envelopeSessionState.providerState : void 0;
3336
3548
  const hasResumableInstance = Boolean(
@@ -3388,6 +3600,11 @@ export {
3388
3600
  assertDescriptorRegistryInvariants,
3389
3601
  SandboxConfigError,
3390
3602
  SandboxProviderUnavailableError,
3603
+ modalSandboxAttributionEnvironment,
3604
+ modalSandboxAttributionTags,
3605
+ tagModalSandbox,
3606
+ terminateModalSandboxById,
3607
+ sweepModalOrphanSandboxes,
3391
3608
  subjectFor,
3392
3609
  SelfhostedControlError,
3393
3610
  agentErrorToControlError,
@@ -3475,4 +3692,4 @@ export {
3475
3692
  collectSandboxEnvironment2 as collectSandboxEnvironment,
3476
3693
  parseExposedPorts2 as parseExposedPorts
3477
3694
  };
3478
- //# sourceMappingURL=chunk-2PO56VAL.js.map
3695
+ //# sourceMappingURL=chunk-D5KU3QUC.js.map