@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.
- package/dist/{chunk-4YJ5Z52Z.js → chunk-7E2ZSCZK.js} +6 -6
- package/dist/{chunk-4YJ5Z52Z.js.map → chunk-7E2ZSCZK.js.map} +1 -1
- package/dist/{chunk-HHWIZHQM.js → chunk-GNEGANUF.js} +942 -300
- package/dist/chunk-GNEGANUF.js.map +1 -0
- package/dist/codex-tool-search.d.ts +100 -0
- package/dist/context-compaction.d.ts +236 -0
- package/dist/history-sanitizer.d.ts +320 -0
- package/dist/index.d.ts +186 -718
- package/dist/index.js +612 -126
- package/dist/index.js.map +1 -1
- package/dist/{localSources-T3HZ6KY4.js → localSources-BXHBKGF4.js} +2 -2
- package/dist/mcp-network.d.ts +32 -35
- package/dist/metrics.d.ts +33 -0
- package/dist/sandbox/capabilities.d.ts +11 -0
- package/dist/sandbox/channel-a.d.ts +226 -0
- package/dist/sandbox/display-stack.d.ts +99 -0
- package/dist/sandbox/errors.d.ts +16 -0
- package/dist/sandbox/exec-banner.d.ts +19 -0
- package/dist/sandbox/index.d.ts +190 -6
- package/dist/sandbox/index.js +20 -4
- package/dist/sandbox/op-correlation.d.ts +24 -0
- package/dist/sandbox/provider-errors.d.ts +32 -0
- package/dist/sandbox/provider-operation-gate.d.ts +12 -0
- package/dist/sandbox/providers/blaxel.d.ts +2 -0
- package/dist/sandbox/providers/cloudflare.d.ts +2 -0
- package/dist/sandbox/providers/daytona.d.ts +2 -0
- package/dist/sandbox/providers/docker.d.ts +2 -0
- package/dist/sandbox/providers/e2b.d.ts +2 -0
- package/dist/sandbox/providers/index.d.ts +11 -0
- package/dist/sandbox/providers/local.d.ts +2 -0
- package/dist/sandbox/providers/modal.d.ts +132 -0
- package/dist/sandbox/providers/none.d.ts +2 -0
- package/dist/sandbox/providers/runloop.d.ts +2 -0
- package/dist/sandbox/providers/selfhosted.d.ts +2 -0
- package/dist/sandbox/providers/types.d.ts +30 -0
- package/dist/sandbox/providers/vercel.d.ts +2 -0
- package/dist/sandbox/recording.d.ts +80 -0
- package/dist/sandbox/routing/backend-resolver.d.ts +173 -0
- package/dist/sandbox/routing/routing-session.d.ts +401 -0
- package/dist/sandbox/run-credentials.d.ts +65 -0
- package/dist/sandbox/select.d.ts +120 -0
- package/dist/sandbox/selfhosted/capabilities.d.ts +91 -0
- package/dist/sandbox/selfhosted/control-rpc.d.ts +175 -0
- package/dist/sandbox/selfhosted/fault-rendering.d.ts +40 -0
- package/dist/sandbox/selfhosted/op-observer.d.ts +40 -0
- package/dist/sandbox/selfhosted/op-stream.d.ts +104 -0
- package/dist/sandbox/selfhosted/op-testing.d.ts +108 -0
- package/dist/sandbox/selfhosted/op-transport.d.ts +60 -0
- package/dist/sandbox/selfhosted/retry-policy.d.ts +106 -0
- package/dist/sandbox/selfhosted/session.d.ts +552 -0
- package/dist/sandbox/selfhosted/testing.d.ts +48 -0
- package/dist/sandbox/stream-port.d.ts +80 -0
- package/dist/sandbox/stream-token.d.ts +44 -0
- package/dist/sandbox/terminal-server.d.ts +67 -0
- package/dist/sandbox/toolspace-token.d.ts +23 -0
- package/dist/sandbox/turn-tool-cancellation.d.ts +72 -0
- package/dist/sandbox/workspace-archive.d.ts +39 -0
- package/dist/sandbox-computer.d.ts +227 -0
- package/dist/screenshot-error-card.d.ts +1 -0
- package/dist/skill-library.d.ts +10 -12
- package/dist/usage-telemetry.d.ts +45 -0
- package/dist/workspace-skills.d.ts +31 -0
- package/package.json +7 -7
- package/src/context-compaction.ts +600 -6
- package/src/history-sanitizer.ts +10 -92
- package/src/index.ts +301 -62
- package/src/sandbox/channel-a.ts +267 -81
- package/src/sandbox/index.ts +56 -94
- package/src/sandbox/provider-operation-gate.ts +110 -0
- package/src/sandbox/providers/docker.ts +13 -1
- package/src/sandbox/providers/modal.ts +433 -3
- package/src/sandbox/routing/routing-session.ts +84 -8
- package/src/sandbox/selfhosted/session.ts +7 -6
- package/src/sandbox/turn-tool-cancellation.ts +47 -7
- package/src/sandbox/workspace-archive.ts +176 -73
- package/src/sandbox-computer.ts +25 -75
- package/dist/chunk-HHWIZHQM.js.map +0 -1
- package/dist/index-BraazrAO.d.ts +0 -2931
- /package/dist/{localSources-T3HZ6KY4.js.map → localSources-BXHBKGF4.js.map} +0 -0
|
@@ -1,11 +1,23 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1166
|
-
|
|
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
|
-
|
|
1169
|
-
|
|
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
|
-
|
|
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
|
|
573
|
-
* else the control timeout.
|
|
574
|
-
*
|
|
575
|
-
|
|
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.
|
|
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.
|
|
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;
|