@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
|
@@ -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="$(
|
|
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="$(
|
|
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
|
-
//
|
|
408
|
-
//
|
|
409
|
-
|
|
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
|
-
|
|
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
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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.
|
|
179
|
-
*
|
|
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:
|
|
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
|
-
/**
|
|
234
|
-
*
|
|
235
|
-
*
|
|
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
|
|
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:
|
|
266
|
-
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(
|
package/src/sandbox-computer.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
* • "
|
|
1207
|
-
*
|
|
1208
|
-
*
|
|
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
|
-
|
|
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
|
|
1226
|
-
//
|
|
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
|
|
1245
|
-
*
|
|
1246
|
-
*
|
|
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
|
-
//
|
|
1292
|
-
//
|
|
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
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
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
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
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
|
|
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,
|