@opengeni/runtime 0.2.3 → 0.3.1

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,8 @@ 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;
1511
+ var PAINT_SETTLE_DELTA_BYTES = 2e3;
1349
1512
  var DEFAULT_DESKTOP_GEOMETRY = { width: 1280, height: 800, dpi: 96 };
1350
1513
  var DisplayStackError = class extends Error {
1351
1514
  exitCode;
@@ -1370,7 +1533,7 @@ function buildDisplayStackScript(options = {}) {
1370
1533
  const port = options.port ?? DESKTOP_STREAM_PORT4;
1371
1534
  const env = `DESKTOP_W=${geometry.width} DESKTOP_H=${geometry.height} DESKTOP_DPI=${geometry.dpi} STREAM_PORT=${port}`;
1372
1535
  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`;
1536
+ const paintProbe = `p=/tmp/opengeni-desktop/paint-probe.png; prev=0; 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} ] && [ "$prev" -ge ${PAINT_MIN_BYTES} ]; then d=$((sz-prev)); [ "$d" -lt 0 ] && d=$((0-d)); [ "$d" -le ${PAINT_SETTLE_DELTA_BYTES} ] && break; fi; prev=$sz; if [ "$i" = "${PAINT_PROBE_ATTEMPTS}" ]; then echo "OPENGENI_DESKTOP_NOT_PAINTING scrot below ${PAINT_MIN_BYTES}B or unsettled after warmup (last=$sz)"; exit 14; fi; sleep ${PAINT_PROBE_INTERVAL_S}; done`;
1374
1537
  return `mkdir -p /tmp/opengeni-desktop; { ${bringUp} ; } && { ${paintProbe} ; }`;
1375
1538
  }
1376
1539
  function execResultOutput(result) {
@@ -3296,6 +3459,21 @@ function readInstanceId(session) {
3296
3459
  const candidate = state.sandboxId ?? state.instanceId ?? state.id ?? state.hostId ?? state.containerId;
3297
3460
  return typeof candidate === "string" && candidate.length > 0 ? candidate : "";
3298
3461
  }
3462
+ async function terminateCreatedSandbox(client, session, sessionState) {
3463
+ const clientWithDelete = client;
3464
+ if (typeof clientWithDelete.delete === "function" && sessionState !== void 0) {
3465
+ try {
3466
+ await clientWithDelete.delete(sessionState);
3467
+ } catch {
3468
+ }
3469
+ return;
3470
+ }
3471
+ const sess = session;
3472
+ try {
3473
+ await (sess.terminate ?? sess.kill ?? sess.close)?.();
3474
+ } catch {
3475
+ }
3476
+ }
3299
3477
  async function establishSandboxSessionFromEnvelope(settings, envelope, opts) {
3300
3478
  const envelopeBackend = typeof envelope?.backendId === "string" ? envelope.backendId : void 0;
3301
3479
  const backend = opts.backendOverride ?? envelopeBackend ?? settings.sandboxBackend;
@@ -3311,33 +3489,69 @@ async function establishSandboxSessionFromEnvelope(settings, envelope, opts) {
3311
3489
  const envelopeSessionState = envelope && typeof envelope === "object" ? envelope.sessionState : void 0;
3312
3490
  const workspaceArchive = readWorkspaceArchiveFromEnvelopeSessionState(envelopeSessionState);
3313
3491
  const coldRestore = async (resumeFallbackState) => {
3314
- const restored = await client.create({ manifest: createManifest });
3492
+ const createStarted = Date.now();
3493
+ let restored;
3494
+ try {
3495
+ restored = await client.create({ manifest: createManifest });
3496
+ recordSandboxCreateMetric(opts.metrics, client.backendId, "completed", createStarted);
3497
+ } catch (error) {
3498
+ recordSandboxCreateMetric(opts.metrics, client.backendId, "failed", createStarted);
3499
+ throw error;
3500
+ }
3501
+ let restoredState = restored.state;
3502
+ let established = {
3503
+ client,
3504
+ session: restored,
3505
+ sessionState: restoredState ?? resumeFallbackState,
3506
+ instanceId: readInstanceId(restored),
3507
+ backendId: client.backendId
3508
+ };
3509
+ if (opts.onSandboxCreated) {
3510
+ try {
3511
+ await opts.onSandboxCreated(established);
3512
+ } catch (createCallbackError) {
3513
+ await terminateCreatedSandbox(client, restored, restoredState);
3514
+ throw createCallbackError;
3515
+ }
3516
+ }
3315
3517
  if (workspaceArchive) {
3316
3518
  const hydrate = restored.hydrateWorkspace;
3317
3519
  if (typeof hydrate === "function") {
3318
3520
  try {
3319
3521
  await hydrate.call(restored, workspaceArchive);
3320
3522
  } 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;
3523
+ await terminateCreatedSandbox(client, restored, restoredState);
3524
+ throw hydrateError;
3525
+ }
3526
+ const hydratedState = restored.state;
3527
+ const hydratedInstanceId = readInstanceId(restored);
3528
+ if (hydratedInstanceId && hydratedInstanceId !== established.instanceId) {
3529
+ established = {
3530
+ client,
3531
+ session: restored,
3532
+ sessionState: hydratedState ?? resumeFallbackState,
3533
+ instanceId: hydratedInstanceId,
3534
+ backendId: client.backendId
3535
+ };
3536
+ if (opts.onSandboxCreated) {
3330
3537
  try {
3331
- await (sess.terminate ?? sess.close)?.();
3332
- } catch {
3538
+ await opts.onSandboxCreated(established);
3539
+ } catch (createCallbackError) {
3540
+ await terminateCreatedSandbox(client, restored, hydratedState);
3541
+ throw createCallbackError;
3333
3542
  }
3334
3543
  }
3335
- throw hydrateError;
3336
3544
  }
3337
3545
  }
3338
3546
  }
3339
- const restoredState = restored.state;
3340
- return { client, session: restored, sessionState: restoredState ?? resumeFallbackState, instanceId: readInstanceId(restored), backendId: client.backendId };
3547
+ restoredState = restored.state;
3548
+ return {
3549
+ client,
3550
+ session: restored,
3551
+ sessionState: restoredState ?? resumeFallbackState,
3552
+ instanceId: readInstanceId(restored),
3553
+ backendId: client.backendId
3554
+ };
3341
3555
  };
3342
3556
  const envelopeProviderState = envelopeSessionState && typeof envelopeSessionState === "object" ? envelopeSessionState.providerState : void 0;
3343
3557
  const hasResumableInstance = Boolean(
@@ -3364,6 +3578,16 @@ async function establishSandboxSessionFromEnvelope(settings, envelope, opts) {
3364
3578
  }
3365
3579
  return await coldRestore();
3366
3580
  }
3581
+ function recordSandboxCreateMetric(metrics, backend, outcome, startedMs) {
3582
+ try {
3583
+ metrics?.onSandboxCreate?.({
3584
+ backend,
3585
+ outcome,
3586
+ durationSeconds: Math.max(0, (Date.now() - startedMs) / 1e3)
3587
+ });
3588
+ } catch {
3589
+ }
3590
+ }
3367
3591
  async function serializeEstablishedSandboxEnvelope(established) {
3368
3592
  const client = established.client;
3369
3593
  if (!client || typeof client.serializeSessionState !== "function") {
@@ -3395,6 +3619,11 @@ export {
3395
3619
  assertDescriptorRegistryInvariants,
3396
3620
  SandboxConfigError,
3397
3621
  SandboxProviderUnavailableError,
3622
+ modalSandboxAttributionEnvironment,
3623
+ modalSandboxAttributionTags,
3624
+ tagModalSandbox,
3625
+ terminateModalSandboxById,
3626
+ sweepModalOrphanSandboxes,
3398
3627
  subjectFor,
3399
3628
  SelfhostedControlError,
3400
3629
  agentErrorToControlError,
@@ -3482,4 +3711,4 @@ export {
3482
3711
  collectSandboxEnvironment2 as collectSandboxEnvironment,
3483
3712
  parseExposedPorts2 as parseExposedPorts
3484
3713
  };
3485
- //# sourceMappingURL=chunk-KNW7AMQB.js.map
3714
+ //# sourceMappingURL=chunk-HGQ252FL.js.map