@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
@@ -308,15 +308,53 @@ function shellMarkerPath(token: string): string {
308
308
  return `${SHELL_MARKER_DIR}/${token}`;
309
309
  }
310
310
 
311
+ function processInspectionCommandLines(): string[] {
312
+ return [
313
+ "__opengeni_process_group_id() {",
314
+ ' __opengeni_lookup_pid="$1"',
315
+ ' __opengeni_lookup_pgid=""',
316
+ " if command -v ps >/dev/null 2>&1; then",
317
+ ' __opengeni_lookup_pgid="$(command ps -o pgid= -p "$__opengeni_lookup_pid" 2>/dev/null | command tr -d \'[:space:]\')"',
318
+ " fi",
319
+ ' case "$__opengeni_lookup_pgid" in',
320
+ ' ""|*[!0-9]*) ;;',
321
+ " *) command printf '%s\\n' \"$__opengeni_lookup_pgid\"; return 0 ;;",
322
+ " esac",
323
+ ' [ -r "/proc/$__opengeni_lookup_pid/stat" ] || return 1',
324
+ ' IFS= read -r __opengeni_lookup_stat < "/proc/$__opengeni_lookup_pid/stat" || return 1',
325
+ ' __opengeni_lookup_tail="${__opengeni_lookup_stat##*) }"',
326
+ ' [ "$__opengeni_lookup_tail" != "$__opengeni_lookup_stat" ] || return 1',
327
+ " set -- $__opengeni_lookup_tail",
328
+ ' [ "$#" -ge 3 ] || return 1',
329
+ ' case "$3" in ""|*[!0-9]*) return 1 ;; esac',
330
+ " command printf '%s\\n' \"$3\"",
331
+ "}",
332
+ "__opengeni_process_args() {",
333
+ ' __opengeni_lookup_pid="$1"',
334
+ ' __opengeni_lookup_args=""',
335
+ " if command -v ps >/dev/null 2>&1; then",
336
+ ' __opengeni_lookup_args="$(command ps -ww -o args= -p "$__opengeni_lookup_pid" 2>/dev/null)"',
337
+ " fi",
338
+ ' if [ -n "$__opengeni_lookup_args" ]; then',
339
+ " command printf '%s\\n' \"$__opengeni_lookup_args\"",
340
+ " return 0",
341
+ " fi",
342
+ ' [ -r "/proc/$__opengeni_lookup_pid/cmdline" ] || return 1',
343
+ " command tr '\\000' ' ' < \"/proc/$__opengeni_lookup_pid/cmdline\"",
344
+ "}",
345
+ ];
346
+ }
347
+
311
348
  function cancellableGroupLeaderCommand(command: string, markerPath: string): string {
312
349
  const marker = singleQuote(markerPath);
313
350
  const markerDir = singleQuote(SHELL_MARKER_DIR);
314
351
  return [
352
+ ...processInspectionCommandLines(),
315
353
  `__opengeni_marker=${marker}`,
316
354
  "umask 077",
317
355
  `command mkdir -p ${markerDir} || exit 125`,
318
356
  '__opengeni_pid="$$"',
319
- '__opengeni_pgid="$(command ps -o pgid= -p "$__opengeni_pid" 2>/dev/null | command tr -d \'[:space:]\')"',
357
+ '__opengeni_pgid="$(__opengeni_process_group_id "$__opengeni_pid")"',
320
358
  'case "$__opengeni_pid:$__opengeni_pgid" in *[!0-9:]*|*:|:*) exit 125 ;; esac',
321
359
  '[ "$__opengeni_pid" -gt 1 ] && [ "$__opengeni_pgid" -gt 1 ] || exit 125',
322
360
  // Never signal a provider/container-wide process group. A cancellable PTY
@@ -335,8 +373,9 @@ function cancellableGroupLeaderCommand(command: string, markerPath: string): str
335
373
  export function cancellableShellCommand(command: string, markerPath: string): string {
336
374
  const groupLeaderCommand = cancellableGroupLeaderCommand(command, markerPath);
337
375
  return [
376
+ ...processInspectionCommandLines(),
338
377
  '__opengeni_outer_pid="$$"',
339
- '__opengeni_outer_pgid="$(command ps -o pgid= -p "$__opengeni_outer_pid" 2>/dev/null | command tr -d \'[:space:]\')"',
378
+ '__opengeni_outer_pgid="$(__opengeni_process_group_id "$__opengeni_outer_pid")"',
340
379
  'case "$__opengeni_outer_pid:$__opengeni_outer_pgid" in *[!0-9:]*|*:|:*) exit 125 ;; esac',
341
380
  'if [ "$__opengeni_outer_pid" != "$__opengeni_outer_pgid" ]; then',
342
381
  ' __opengeni_setsid="$(command -v setsid 2>/dev/null)"',
@@ -400,15 +439,16 @@ function identityGuardScript(
400
439
  if (!identity || !state.token) return `exit ${missingIdentityExitCode}`;
401
440
  const token = singleQuote(state.token);
402
441
  return [
442
+ ...processInspectionCommandLines(),
403
443
  `__opengeni_pid=${identity.pid}`,
404
444
  `__opengeni_pgid=${identity.processGroupId}`,
405
445
  `__opengeni_token=${token}`,
406
- // The randomized token lands late in the wrapped command line. Procps
407
- // otherwise truncates `args` to the terminal width, which can make a live
408
- // group look stale and open the quiescence fence without signalling it.
409
- '__opengeni_args="$(command ps -ww -o args= -p "$__opengeni_pid" 2>/dev/null)"',
446
+ // The randomized token lands late in the wrapped command line. Procps needs
447
+ // `-ww` to avoid terminal-width truncation; minimal Linux images may omit ps,
448
+ // so the same exact command line is read from /proc instead.
449
+ `__opengeni_args="$(__opengeni_process_args "$__opengeni_pid")" || exit ${missingIdentityExitCode}`,
410
450
  'case "$__opengeni_args" in *"$__opengeni_token"*) ;; *) exit 0 ;; esac',
411
- '__opengeni_live_pgid="$(command ps -o pgid= -p "$__opengeni_pid" 2>/dev/null | command tr -d \'[:space:]\')"',
451
+ `__opengeni_live_pgid="$(__opengeni_process_group_id "$__opengeni_pid")" || exit ${missingIdentityExitCode}`,
412
452
  '[ "$__opengeni_live_pgid" = "$__opengeni_pgid" ] || exit 0',
413
453
  body,
414
454
  ].join("\n");
@@ -1,28 +1,34 @@
1
1
  import { createHash } from "node:crypto";
2
+ import {
3
+ WORKSPACE_ARCHIVE_DESCRIPTOR_VERSION,
4
+ decodeNativeSnapshotRef,
5
+ parseWorkspaceArchiveDescriptor,
6
+ type NativeSnapshotDescriptor,
7
+ type NativeSnapshotRef,
8
+ type TarWorkspaceArchiveDescriptor,
9
+ type WorkspaceArchiveDescriptor,
10
+ type WorkspaceTreeFingerprint,
11
+ } from "@opengeni/contracts";
12
+ import { withSandboxProviderCapture } from "./provider-operation-gate";
2
13
 
3
- export const WORKSPACE_ARCHIVE_DESCRIPTOR_VERSION = 1 as const;
4
-
5
- export type WorkspaceTreeFingerprint = {
6
- algorithm: "sha256";
7
- sha256: string;
8
- entryCount: number;
9
- fileCount: number;
10
- totalFileBytes: number;
11
- };
12
-
13
- export type WorkspaceArchiveDescriptor = {
14
- version: typeof WORKSPACE_ARCHIVE_DESCRIPTOR_VERSION;
15
- revision: string;
16
- archiveSha256: string;
17
- archiveBytes: number;
18
- capturedAt: string;
19
- workspace: WorkspaceTreeFingerprint;
20
- };
14
+ export {
15
+ WORKSPACE_ARCHIVE_DESCRIPTOR_VERSION,
16
+ decodeNativeSnapshotRef,
17
+ parseWorkspaceArchiveDescriptor,
18
+ type NativeSnapshotDescriptor,
19
+ type NativeSnapshotProvider,
20
+ type NativeSnapshotRef,
21
+ type TarWorkspaceArchiveDescriptor,
22
+ type WorkspaceArchiveDescriptor,
23
+ type WorkspaceTreeFingerprint,
24
+ } from "@opengeni/contracts";
21
25
 
22
26
  export type VerifiedWorkspaceArchive = {
23
27
  bytes: Uint8Array;
24
28
  base64: string;
25
29
  descriptor: WorkspaceArchiveDescriptor;
30
+ kind: "tar" | "provider_snapshot";
31
+ nativeSnapshot?: NativeSnapshotRef;
26
32
  };
27
33
 
28
34
  export type WorkspaceArchiveIntegrityCode =
@@ -33,7 +39,9 @@ export type WorkspaceArchiveIntegrityCode =
33
39
  | "archive_hydration_failed"
34
40
  | "workspace_fingerprint_unavailable"
35
41
  | "workspace_changed_during_capture"
36
- | "workspace_fingerprint_mismatch";
42
+ | "workspace_fingerprint_mismatch"
43
+ | "native_snapshot_reference_invalid"
44
+ | "native_snapshot_fallback_unverified";
37
45
 
38
46
  export class WorkspaceArchiveIntegrityError extends Error {
39
47
  readonly name = "WorkspaceArchiveIntegrityError";
@@ -61,46 +69,18 @@ type WorkspaceSession = {
61
69
  maxOutputTokens?: number;
62
70
  }) => Promise<unknown>;
63
71
  persistWorkspace?: () => Promise<Uint8Array | undefined>;
72
+ state?: {
73
+ workspacePersistence?: unknown;
74
+ manifest?: {
75
+ ephemeralPersistencePaths?: () => Set<unknown>;
76
+ };
77
+ };
64
78
  };
65
79
 
66
- const SHA256 = /^[a-f0-9]{64}$/;
67
- const REVISION = /^wa1:[0-9]{13}:[a-f0-9]{64}$/;
68
-
69
80
  function sha256(bytes: Uint8Array): string {
70
81
  return createHash("sha256").update(bytes).digest("hex");
71
82
  }
72
83
 
73
- function nonnegativeInteger(value: unknown): value is number {
74
- return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
75
- }
76
-
77
- export function parseWorkspaceArchiveDescriptor(value: unknown): WorkspaceArchiveDescriptor | null {
78
- if (!value || typeof value !== "object") return null;
79
- const candidate = value as Partial<WorkspaceArchiveDescriptor>;
80
- const workspace = candidate.workspace as Partial<WorkspaceTreeFingerprint> | undefined;
81
- if (
82
- candidate.version !== WORKSPACE_ARCHIVE_DESCRIPTOR_VERSION ||
83
- typeof candidate.revision !== "string" ||
84
- !REVISION.test(candidate.revision) ||
85
- typeof candidate.archiveSha256 !== "string" ||
86
- !SHA256.test(candidate.archiveSha256) ||
87
- !nonnegativeInteger(candidate.archiveBytes) ||
88
- candidate.archiveBytes === 0 ||
89
- typeof candidate.capturedAt !== "string" ||
90
- !Number.isFinite(Date.parse(candidate.capturedAt)) ||
91
- !workspace ||
92
- workspace.algorithm !== "sha256" ||
93
- typeof workspace.sha256 !== "string" ||
94
- !SHA256.test(workspace.sha256) ||
95
- !nonnegativeInteger(workspace.entryCount) ||
96
- !nonnegativeInteger(workspace.fileCount) ||
97
- !nonnegativeInteger(workspace.totalFileBytes)
98
- ) {
99
- return null;
100
- }
101
- return candidate as WorkspaceArchiveDescriptor;
102
- }
103
-
104
84
  function decodeBase64Strict(value: string): Uint8Array | null {
105
85
  if (value.length === 0 || value.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) {
106
86
  return null;
@@ -109,6 +89,37 @@ function decodeBase64Strict(value: string): Uint8Array | null {
109
89
  return Buffer.from(bytes).toString("base64") === value ? bytes : null;
110
90
  }
111
91
 
92
+ export function describeNativeSnapshotArchive(
93
+ bytes: Uint8Array,
94
+ capturedAtMs = Date.now(),
95
+ ): NativeSnapshotDescriptor | null {
96
+ const native = decodeNativeSnapshotRef(bytes);
97
+ if (!native) return null;
98
+ const archiveSha256 = sha256(bytes);
99
+ return {
100
+ version: WORKSPACE_ARCHIVE_DESCRIPTOR_VERSION,
101
+ kind: "provider_snapshot",
102
+ revision: `wa2:${String(capturedAtMs).padStart(13, "0")}:${archiveSha256}`,
103
+ archiveSha256,
104
+ archiveBytes: bytes.length,
105
+ capturedAt: new Date(capturedAtMs).toISOString(),
106
+ provider: native.provider,
107
+ snapshotId: native.snapshotId,
108
+ ...(native.workspacePersistence ? { workspacePersistence: native.workspacePersistence } : {}),
109
+ };
110
+ }
111
+
112
+ export function describeLegacyNativeSnapshotArchive(
113
+ base64: unknown,
114
+ observedAtMs = Date.now(),
115
+ ): { archiveBase64: string; descriptor: NativeSnapshotDescriptor } | null {
116
+ if (typeof base64 !== "string") return null;
117
+ const bytes = decodeBase64Strict(base64);
118
+ if (!bytes) return null;
119
+ const descriptor = describeNativeSnapshotArchive(bytes, observedAtMs);
120
+ return descriptor ? { archiveBase64: base64, descriptor } : null;
121
+ }
122
+
112
123
  export function readVerifiedWorkspaceArchive(
113
124
  base64: unknown,
114
125
  metadata: unknown,
@@ -147,7 +158,34 @@ export function readVerifiedWorkspaceArchive(
147
158
  `workspace archive revision ${descriptor.revision} failed SHA-256/size verification`,
148
159
  );
149
160
  }
150
- return { bytes, base64, descriptor };
161
+ const nativeSnapshot = decodeNativeSnapshotRef(bytes);
162
+ if (descriptor.version === WORKSPACE_ARCHIVE_DESCRIPTOR_VERSION) {
163
+ if (
164
+ !nativeSnapshot ||
165
+ nativeSnapshot.provider !== descriptor.provider ||
166
+ nativeSnapshot.snapshotId !== descriptor.snapshotId ||
167
+ nativeSnapshot.workspacePersistence !== descriptor.workspacePersistence
168
+ ) {
169
+ throw new WorkspaceArchiveIntegrityError(
170
+ "native_snapshot_reference_invalid",
171
+ `native snapshot receipt does not match descriptor revision ${descriptor.revision}`,
172
+ );
173
+ }
174
+ } else if (nativeSnapshot) {
175
+ throw new WorkspaceArchiveIntegrityError(
176
+ "native_snapshot_reference_invalid",
177
+ `legacy native snapshot receipt ${descriptor.revision} was not durably upgraded to a v2 provider descriptor`,
178
+ );
179
+ }
180
+ return {
181
+ bytes,
182
+ base64,
183
+ descriptor,
184
+ kind: descriptor.version === WORKSPACE_ARCHIVE_DESCRIPTOR_VERSION ? "provider_snapshot" : "tar",
185
+ ...(descriptor.version === WORKSPACE_ARCHIVE_DESCRIPTOR_VERSION && nativeSnapshot
186
+ ? { nativeSnapshot }
187
+ : {}),
188
+ };
151
189
  }
152
190
 
153
191
  function stdoutFromExecResult(value: unknown): string {
@@ -164,19 +202,47 @@ function stdoutFromExecResult(value: unknown): string {
164
202
  : "";
165
203
  }
166
204
 
167
- const WORKSPACE_FINGERPRINT_COMMAND = String.raw`bash -o pipefail -c '
168
- set -eu
205
+ function shellQuote(value: string): string {
206
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
207
+ }
208
+
209
+ function workspaceFingerprintExcludes(session: WorkspaceSession): string[] {
210
+ const paths = session.state?.manifest?.ephemeralPersistencePaths?.();
211
+ if (!paths) return [];
212
+ return [...paths]
213
+ .filter((path): path is string => typeof path === "string" && path.length > 0)
214
+ .sort((left, right) => left.localeCompare(right));
215
+ }
216
+
217
+ function workspaceFingerprintCommand(excludedPaths: readonly string[]): string {
218
+ const tarExcludes = excludedPaths.flatMap((path) => [
219
+ shellQuote(`--exclude=${path}`),
220
+ shellQuote(`--exclude=./${path}`),
221
+ ]);
222
+ const findExcludes = excludedPaths.flatMap((path) => [
223
+ `-path ${shellQuote(`./${path}`)}`,
224
+ `-path ${shellQuote(`./${path}/*`)}`,
225
+ ]);
226
+ const findPrefix =
227
+ findExcludes.length === 0 ? "" : `\\( ${findExcludes.join(" -o ")} \\) -prune -o `;
228
+ const script = String.raw`set -eu
169
229
  cd /workspace
170
- digest=$(LC_ALL=C tar --sort=name --mtime="@0" --owner=0 --group=0 --numeric-owner --format=gnu -cf - . | sha256sum | awk "{print \$1}")
171
- entries=$(find . -xdev -mindepth 1 -printf x | wc -c | tr -d " ")
172
- files=$(find . -xdev -type f -printf x | wc -c | tr -d " ")
173
- bytes=$(find . -xdev -type f -printf "%s\n" | awk "{s+=\$1} END {printf \"%.0f\", s+0}")
174
- printf "OPENGENI_WORKSPACE_FINGERPRINT_V1 %s %s %s %s\n" "$digest" "$entries" "$files" "$bytes"
175
- '`;
230
+ digest=$(LC_ALL=C tar ${tarExcludes.join(" ")} --sort=name --mtime="@0" --owner=0 --group=0 --numeric-owner --hard-dereference --format=gnu -cf - . | sha256sum | awk "{print \$1}")
231
+ entries=$(find . -xdev -mindepth 1 ${findPrefix}-printf x | wc -c | tr -d " ")
232
+ files=$(find . -xdev ${findPrefix}-type f -printf x | wc -c | tr -d " ")
233
+ bytes=$(find . -xdev ${findPrefix}-type f -printf "%s\n" | awk "{s+=\$1} END {printf \"%.0f\", s+0}")
234
+ printf "OPENGENI_WORKSPACE_FINGERPRINT_V1 %s %s %s %s\n" "$digest" "$entries" "$files" "$bytes"`;
235
+ return `bash -o pipefail -c ${shellQuote(script)}`;
236
+ }
176
237
 
177
238
  /** Hashes names, kinds, modes, symlink targets and all file bytes through one
178
- * deterministic GNU tar stream. Only the aggregate is returned; paths and file
179
- * contents never enter logs or durable metadata. */
239
+ * deterministic GNU tar stream. Hardlinks are dereferenced deliberately:
240
+ * content-preserving providers may restore them as independent files, and inode
241
+ * topology is not part of OpenGeni's workspace contract. Manifest paths that
242
+ * the provider's tar persistence deliberately excludes are pruned from both the
243
+ * digest and counters, so verification covers the exact persistent projection.
244
+ * Only the aggregate is returned; paths and file contents never enter logs or
245
+ * durable metadata. */
180
246
  export async function fingerprintSandboxWorkspace(
181
247
  session: unknown,
182
248
  ): Promise<WorkspaceTreeFingerprint> {
@@ -189,7 +255,7 @@ export async function fingerprintSandboxWorkspace(
189
255
  );
190
256
  }
191
257
  const result = await run.call(target, {
192
- cmd: WORKSPACE_FINGERPRINT_COMMAND,
258
+ cmd: workspaceFingerprintCommand(workspaceFingerprintExcludes(target)),
193
259
  yieldTimeMs: 120_000,
194
260
  maxOutputTokens: 1_000,
195
261
  });
@@ -230,12 +296,22 @@ function fingerprintsEqual(a: WorkspaceTreeFingerprint, b: WorkspaceTreeFingerpr
230
296
  );
231
297
  }
232
298
 
233
- /** Capture is accepted only when the complete tree is byte-identical immediately
234
- * before and after the provider snapshot. A concurrent mutation makes the
235
- * candidate non-durable and it is never folded onto the lease. */
299
+ /** Provider-native persistence returns an opaque receipt and is accepted under
300
+ * the provider/artifact publication fence. A real tar capture is accepted only
301
+ * when the complete persistent tree is byte-identical immediately before and
302
+ * after serialization; a concurrent mutation makes that candidate unusable. */
236
303
  export async function captureVerifiedWorkspaceArchive(
237
304
  session: unknown,
238
305
  capturedAtMs = Date.now(),
306
+ ): Promise<VerifiedWorkspaceArchive> {
307
+ return await withSandboxProviderCapture(session, async () => {
308
+ return await captureVerifiedWorkspaceArchiveExclusive(session, capturedAtMs);
309
+ });
310
+ }
311
+
312
+ async function captureVerifiedWorkspaceArchiveExclusive(
313
+ session: unknown,
314
+ capturedAtMs: number,
239
315
  ): Promise<VerifiedWorkspaceArchive> {
240
316
  const target = session as WorkspaceSession;
241
317
  if (typeof target.persistWorkspace !== "function") {
@@ -244,7 +320,13 @@ export async function captureVerifiedWorkspaceArchive(
244
320
  "sandbox session does not support workspace persistence",
245
321
  );
246
322
  }
247
- const before = await fingerprintSandboxWorkspace(target);
323
+ const configuredPersistence = target.state?.workspacePersistence;
324
+ const configuredTarFallback = workspaceFingerprintExcludes(target).length > 0;
325
+ const expectsNativeSnapshot =
326
+ typeof configuredPersistence === "string" &&
327
+ configuredPersistence !== "tar" &&
328
+ !configuredTarFallback;
329
+ const before = expectsNativeSnapshot ? null : await fingerprintSandboxWorkspace(target);
248
330
  const bytes = await target.persistWorkspace();
249
331
  if (!bytes || bytes.length === 0) {
250
332
  throw new WorkspaceArchiveIntegrityError(
@@ -253,8 +335,25 @@ export async function captureVerifiedWorkspaceArchive(
253
335
  { retryable: true },
254
336
  );
255
337
  }
338
+ const nativeSnapshot = decodeNativeSnapshotRef(bytes);
339
+ if (nativeSnapshot) {
340
+ const descriptor = describeNativeSnapshotArchive(bytes, capturedAtMs)!;
341
+ return {
342
+ bytes,
343
+ base64: Buffer.from(bytes).toString("base64"),
344
+ descriptor,
345
+ kind: "provider_snapshot",
346
+ nativeSnapshot,
347
+ };
348
+ }
349
+ if (expectsNativeSnapshot) {
350
+ throw new WorkspaceArchiveIntegrityError(
351
+ "native_snapshot_fallback_unverified",
352
+ `sandbox configured for ${String(configuredPersistence)} returned a tar archive instead of a native snapshot receipt`,
353
+ );
354
+ }
256
355
  const after = await fingerprintSandboxWorkspace(target);
257
- if (!fingerprintsEqual(before, after)) {
356
+ if (!before || !fingerprintsEqual(before, after)) {
258
357
  throw new WorkspaceArchiveIntegrityError(
259
358
  "workspace_changed_during_capture",
260
359
  "workspace changed while its durable archive was being captured",
@@ -262,8 +361,8 @@ export async function captureVerifiedWorkspaceArchive(
262
361
  );
263
362
  }
264
363
  const archiveSha256 = sha256(bytes);
265
- const descriptor: WorkspaceArchiveDescriptor = {
266
- version: WORKSPACE_ARCHIVE_DESCRIPTOR_VERSION,
364
+ const descriptor: TarWorkspaceArchiveDescriptor = {
365
+ version: 1,
267
366
  revision: `wa1:${String(capturedAtMs).padStart(13, "0")}:${archiveSha256}`,
268
367
  archiveSha256,
269
368
  archiveBytes: bytes.length,
@@ -274,13 +373,17 @@ export async function captureVerifiedWorkspaceArchive(
274
373
  bytes,
275
374
  base64: Buffer.from(bytes).toString("base64"),
276
375
  descriptor,
376
+ kind: "tar",
277
377
  };
278
378
  }
279
379
 
280
380
  export async function verifyRestoredWorkspace(
281
381
  session: unknown,
282
382
  descriptor: WorkspaceArchiveDescriptor,
283
- ): Promise<WorkspaceTreeFingerprint> {
383
+ ): Promise<WorkspaceTreeFingerprint | null> {
384
+ if (descriptor.version === WORKSPACE_ARCHIVE_DESCRIPTOR_VERSION) {
385
+ return null;
386
+ }
284
387
  const actual = await fingerprintSandboxWorkspace(session);
285
388
  if (!fingerprintsEqual(actual, descriptor.workspace)) {
286
389
  throw new WorkspaceArchiveIntegrityError(
@@ -940,23 +940,6 @@ function renderImageForTextTransport(output: ToolOutputImage | string): string {
940
940
  return `data:${mediaType};base64,${Buffer.from(image.data).toString("base64")}`;
941
941
  }
942
942
 
943
- /** Whether the bound model supports the structured tool-output transport, in
944
- * lockstep with the SDK's private `supportsStructuredToolOutputTransport`
945
- * (capabilities/transport): a ChatCompletions-family model — and an UNBOUND model
946
- * (undefined) — does NOT, so it gets the function tools; every other model keeps
947
- * the hosted `computer_use_preview` tool. The codex neutralize trick in index.ts
948
- * drops `_modelInstance`, so this returns false there and the function tools win. */
949
- function supportsStructuredToolOutputTransport(modelInstance: unknown): boolean {
950
- if (!modelInstance) return false;
951
- const constructorName =
952
- typeof modelInstance === "object" &&
953
- modelInstance &&
954
- typeof (modelInstance as { constructor?: unknown }).constructor === "function"
955
- ? ((modelInstance as { constructor: { name?: string } }).constructor.name ?? "")
956
- : "";
957
- return !constructorName.includes("ChatCompletions");
958
- }
959
-
960
943
  const COMPUTER_READ_ONLY_MESSAGE =
961
944
  "computer-use is read-only for this session — click, double_click, move, scroll, type, keypress, and drag are disabled. Call computer_screenshot to observe the desktop.";
962
945
 
@@ -1193,39 +1176,28 @@ export function computerFunctionTools(
1193
1176
  /**
1194
1177
  * EXPLICIT tool-transport selection, decided by the caller that knows the
1195
1178
  * provider's true wire identity (the worker's model resolution — see agent-turn.ts),
1196
- * NOT inferred from the bound model instance's constructor name. This is the
1197
- * HARDENING seam: `supportsStructuredToolOutputTransport` string-sniffs the
1198
- * constructor for "ChatCompletions", which a wrapped / proxied / minified model
1199
- * instance would defeat — silently handing a chat-completions provider the HOSTED
1200
- * `computer_use_preview` tool it 400s on every turn. When `toolMode` is set, tools()
1201
- * OBEYS it and never consults the sniff:
1179
+ * never inferred from the bound model instance's constructor name:
1202
1180
  * • "hosted" → the single hosted `computer_use_preview` tool (Responses backends).
1203
1181
  * • "function-image" → the FUNCTION `computer_*` tools with screenshots delivered as a
1204
1182
  * structured `{type:'image'}` output (the codex/ChatGPT backend,
1205
1183
  * which rejects hosted tool types but SEES structured image results).
1206
- * • "function-text" → the FUNCTION tools with screenshots rendered as a text
1207
- * `data:…;base64` URL (chat-completions providers, which can't read
1208
- * structured image tool results).
1184
+ * • "disabled" → no computer tools. Providers without a proven visual image
1185
+ * transport fail closed instead of receiving base64 as text.
1186
+ * • "function-text" → deprecated fail-closed alias retained for source compatibility.
1187
+ * • omitted → fail closed exactly like "disabled"; public runtime callers must
1188
+ * prove the transport explicitly before computer tools are exposed.
1209
1189
  */
1210
- export type ComputerToolMode = "hosted" | "function-image" | "function-text";
1190
+ export type ComputerToolMode = "hosted" | "function-image" | "disabled" | "function-text";
1211
1191
 
1212
1192
  export type ComputerUseArgs = {
1213
1193
  dimensions?: [number, number];
1214
1194
  readOnly?: boolean;
1215
1195
  display?: string;
1216
1196
  needsApproval?: boolean | ((ctx: unknown, action: unknown) => boolean | Promise<boolean>);
1217
- // Deliver screenshots from the FUNCTION tools as a REAL image the model can see
1218
- // (a structured `{type:'image'}` tool output → agents-core normalizes it to an
1219
- // `input_image` content item inside the function_call_output) instead of the text
1220
- // data-URL string. Only the codex/ChatGPT backend can read structured image tool
1221
- // results; chat-completions providers cannot, so this stays OFF (text rendering)
1222
- // by default and is turned on only on the codex path (see index.ts). Ignored when
1223
- // `toolMode` is set (the mode carries its own image-delivery choice).
1197
+ /** @deprecated Use `toolMode: "function-image"`. Omitted transport now fails closed. */
1224
1198
  imageFunctionResults?: boolean;
1225
- // EXPLICIT transport selection (see {@link ComputerToolMode}). When present, tools()
1226
- // obeys it directly — the constructor-name sniff is NOT consulted. When ABSENT, the
1227
- // legacy sniff behaviour is preserved byte-for-byte (back-compat for any embedder
1228
- // that constructs the capability without threading a mode).
1199
+ // EXPLICIT transport selection (see {@link ComputerToolMode}). When absent, tools()
1200
+ // returns no computer tools; constructor-name sniffing is never a transport proof.
1229
1201
  toolMode?: ComputerToolMode;
1230
1202
  /** Called after the display is ready on the first actual computer action. */
1231
1203
  onReady?: (session: SandboxSessionLike) => Promise<void>;
@@ -1241,17 +1213,9 @@ export function computerUse(args: ComputerUseArgs = {}): ComputerUseCapability {
1241
1213
  * the LIVE externally-owned session, so the agent's actions and the viewers'
1242
1214
  * pixels are one display.
1243
1215
  *
1244
- * `tools()` is TRANSPORT-AWARE, mirroring the SDK's `filesystem()` capability
1245
- * (which branches its `view_image` / `apply_patch` on
1246
- * `supportsStructuredToolOutputTransport(this._modelInstance)`):
1247
- * • structured transport (the Responses/OpenAI backend) → the single HOSTED
1248
- * `computer_use_preview` tool over a Computer bound to the session (unchanged).
1249
- * • text transport (codex / ChatGPT backend — or an unbound model) → a set of
1250
- * FUNCTION tools ({@link computerFunctionTools}) that route to the SAME Computer,
1251
- * because the codex backend rejects the hosted computer tool type.
1252
- * The bound model instance is captured by the SDK's `bind().bindRunAs().bindModel()`
1253
- * chain (base `Capability._modelInstance`); the codex path in index.ts neutralizes
1254
- * `bindModel` so `_modelInstance` stays undefined here → the function tools win.
1216
+ * `tools()` is transport-aware only through `ComputerUseArgs.toolMode`. The bound
1217
+ * model instance is intentionally ignored: a constructor name cannot prove whether
1218
+ * screenshots travel as hosted computer output or structured function images.
1255
1219
  */
1256
1220
  export class ComputerUseCapability extends Capability {
1257
1221
  readonly type = "computer-use";
@@ -1288,12 +1252,8 @@ export class ComputerUseCapability extends Capability {
1288
1252
  await this.args.onReady?.(preparedSession);
1289
1253
  },
1290
1254
  });
1291
- // HARDENING: when the caller declares an EXPLICIT toolMode, obey it and NEVER
1292
- // consult `supportsStructuredToolOutputTransport` — tool selection must not
1293
- // depend on the model instance's constructor name (a wrapped/proxied/minified
1294
- // instance would defeat the "ChatCompletions" string-sniff and silently hand a
1295
- // chat-completions provider the hosted tool it 400s on). The mode is decided by
1296
- // the worker, where provider identity is authoritative (see agent-turn.ts).
1255
+ // Tool selection must not depend on the model instance's constructor name. The
1256
+ // worker supplies a proven explicit mode; public callers that omit it fail closed.
1297
1257
  switch (this.args.toolMode) {
1298
1258
  case "hosted":
1299
1259
  return [this.hostedComputerTool(computer)];
@@ -1304,31 +1264,21 @@ export class ComputerUseCapability extends Capability {
1304
1264
  this.args.needsApproval,
1305
1265
  true,
1306
1266
  );
1267
+ case "disabled":
1307
1268
  case "function-text":
1308
- return computerFunctionTools(
1309
- computer,
1310
- this.args.readOnly ?? false,
1311
- this.args.needsApproval,
1312
- false,
1313
- );
1314
1269
  case undefined:
1315
- break; // fall through to the legacy sniff (back-compat), preserved byte-for-byte
1316
- }
1317
- // Legacy (no toolMode): structured transport keeps the HOSTED computer tool
1318
- // (unchanged); the codex / text backend gets the FUNCTION tools it can call.
1319
- if (supportsStructuredToolOutputTransport(this._modelInstance)) {
1320
- return [this.hostedComputerTool(computer)];
1270
+ // A text data URL is not a visual observation for the provider and can
1271
+ // consume hundreds of thousands of apparent text tokens. Disable the
1272
+ // entire coordinate-dependent capability: action tools without a usable
1273
+ // screenshot would be blind and unsafe.
1274
+ return [];
1321
1275
  }
1322
- return computerFunctionTools(
1323
- computer,
1324
- this.args.readOnly ?? false,
1325
- this.args.needsApproval,
1326
- this.args.imageFunctionResults ?? false,
1327
- );
1276
+ // Runtime callers may bypass TypeScript or pass a value from a newer client.
1277
+ // Unknown transport modes must remain fail-closed.
1278
+ return [];
1328
1279
  }
1329
1280
 
1330
- /** The single HOSTED `computer_use_preview` tool bound to `computer` — identical
1331
- * construction for the explicit "hosted" mode and the legacy structured-sniff path. */
1281
+ /** The single HOSTED `computer_use_preview` tool bound to `computer`. */
1332
1282
  private hostedComputerTool(computer: Computer): Tool<unknown> {
1333
1283
  return computerTool({
1334
1284
  computer,