@opengeni/runtime 0.2.3 → 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
@@ -1346,6 +1507,7 @@ var STREAM_PORT = DESKTOP_STREAM_PORT4;
1346
1507
  var DISPLAY_STACK_TIMEOUT_MS = 9e4;
1347
1508
  var PAINT_PROBE_ATTEMPTS = 150;
1348
1509
  var PAINT_PROBE_INTERVAL_S = 0.2;
1510
+ var PAINT_MIN_BYTES = 6e4;
1349
1511
  var DEFAULT_DESKTOP_GEOMETRY = { width: 1280, height: 800, dpi: 96 };
1350
1512
  var DisplayStackError = class extends Error {
1351
1513
  exitCode;
@@ -1370,7 +1532,7 @@ function buildDisplayStackScript(options = {}) {
1370
1532
  const port = options.port ?? DESKTOP_STREAM_PORT4;
1371
1533
  const env = `DESKTOP_W=${geometry.width} DESKTOP_H=${geometry.height} DESKTOP_DPI=${geometry.dpi} STREAM_PORT=${port}`;
1372
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`;
1373
- 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 && [ -s "$p" ]; then rm -f "$p"; break; fi; rm -f "$p"; if [ "$i" = "${PAINT_PROBE_ATTEMPTS}" ]; then echo "OPENGENI_DESKTOP_NOT_PAINTING scrot empty after warmup"; exit 14; fi; sleep ${PAINT_PROBE_INTERVAL_S}; done`;
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`;
1374
1536
  return `mkdir -p /tmp/opengeni-desktop; { ${bringUp} ; } && { ${paintProbe} ; }`;
1375
1537
  }
1376
1538
  function execResultOutput(result) {
@@ -3296,6 +3458,21 @@ function readInstanceId(session) {
3296
3458
  const candidate = state.sandboxId ?? state.instanceId ?? state.id ?? state.hostId ?? state.containerId;
3297
3459
  return typeof candidate === "string" && candidate.length > 0 ? candidate : "";
3298
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
+ }
3299
3476
  async function establishSandboxSessionFromEnvelope(settings, envelope, opts) {
3300
3477
  const envelopeBackend = typeof envelope?.backendId === "string" ? envelope.backendId : void 0;
3301
3478
  const backend = opts.backendOverride ?? envelopeBackend ?? settings.sandboxBackend;
@@ -3312,32 +3489,60 @@ async function establishSandboxSessionFromEnvelope(settings, envelope, opts) {
3312
3489
  const workspaceArchive = readWorkspaceArchiveFromEnvelopeSessionState(envelopeSessionState);
3313
3490
  const coldRestore = async (resumeFallbackState) => {
3314
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
+ }
3315
3508
  if (workspaceArchive) {
3316
3509
  const hydrate = restored.hydrateWorkspace;
3317
3510
  if (typeof hydrate === "function") {
3318
3511
  try {
3319
3512
  await hydrate.call(restored, workspaceArchive);
3320
3513
  } catch (hydrateError) {
3321
- const restoredState2 = restored.state;
3322
- const clientWithDelete = client;
3323
- if (typeof clientWithDelete.delete === "function" && restoredState2 !== void 0) {
3324
- try {
3325
- await clientWithDelete.delete(restoredState2);
3326
- } catch {
3327
- }
3328
- } else {
3329
- 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) {
3330
3528
  try {
3331
- await (sess.terminate ?? sess.close)?.();
3332
- } catch {
3529
+ await opts.onSandboxCreated(established);
3530
+ } catch (createCallbackError) {
3531
+ await terminateCreatedSandbox(client, restored, hydratedState);
3532
+ throw createCallbackError;
3333
3533
  }
3334
3534
  }
3335
- throw hydrateError;
3336
3535
  }
3337
3536
  }
3338
3537
  }
3339
- const restoredState = restored.state;
3340
- 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
+ };
3341
3546
  };
3342
3547
  const envelopeProviderState = envelopeSessionState && typeof envelopeSessionState === "object" ? envelopeSessionState.providerState : void 0;
3343
3548
  const hasResumableInstance = Boolean(
@@ -3395,6 +3600,11 @@ export {
3395
3600
  assertDescriptorRegistryInvariants,
3396
3601
  SandboxConfigError,
3397
3602
  SandboxProviderUnavailableError,
3603
+ modalSandboxAttributionEnvironment,
3604
+ modalSandboxAttributionTags,
3605
+ tagModalSandbox,
3606
+ terminateModalSandboxById,
3607
+ sweepModalOrphanSandboxes,
3398
3608
  subjectFor,
3399
3609
  SelfhostedControlError,
3400
3610
  agentErrorToControlError,
@@ -3482,4 +3692,4 @@ export {
3482
3692
  collectSandboxEnvironment2 as collectSandboxEnvironment,
3483
3693
  parseExposedPorts2 as parseExposedPorts
3484
3694
  };
3485
- //# sourceMappingURL=chunk-KNW7AMQB.js.map
3695
+ //# sourceMappingURL=chunk-D5KU3QUC.js.map