@opengeni/runtime 0.14.1 → 0.14.15

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 (79) hide show
  1. package/dist/{chunk-4YJ5Z52Z.js → chunk-7E2ZSCZK.js} +6 -6
  2. package/dist/{chunk-4YJ5Z52Z.js.map → chunk-7E2ZSCZK.js.map} +1 -1
  3. package/dist/{chunk-HHWIZHQM.js → chunk-GNEGANUF.js} +942 -300
  4. package/dist/chunk-GNEGANUF.js.map +1 -0
  5. package/dist/codex-tool-search.d.ts +100 -0
  6. package/dist/context-compaction.d.ts +236 -0
  7. package/dist/history-sanitizer.d.ts +320 -0
  8. package/dist/index.d.ts +186 -718
  9. package/dist/index.js +612 -126
  10. package/dist/index.js.map +1 -1
  11. package/dist/{localSources-T3HZ6KY4.js → localSources-BXHBKGF4.js} +2 -2
  12. package/dist/mcp-network.d.ts +32 -35
  13. package/dist/metrics.d.ts +33 -0
  14. package/dist/sandbox/capabilities.d.ts +11 -0
  15. package/dist/sandbox/channel-a.d.ts +226 -0
  16. package/dist/sandbox/display-stack.d.ts +99 -0
  17. package/dist/sandbox/errors.d.ts +16 -0
  18. package/dist/sandbox/exec-banner.d.ts +19 -0
  19. package/dist/sandbox/index.d.ts +190 -6
  20. package/dist/sandbox/index.js +20 -4
  21. package/dist/sandbox/op-correlation.d.ts +24 -0
  22. package/dist/sandbox/provider-errors.d.ts +32 -0
  23. package/dist/sandbox/provider-operation-gate.d.ts +12 -0
  24. package/dist/sandbox/providers/blaxel.d.ts +2 -0
  25. package/dist/sandbox/providers/cloudflare.d.ts +2 -0
  26. package/dist/sandbox/providers/daytona.d.ts +2 -0
  27. package/dist/sandbox/providers/docker.d.ts +2 -0
  28. package/dist/sandbox/providers/e2b.d.ts +2 -0
  29. package/dist/sandbox/providers/index.d.ts +11 -0
  30. package/dist/sandbox/providers/local.d.ts +2 -0
  31. package/dist/sandbox/providers/modal.d.ts +132 -0
  32. package/dist/sandbox/providers/none.d.ts +2 -0
  33. package/dist/sandbox/providers/runloop.d.ts +2 -0
  34. package/dist/sandbox/providers/selfhosted.d.ts +2 -0
  35. package/dist/sandbox/providers/types.d.ts +30 -0
  36. package/dist/sandbox/providers/vercel.d.ts +2 -0
  37. package/dist/sandbox/recording.d.ts +80 -0
  38. package/dist/sandbox/routing/backend-resolver.d.ts +173 -0
  39. package/dist/sandbox/routing/routing-session.d.ts +401 -0
  40. package/dist/sandbox/run-credentials.d.ts +65 -0
  41. package/dist/sandbox/select.d.ts +120 -0
  42. package/dist/sandbox/selfhosted/capabilities.d.ts +91 -0
  43. package/dist/sandbox/selfhosted/control-rpc.d.ts +175 -0
  44. package/dist/sandbox/selfhosted/fault-rendering.d.ts +40 -0
  45. package/dist/sandbox/selfhosted/op-observer.d.ts +40 -0
  46. package/dist/sandbox/selfhosted/op-stream.d.ts +104 -0
  47. package/dist/sandbox/selfhosted/op-testing.d.ts +108 -0
  48. package/dist/sandbox/selfhosted/op-transport.d.ts +60 -0
  49. package/dist/sandbox/selfhosted/retry-policy.d.ts +106 -0
  50. package/dist/sandbox/selfhosted/session.d.ts +552 -0
  51. package/dist/sandbox/selfhosted/testing.d.ts +48 -0
  52. package/dist/sandbox/stream-port.d.ts +80 -0
  53. package/dist/sandbox/stream-token.d.ts +44 -0
  54. package/dist/sandbox/terminal-server.d.ts +67 -0
  55. package/dist/sandbox/toolspace-token.d.ts +23 -0
  56. package/dist/sandbox/turn-tool-cancellation.d.ts +72 -0
  57. package/dist/sandbox/workspace-archive.d.ts +39 -0
  58. package/dist/sandbox-computer.d.ts +227 -0
  59. package/dist/screenshot-error-card.d.ts +1 -0
  60. package/dist/skill-library.d.ts +10 -12
  61. package/dist/usage-telemetry.d.ts +45 -0
  62. package/dist/workspace-skills.d.ts +31 -0
  63. package/package.json +7 -7
  64. package/src/context-compaction.ts +600 -6
  65. package/src/history-sanitizer.ts +10 -92
  66. package/src/index.ts +301 -62
  67. package/src/sandbox/channel-a.ts +267 -81
  68. package/src/sandbox/index.ts +56 -94
  69. package/src/sandbox/provider-operation-gate.ts +110 -0
  70. package/src/sandbox/providers/docker.ts +13 -1
  71. package/src/sandbox/providers/modal.ts +433 -3
  72. package/src/sandbox/routing/routing-session.ts +84 -8
  73. package/src/sandbox/selfhosted/session.ts +7 -6
  74. package/src/sandbox/turn-tool-cancellation.ts +47 -7
  75. package/src/sandbox/workspace-archive.ts +176 -73
  76. package/src/sandbox-computer.ts +25 -75
  77. package/dist/chunk-HHWIZHQM.js.map +0 -1
  78. package/dist/index-BraazrAO.d.ts +0 -2931
  79. /package/dist/{localSources-T3HZ6KY4.js.map → localSources-BXHBKGF4.js.map} +0 -0
@@ -1,11 +1,23 @@
1
- import { ModalImageSelector, ModalSandboxClient } from "@openai/agents-extensions/sandbox/modal";
1
+ import {
2
+ ModalImageSelector,
3
+ ModalSandboxClient,
4
+ type ModalSandboxSession,
5
+ type ModalSandboxSessionState,
6
+ } from "@openai/agents-extensions/sandbox/modal";
2
7
  import { effectiveModalIdleTimeoutSeconds } from "@opengeni/config";
3
8
  import type { Settings } from "@opengeni/config";
9
+ import {
10
+ canonicalModalCheckpointProviderBinding,
11
+ type ModalCheckpointProviderBinding,
12
+ } from "@opengeni/contracts";
4
13
  import { CAPABILITY_DESCRIPTORS } from "../capabilities";
5
14
  import { SandboxConfigError } from "../errors";
6
15
  import type { ProviderRegistration } from "./types";
7
16
 
17
+ export type { ModalCheckpointProviderBinding } from "@opengeni/contracts";
18
+
8
19
  const MODAL_ORPHAN_SWEEP_LIMIT = 50;
20
+ const OPENGENI_MODAL_SDK_VERSION = "0.9.0";
9
21
  // A provider box is invisible to the lease until Modal create + manifest
10
22
  // materialization returns and the creation callback records its instance id.
11
23
  // Production baseline (2026-07-15, all 8 turn workers): 155/155 completed creates
@@ -63,6 +75,194 @@ export function modalSandboxAttributionTags(
63
75
  };
64
76
  }
65
77
 
78
+ type MutableModalSnapshotSandbox = {
79
+ snapshotFilesystem?: (...args: unknown[]) => Promise<unknown>;
80
+ snapshotDirectory?: (...args: unknown[]) => Promise<unknown>;
81
+ };
82
+
83
+ type MutableModalSandboxSession = {
84
+ modal?: { version?: () => string };
85
+ sandbox?: MutableModalSnapshotSandbox;
86
+ state?: {
87
+ workspacePersistence?: string;
88
+ snapshotFilesystemTimeoutMs?: number;
89
+ };
90
+ persistWorkspace?: () => Promise<Uint8Array>;
91
+ writeStdin?: (args: {
92
+ sessionId: number;
93
+ chars?: string;
94
+ yieldTimeMs?: number;
95
+ maxOutputTokens?: number;
96
+ }) => Promise<string>;
97
+ };
98
+
99
+ const modalRetentionWrappedSessions = new WeakSet<object>();
100
+ const modalFilesystemRetentionWrappedSandboxes = new WeakSet<object>();
101
+ const modalDirectoryRetentionWrappedSandboxes = new WeakSet<object>();
102
+
103
+ const MODAL_EXEC_STDIN_WRITE_PATH =
104
+ "/modal.task_command_router.TaskCommandRouter/TaskExecStdinWrite";
105
+ const MODAL_EXEC_ALREADY_COMPLETED_DETAILS =
106
+ /^Exec has already completed; stdin is no longer accepting writes(?: \(Error code: [A-Z0-9]+\))?$/;
107
+
108
+ /**
109
+ * Modal proves that the exact exec has already terminated with a typed
110
+ * FAILED_PRECONDITION from its stdin-write RPC. Keep this deliberately
111
+ * structural and exact: other FAILED_PRECONDITION errors are not process
112
+ * lifetime authority.
113
+ */
114
+ export function isModalExecAlreadyCompletedError(error: unknown): boolean {
115
+ if (!error || typeof error !== "object") return false;
116
+ const record = error as {
117
+ name?: unknown;
118
+ path?: unknown;
119
+ code?: unknown;
120
+ details?: unknown;
121
+ };
122
+ return (
123
+ record.name === "ClientError" &&
124
+ record.path === MODAL_EXEC_STDIN_WRITE_PATH &&
125
+ record.code === 9 &&
126
+ typeof record.details === "string" &&
127
+ MODAL_EXEC_ALREADY_COMPLETED_DETAILS.test(record.details)
128
+ );
129
+ }
130
+
131
+ function assertPinnedModalSdk(session: MutableModalSandboxSession): void {
132
+ const actualVersion = session.modal?.version?.();
133
+ if (actualVersion !== OPENGENI_MODAL_SDK_VERSION) {
134
+ throw new Error(
135
+ `OpenGeni Modal snapshot compatibility requires modal@${OPENGENI_MODAL_SDK_VERSION}; ` +
136
+ `the active session reported ${actualVersion ?? "no version"}`,
137
+ );
138
+ }
139
+ }
140
+
141
+ function installModalNativeSnapshotRetention(session: MutableModalSandboxSession): void {
142
+ const persistence = session.state?.workspacePersistence;
143
+ if (persistence !== "snapshot_filesystem" && persistence !== "snapshot_directory") {
144
+ return;
145
+ }
146
+ const sandbox = session.sandbox;
147
+ if (!sandbox || typeof sandbox !== "object") {
148
+ throw new Error(`Modal ${persistence} persistence has no active provider sandbox`);
149
+ }
150
+
151
+ if (persistence === "snapshot_filesystem") {
152
+ if (modalFilesystemRetentionWrappedSandboxes.has(sandbox)) return;
153
+ const snapshotFilesystem = sandbox.snapshotFilesystem;
154
+ if (typeof snapshotFilesystem !== "function") {
155
+ throw new Error("Modal snapshot_filesystem persistence is unavailable");
156
+ }
157
+ // Agents Extensions 0.13.x still invokes the Modal 0.7 positional timeout
158
+ // signature. Modal 0.9 moved timeout into an options object and changed the
159
+ // default Image retention from indefinite to 30 days. Translate at the
160
+ // provider boundary and retain the Image until OpenGeni's artifact ledger
161
+ // proves it unreferenced and garbage-collects its exact provider id.
162
+ sandbox.snapshotFilesystem = async (legacyParams?: unknown) => {
163
+ if (legacyParams !== undefined && typeof legacyParams !== "number") {
164
+ throw new Error("Unexpected Modal snapshot_filesystem adapter call shape");
165
+ }
166
+ return await snapshotFilesystem.call(sandbox, {
167
+ ...(legacyParams === undefined ? {} : { timeoutMs: legacyParams }),
168
+ ttlMs: null,
169
+ });
170
+ };
171
+ modalFilesystemRetentionWrappedSandboxes.add(sandbox);
172
+ return;
173
+ }
174
+
175
+ if (modalDirectoryRetentionWrappedSandboxes.has(sandbox)) return;
176
+ const snapshotDirectory = sandbox.snapshotDirectory;
177
+ if (typeof snapshotDirectory !== "function") {
178
+ throw new Error("Modal snapshot_directory persistence is unavailable");
179
+ }
180
+ sandbox.snapshotDirectory = async (path?: unknown, legacyParams?: unknown) => {
181
+ if (typeof path !== "string" || legacyParams !== undefined) {
182
+ throw new Error("Unexpected Modal snapshot_directory adapter call shape");
183
+ }
184
+ return await snapshotDirectory.call(sandbox, path, {
185
+ ...(session.state?.snapshotFilesystemTimeoutMs === undefined
186
+ ? {}
187
+ : { timeoutMs: session.state.snapshotFilesystemTimeoutMs }),
188
+ ttlMs: null,
189
+ });
190
+ };
191
+ modalDirectoryRetentionWrappedSandboxes.add(sandbox);
192
+ }
193
+
194
+ function installModalExecCompletionRecovery(session: MutableModalSandboxSession): void {
195
+ const writeStdin = session.writeStdin;
196
+ if (typeof writeStdin !== "function") return;
197
+ session.writeStdin = async (args) => {
198
+ try {
199
+ return await writeStdin.call(session, args);
200
+ } catch (error) {
201
+ if (
202
+ !isModalExecAlreadyCompletedError(error) ||
203
+ !Number.isSafeInteger(args.sessionId) ||
204
+ args.sessionId <= 0
205
+ ) {
206
+ throw error;
207
+ }
208
+ // Agents Extensions checks its local active-process map before writing,
209
+ // but the process can finish before Modal receives TaskExecStdinWrite.
210
+ // An empty retry performs no side effect: it lets the adapter observe the
211
+ // already-terminal process, delete its stale map entry, and return the
212
+ // ordinary exact exit banner consumed by OpenGeni's durable settlement.
213
+ // If that cleanup poll itself loses transport, the original typed
214
+ // completion is still authoritative and the canonical lost-session
215
+ // result lets OpenGeni close the exact retained process without failing
216
+ // the turn or replaying stdin.
217
+ try {
218
+ return await writeStdin.call(session, { ...args, chars: "" });
219
+ } catch {
220
+ return `write_stdin failed: session not found: ${args.sessionId}`;
221
+ }
222
+ }
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Bridge the pinned Agents Extensions Modal adapter to Modal 0.9's provider
228
+ * contracts. The wrapper re-checks the private provider sandbox on every
229
+ * capture because snapshot_filesystem hydration replaces that object.
230
+ */
231
+ export function installOpenGeniModalSnapshotPolicy<T extends object>(session: T): T {
232
+ const mutable = session as MutableModalSandboxSession;
233
+ if (modalRetentionWrappedSessions.has(session)) return session;
234
+ if (typeof mutable.persistWorkspace !== "function") {
235
+ throw new Error("Modal session does not expose workspace persistence");
236
+ }
237
+ assertPinnedModalSdk(mutable);
238
+ installModalNativeSnapshotRetention(mutable);
239
+ installModalExecCompletionRecovery(mutable);
240
+
241
+ const persistWorkspace = mutable.persistWorkspace.bind(session);
242
+ mutable.persistWorkspace = async () => {
243
+ assertPinnedModalSdk(mutable);
244
+ installModalNativeSnapshotRetention(mutable);
245
+ return await persistWorkspace();
246
+ };
247
+ modalRetentionWrappedSessions.add(session);
248
+ return session;
249
+ }
250
+
251
+ export class OpenGeniModalSandboxClient extends ModalSandboxClient {
252
+ override async create(
253
+ args?: Parameters<ModalSandboxClient["create"]>[0],
254
+ manifestOptions?: Parameters<ModalSandboxClient["create"]>[1],
255
+ ): Promise<ModalSandboxSession> {
256
+ const session = await super.create(args, manifestOptions);
257
+ return installOpenGeniModalSnapshotPolicy(session);
258
+ }
259
+
260
+ override async resume(state: ModalSandboxSessionState): Promise<ModalSandboxSession> {
261
+ const session = await super.resume(state);
262
+ return installOpenGeniModalSnapshotPolicy(session);
263
+ }
264
+ }
265
+
66
266
  export const modalProvider: ProviderRegistration = {
67
267
  backend: "modal",
68
268
  descriptor: CAPABILITY_DESCRIPTORS.modal,
@@ -116,7 +316,7 @@ export const modalProvider: ProviderRegistration = {
116
316
  if (settings.modalEnvironment) {
117
317
  options.environment = settings.modalEnvironment;
118
318
  }
119
- return new ModalSandboxClient(options);
319
+ return new OpenGeniModalSandboxClient(options);
120
320
  },
121
321
  };
122
322
 
@@ -241,7 +441,6 @@ function modalClientOptions(
241
441
  ...(settings.modalTokenId ? { tokenId: settings.modalTokenId } : {}),
242
442
  ...(settings.modalTokenSecret ? { tokenSecret: settings.modalTokenSecret } : {}),
243
443
  ...(settings.modalEnvironment ? { environment: settings.modalEnvironment } : {}),
244
- ...(settings.modalTimeoutSeconds ? { timeoutMs: settings.modalTimeoutSeconds * 1000 } : {}),
245
444
  };
246
445
  }
247
446
 
@@ -250,6 +449,237 @@ async function createModalClient(settings: Settings): Promise<ModalClientLike> {
250
449
  return new modal.ModalClient(modalClientOptions(settings));
251
450
  }
252
451
 
452
+ function isModalNotFoundError(error: unknown): boolean {
453
+ const candidate = error as {
454
+ name?: unknown;
455
+ code?: unknown;
456
+ };
457
+ // The pinned Modal SDK normalizes image/sandbox NOT_FOUND and its documented
458
+ // FAILED_PRECONDITION alias to NotFoundError. Numeric gRPC code 5 is retained
459
+ // for narrow adapter/fake compatibility. Never classify message text: an auth,
460
+ // workspace, DNS, or proxy error containing "not found" is not deletion proof.
461
+ return candidate?.name === "NotFoundError" || candidate?.code === 5;
462
+ }
463
+
464
+ async function modalCheckpointProviderBindingForClient(
465
+ settings: Settings,
466
+ modal: ModalClientLike,
467
+ ): Promise<ModalCheckpointProviderBinding> {
468
+ const identity = await modal.cpClient.workspaceNameLookup({});
469
+ const workspaceName = identity.workspaceName || identity.username;
470
+ if (!workspaceName) {
471
+ throw new Error("Modal credential resolved no workspace identity");
472
+ }
473
+ const resolved = canonicalModalCheckpointProviderBinding({
474
+ version: 1,
475
+ serverUrl: modal.profile.serverUrl,
476
+ workspaceName,
477
+ // Resolve through the authenticated client, not the optional OpenGeni
478
+ // override alone. When the override is absent Modal may select a profile
479
+ // environment; persisting "" would fail to fence a later profile change.
480
+ environment: modal.environmentName(settings.modalEnvironment),
481
+ });
482
+ if (!resolved) {
483
+ throw new Error("Modal credential resolved an invalid checkpoint provider identity");
484
+ }
485
+ return resolved.binding;
486
+ }
487
+
488
+ const modalSessionCheckpointBindings = new WeakMap<
489
+ object,
490
+ Promise<{ key: string; binding: ModalCheckpointProviderBinding }>
491
+ >();
492
+
493
+ /**
494
+ * Resolve checkpoint ownership through the exact authenticated Modal client
495
+ * embedded in the session that creates the snapshot. The field is private in
496
+ * the pinned Agents extension, so this adapter is intentionally fail-closed:
497
+ * an upstream shape change disables native publication instead of guessing
498
+ * ownership from a newly-resolved ambient profile.
499
+ */
500
+ export async function resolveModalCheckpointProviderBindingForSession(
501
+ settings: Settings,
502
+ session: unknown,
503
+ ): Promise<{ key: string; binding: ModalCheckpointProviderBinding }> {
504
+ if (!session || typeof session !== "object") {
505
+ throw new Error("Modal checkpoint session identity is unavailable");
506
+ }
507
+ const cached = modalSessionCheckpointBindings.get(session);
508
+ if (cached) return await cached;
509
+ const modal = (session as { modal?: unknown }).modal as ModalClientLike | undefined;
510
+ if (
511
+ !modal ||
512
+ typeof modal.cpClient?.workspaceNameLookup !== "function" ||
513
+ typeof modal.profile?.serverUrl !== "string" ||
514
+ typeof modal.environmentName !== "function"
515
+ ) {
516
+ throw new Error("Pinned Modal session no longer exposes its authenticated checkpoint identity");
517
+ }
518
+ const pending = modalCheckpointProviderBindingForClient(settings, modal).then(
519
+ (binding) => canonicalModalCheckpointProviderBinding(binding)!,
520
+ );
521
+ modalSessionCheckpointBindings.set(session, pending);
522
+ try {
523
+ return await pending;
524
+ } catch (error) {
525
+ modalSessionCheckpointBindings.delete(session);
526
+ throw error;
527
+ }
528
+ }
529
+
530
+ /** Compare a restore receipt against the exact authenticated Modal client
531
+ * embedded in the created session. This deliberately does not resolve a second
532
+ * ambient client: the session that will consume the snapshot is the authority. */
533
+ export async function modalSessionMatchesCheckpointProviderBinding(
534
+ settings: Settings,
535
+ session: unknown,
536
+ expectedBindingKey: string,
537
+ ): Promise<boolean> {
538
+ const identity = await resolveModalCheckpointProviderBindingForSession(settings, session);
539
+ return identity.key === expectedBindingKey;
540
+ }
541
+
542
+ /** Resolve the authoritative Modal workspace behind the configured credential.
543
+ * Snapshot ids are workspace-scoped; persisting this non-secret binding lets GC
544
+ * reject a credential rotation that points at a different workspace instead of
545
+ * issuing a destructive call under ambient credentials. */
546
+ export async function resolveModalCheckpointProviderBinding(
547
+ settings: Settings,
548
+ createClient: (settings: Settings) => Promise<ModalClientLike> = createModalClient,
549
+ ): Promise<{ key: string; binding: ModalCheckpointProviderBinding }> {
550
+ const modal = await createClient(settings);
551
+ try {
552
+ const binding = await modalCheckpointProviderBindingForClient(settings, modal);
553
+ return canonicalModalCheckpointProviderBinding(binding)!;
554
+ } finally {
555
+ modal.close();
556
+ }
557
+ }
558
+
559
+ /** Prove a legacy lease's live sandbox is visible in the same Modal namespace
560
+ * whose identity will own the adopted checkpoint row. */
561
+ export async function resolveModalCheckpointProviderBindingForLiveSandbox(
562
+ settings: Settings,
563
+ sandboxId: string,
564
+ createClient: (settings: Settings) => Promise<ModalClientLike> = createModalClient,
565
+ ): Promise<{ key: string; binding: ModalCheckpointProviderBinding }> {
566
+ if (!sandboxId) throw new Error("Modal live-sandbox identity requires a sandbox id");
567
+ const modal = await createClient(settings);
568
+ try {
569
+ const binding = await modalCheckpointProviderBindingForClient(settings, modal);
570
+ const sandbox = await modal.sandboxes.fromId(sandboxId);
571
+ const exitCode = await sandbox.poll();
572
+ if (exitCode !== null) {
573
+ throw new Error(`Modal sandbox ${sandboxId} is no longer running`);
574
+ }
575
+ return canonicalModalCheckpointProviderBinding(binding)!;
576
+ } finally {
577
+ modal.close();
578
+ }
579
+ }
580
+
581
+ /**
582
+ * Observe one exact historical Modal sandbox without consulting the current
583
+ * lease or its (possibly successor-owned) resume envelope. This is deliberately
584
+ * lifecycle-only: a running sandbox does not prove whether an individual
585
+ * retained process is still running, while a terminal/missing sandbox proves
586
+ * that none of its processes can still execute.
587
+ */
588
+ export async function inspectModalSandboxLifecycle(
589
+ settings: Settings,
590
+ sandboxId: string,
591
+ expectedBindingKey?: string | null,
592
+ createClient: (settings: Settings) => Promise<ModalClientLike> = createModalClient,
593
+ ): Promise<
594
+ | {
595
+ status: "running";
596
+ providerBindingKey: string;
597
+ providerBinding: ModalCheckpointProviderBinding;
598
+ }
599
+ | {
600
+ status: "terminated";
601
+ exitCode: number;
602
+ providerBindingKey: string;
603
+ providerBinding: ModalCheckpointProviderBinding;
604
+ }
605
+ | {
606
+ status: "not_found";
607
+ providerBindingKey: string;
608
+ providerBinding: ModalCheckpointProviderBinding;
609
+ }
610
+ > {
611
+ if (!sandboxId) throw new Error("Modal lifecycle inspection requires a sandbox id");
612
+ const modal = await createClient(settings);
613
+ try {
614
+ const binding = canonicalModalCheckpointProviderBinding(
615
+ await modalCheckpointProviderBindingForClient(settings, modal),
616
+ )!;
617
+ if (expectedBindingKey && binding.key !== expectedBindingKey) {
618
+ throw new Error(
619
+ "Modal lifecycle inspection refused because the configured credential workspace changed",
620
+ );
621
+ }
622
+ try {
623
+ const sandbox = await modal.sandboxes.fromId(sandboxId);
624
+ const exitCode = await sandbox.poll();
625
+ return exitCode === null
626
+ ? {
627
+ status: "running",
628
+ providerBindingKey: binding.key,
629
+ providerBinding: binding.binding,
630
+ }
631
+ : {
632
+ status: "terminated",
633
+ exitCode,
634
+ providerBindingKey: binding.key,
635
+ providerBinding: binding.binding,
636
+ };
637
+ } catch (error) {
638
+ if (isModalNotFoundError(error)) {
639
+ return {
640
+ status: "not_found",
641
+ providerBindingKey: binding.key,
642
+ providerBinding: binding.binding,
643
+ };
644
+ }
645
+ throw error;
646
+ }
647
+ } finally {
648
+ modal.close();
649
+ }
650
+ }
651
+
652
+ export async function deleteModalCheckpointSnapshot(
653
+ settings: Settings,
654
+ expectedBindingKey: string,
655
+ snapshotId: string,
656
+ createClient: (settings: Settings) => Promise<ModalClientLike> = createModalClient,
657
+ ): Promise<"deleted" | "not_found"> {
658
+ if (!snapshotId) throw new Error("Modal checkpoint deletion requires a snapshot id");
659
+ // Resolve identity and delete through the same authenticated client. A cached
660
+ // or separately-created identity probe could approve workspace A and then
661
+ // issue the destructive call through a rotated ambient profile for workspace B.
662
+ const modal = await createClient(settings);
663
+ try {
664
+ const binding = await modalCheckpointProviderBindingForClient(settings, modal);
665
+ const identity = canonicalModalCheckpointProviderBinding(binding)!;
666
+ if (expectedBindingKey !== identity.key) {
667
+ throw new Error(
668
+ "Modal checkpoint deletion refused because the configured credential workspace changed",
669
+ );
670
+ }
671
+ try {
672
+ await modal.images.delete(snapshotId);
673
+ return "deleted";
674
+ } catch (error) {
675
+ if (isModalNotFoundError(error)) return "not_found";
676
+ throw error;
677
+ }
678
+ } finally {
679
+ modal.close();
680
+ }
681
+ }
682
+
253
683
  export async function tagModalSandbox(
254
684
  settings: Settings,
255
685
  sandboxId: string,
@@ -32,6 +32,7 @@ import { SelfhostedControlError } from "../selfhosted/control-rpc";
32
32
  import { renderSelfhostedFault } from "../selfhosted/fault-rendering";
33
33
  import { isExecSessionLostBanner } from "../channel-a";
34
34
  import { parseExecBannerExitCode, parseExecBannerSessionId } from "../exec-banner";
35
+ import { withSandboxProviderOperation } from "../provider-operation-gate";
35
36
 
36
37
  /** The per-session active-sandbox pointer the proxy re-reads on every op. Mirror
37
38
  * of `@opengeni/db`'s `ActiveSandboxPointer` (structural, so the leaf does not
@@ -794,7 +795,9 @@ export class RoutingSandboxSession implements RoutableBackendSession {
794
795
  if (!write) throw new RoutingUnsupportedError(op, record.backend.kind);
795
796
  let result: string;
796
797
  try {
797
- result = await write.call(record.backend.session, args);
798
+ result = await withSandboxProviderOperation(record.backend.session, () =>
799
+ write.call(record.backend.session, args),
800
+ );
798
801
  } catch (error) {
799
802
  if (this.deps.afterProcessMutation) {
800
803
  const pending: PendingProcessMutationSettlement = {
@@ -856,7 +859,9 @@ export class RoutingSandboxSession implements RoutableBackendSession {
856
859
  await this.ensureParentPromotion(record);
857
860
  const write = record.backend.session.writeStdin;
858
861
  if (!write) throw new RoutingUnsupportedError("writeStdin", record.backend.kind);
859
- const result = await write.call(record.backend.session, args);
862
+ const result = await withSandboxProviderOperation(record.backend.session, () =>
863
+ write.call(record.backend.session, args),
864
+ );
860
865
  const proof = retainedProcessTerminalProof(result, providerSessionId);
861
866
  if (proof) await this.settleRetainedProcess(record, proof, result);
862
867
  return result;
@@ -890,7 +895,7 @@ export class RoutingSandboxSession implements RoutableBackendSession {
890
895
  : undefined;
891
896
  let result: T;
892
897
  try {
893
- result = await fn(backend.session);
898
+ result = await withSandboxProviderOperation(backend.session, () => fn(backend.session));
894
899
  } catch (error) {
895
900
  if (mutatesWorkspace && this.deps.afterMutation) {
896
901
  try {
@@ -1162,11 +1167,19 @@ export class RoutingSandboxSession implements RoutableBackendSession {
1162
1167
  return terminal.result;
1163
1168
  }
1164
1169
  await this.ensureParentPromotion(record);
1165
- if (record.backend.session.execCommand) {
1166
- return await record.backend.session.execCommand(args);
1170
+ const execCommand = record.backend.session.execCommand;
1171
+ if (execCommand) {
1172
+ return await withSandboxProviderOperation(record.backend.session, () =>
1173
+ execCommand.call(record.backend.session, args),
1174
+ );
1167
1175
  }
1168
- if (record.backend.session.exec) {
1169
- return formatExecResult(await record.backend.session.exec(args));
1176
+ const exec = record.backend.session.exec;
1177
+ if (exec) {
1178
+ return formatExecResult(
1179
+ await withSandboxProviderOperation(record.backend.session, () =>
1180
+ exec.call(record.backend.session, args),
1181
+ ),
1182
+ );
1170
1183
  }
1171
1184
  throw new RoutingUnsupportedError("execCommand", record.backend.kind);
1172
1185
  }
@@ -1228,7 +1241,22 @@ export class RoutingSandboxSession implements RoutableBackendSession {
1228
1241
  if (!s.materializeEntry) {
1229
1242
  throw new RoutingUnsupportedError("materializeEntry", this.cached?.kind ?? "unknown");
1230
1243
  }
1231
- return s.materializeEntry(args);
1244
+ await s.materializeEntry(args);
1245
+
1246
+ // The local Docker SDK materializes through a host path and then bind-mounts
1247
+ // that path into the sandbox. A containerized worker using the host Docker
1248
+ // socket can otherwise write into its own container layer while the sandbox
1249
+ // sees a different, empty host directory. `pathExists()` and `readFile()` on
1250
+ // that SDK session inspect the worker-side path, so only a command executed
1251
+ // inside the actual sandbox proves the materialized path is usable by the
1252
+ // agent. Never let lazy `load_skill` report success on a split workspace.
1253
+ const path =
1254
+ args && typeof args === "object" && typeof (args as { path?: unknown }).path === "string"
1255
+ ? (args as { path: string }).path
1256
+ : null;
1257
+ if (path) {
1258
+ await assertProviderCanSeeMaterializedPath(s, path);
1259
+ }
1232
1260
  });
1233
1261
  }
1234
1262
 
@@ -1304,3 +1332,51 @@ export class RoutingSandboxSession implements RoutableBackendSession {
1304
1332
  return this.resolve();
1305
1333
  }
1306
1334
  }
1335
+
1336
+ const MATERIALIZED_PATH_MARKER = "__OPENGENI_MATERIALIZED_PATH_VISIBLE__";
1337
+
1338
+ async function assertProviderCanSeeMaterializedPath(
1339
+ session: RoutableBackendSession,
1340
+ path: string,
1341
+ ): Promise<void> {
1342
+ const command = `test -e ${shellSingleQuote(path)} && printf %s ${shellSingleQuote(
1343
+ MATERIALIZED_PATH_MARKER,
1344
+ )}`;
1345
+ const args = {
1346
+ cmd: command,
1347
+ workdir: providerManifestRoot(session) ?? "/workspace",
1348
+ shell: "sh",
1349
+ login: false,
1350
+ tty: false,
1351
+ };
1352
+ let output: string;
1353
+ if (session.execCommand) {
1354
+ output = await session.execCommand(args);
1355
+ } else if (session.exec) {
1356
+ output = formatExecResult(await session.exec(args));
1357
+ } else {
1358
+ throw new RoutingUnsupportedError("materializeEntry.verify", "unknown");
1359
+ }
1360
+ if (!output.includes(MATERIALIZED_PATH_MARKER)) {
1361
+ throw new Error(
1362
+ `Sandbox materialization completed but the provider cannot read the destination path: ${path}`,
1363
+ );
1364
+ }
1365
+ }
1366
+
1367
+ function providerManifestRoot(session: RoutableBackendSession): string | null {
1368
+ const root = (
1369
+ session.state as
1370
+ | {
1371
+ manifest?: {
1372
+ root?: unknown;
1373
+ };
1374
+ }
1375
+ | undefined
1376
+ )?.manifest?.root;
1377
+ return typeof root === "string" && root.length > 0 ? root : null;
1378
+ }
1379
+
1380
+ function shellSingleQuote(value: string): string {
1381
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
1382
+ }
@@ -569,10 +569,11 @@ export class SelfhostedSession {
569
569
  }
570
570
  }
571
571
 
572
- /** The clamped exec process deadline (ms): the exec-specific budget when threaded,
573
- * else the control timeout. Shared by `exec()` (the wire deadline) and
574
- * `execCommand()` (the timed-out hint's "N-second limit"), so the two never drift. */
575
- private execDeadlineMs(): number {
572
+ /** The effective clamped exec process deadline (ms): the exec-specific budget
573
+ * when threaded, else the control timeout. Public so adapters that project a
574
+ * structured command receipt can report the exact deadline the agent enforces
575
+ * instead of repeating (and potentially drifting from) this normalization. */
576
+ get effectiveExecDeadlineMs(): number {
576
577
  // exec gets its OWN (much larger) deadline distinct from the short control
577
578
  // timeout — a real command routinely outlives 30s. Falls back to `timeoutMs`
578
579
  // when no exec deadline is threaded (unchanged for those callers).
@@ -587,7 +588,7 @@ export class SelfhostedSession {
587
588
  // Keep the process deadline inside the request/reply deadline. Previously the
588
589
  // wire carried timeoutMs=0 (unbounded) while the caller stopped waiting after
589
590
  // ~30s, leaving accepted work invisible and able to starve control liveness.
590
- const executionTimeoutMs = this.execDeadlineMs();
591
+ const executionTimeoutMs = this.effectiveExecDeadlineMs;
591
592
  const execReq: ExecRequest = {
592
593
  // The agent does NOT shell-interpret unless `shell` — Channel-A passes a
593
594
  // single shell command string, so run it through the platform shell.
@@ -736,7 +737,7 @@ export class SelfhostedSession {
736
737
  async execCommand(args: { cmd: string; workdir?: string; runAs?: string }): Promise<string> {
737
738
  const result = await this.exec({ cmd: args.cmd, workdir: args.workdir, runAs: args.runAs });
738
739
  if (result.timedOut) {
739
- const hint = execDeadlineHint(Math.round(this.execDeadlineMs() / 1000));
740
+ const hint = execDeadlineHint(Math.round(this.effectiveExecDeadlineMs / 1000));
740
741
  return result.output ? `${result.output}\n${hint}` : hint;
741
742
  }
742
743
  return result.output;