@opengeni/runtime 0.11.0 → 0.13.3

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.
@@ -0,0 +1,289 @@
1
+ import { createHash } from "node:crypto";
2
+
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
+ };
21
+
22
+ export type VerifiedWorkspaceArchive = {
23
+ bytes: Uint8Array;
24
+ base64: string;
25
+ descriptor: WorkspaceArchiveDescriptor;
26
+ };
27
+
28
+ export type WorkspaceArchiveIntegrityCode =
29
+ | "archive_metadata_missing"
30
+ | "archive_metadata_invalid"
31
+ | "archive_base64_invalid"
32
+ | "archive_hash_mismatch"
33
+ | "archive_hydration_failed"
34
+ | "workspace_fingerprint_unavailable"
35
+ | "workspace_changed_during_capture"
36
+ | "workspace_fingerprint_mismatch";
37
+
38
+ export class WorkspaceArchiveIntegrityError extends Error {
39
+ readonly name = "WorkspaceArchiveIntegrityError";
40
+ readonly retryable: boolean;
41
+
42
+ constructor(
43
+ public readonly code: WorkspaceArchiveIntegrityCode,
44
+ message: string,
45
+ options: { retryable?: boolean } = {},
46
+ ) {
47
+ super(message);
48
+ this.retryable = options.retryable ?? false;
49
+ }
50
+ }
51
+
52
+ type WorkspaceSession = {
53
+ exec?: (args: {
54
+ cmd: string;
55
+ yieldTimeMs?: number;
56
+ maxOutputTokens?: number;
57
+ }) => Promise<unknown>;
58
+ execCommand?: (args: {
59
+ cmd: string;
60
+ yieldTimeMs?: number;
61
+ maxOutputTokens?: number;
62
+ }) => Promise<unknown>;
63
+ persistWorkspace?: () => Promise<Uint8Array | undefined>;
64
+ };
65
+
66
+ const SHA256 = /^[a-f0-9]{64}$/;
67
+ const REVISION = /^wa1:[0-9]{13}:[a-f0-9]{64}$/;
68
+
69
+ function sha256(bytes: Uint8Array): string {
70
+ return createHash("sha256").update(bytes).digest("hex");
71
+ }
72
+
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
+ function decodeBase64Strict(value: string): Uint8Array | null {
105
+ if (value.length === 0 || value.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) {
106
+ return null;
107
+ }
108
+ const bytes = Uint8Array.from(Buffer.from(value, "base64"));
109
+ return Buffer.from(bytes).toString("base64") === value ? bytes : null;
110
+ }
111
+
112
+ export function readVerifiedWorkspaceArchive(
113
+ base64: unknown,
114
+ metadata: unknown,
115
+ ): VerifiedWorkspaceArchive | null {
116
+ if (base64 === undefined || base64 === null || base64 === "") return null;
117
+ if (typeof base64 !== "string") {
118
+ throw new WorkspaceArchiveIntegrityError(
119
+ "archive_base64_invalid",
120
+ "selected workspace archive is not a base64 string",
121
+ );
122
+ }
123
+ if (metadata === undefined || metadata === null) {
124
+ throw new WorkspaceArchiveIntegrityError(
125
+ "archive_metadata_missing",
126
+ "selected workspace archive has no durable revision/fingerprint metadata",
127
+ );
128
+ }
129
+ const descriptor = parseWorkspaceArchiveDescriptor(metadata);
130
+ if (!descriptor) {
131
+ throw new WorkspaceArchiveIntegrityError(
132
+ "archive_metadata_invalid",
133
+ "selected workspace archive metadata is invalid",
134
+ );
135
+ }
136
+ const bytes = decodeBase64Strict(base64);
137
+ if (!bytes) {
138
+ throw new WorkspaceArchiveIntegrityError(
139
+ "archive_base64_invalid",
140
+ `workspace archive revision ${descriptor.revision} has invalid base64 bytes`,
141
+ );
142
+ }
143
+ const actualHash = sha256(bytes);
144
+ if (bytes.length !== descriptor.archiveBytes || actualHash !== descriptor.archiveSha256) {
145
+ throw new WorkspaceArchiveIntegrityError(
146
+ "archive_hash_mismatch",
147
+ `workspace archive revision ${descriptor.revision} failed SHA-256/size verification`,
148
+ );
149
+ }
150
+ return { bytes, base64, descriptor };
151
+ }
152
+
153
+ function stdoutFromExecResult(value: unknown): string {
154
+ if (typeof value === "string") return value;
155
+ if (!value || typeof value !== "object") return "";
156
+ const output = value as { stdout?: unknown; output?: unknown };
157
+ return typeof output.stdout === "string"
158
+ ? output.stdout
159
+ : typeof output.output === "string"
160
+ ? output.output
161
+ : "";
162
+ }
163
+
164
+ const WORKSPACE_FINGERPRINT_COMMAND = String.raw`bash -o pipefail -c '
165
+ set -eu
166
+ cd /workspace
167
+ digest=$(LC_ALL=C tar --sort=name --mtime="@0" --owner=0 --group=0 --numeric-owner --format=gnu -cf - . | sha256sum | awk "{print \$1}")
168
+ entries=$(find . -xdev -mindepth 1 -printf x | wc -c | tr -d " ")
169
+ files=$(find . -xdev -type f -printf x | wc -c | tr -d " ")
170
+ bytes=$(find . -xdev -type f -printf "%s\n" | awk "{s+=\$1} END {printf \"%.0f\", s+0}")
171
+ printf "OPENGENI_WORKSPACE_FINGERPRINT_V1 %s %s %s %s\n" "$digest" "$entries" "$files" "$bytes"
172
+ '`;
173
+
174
+ /** Hashes names, kinds, modes, symlink targets and all file bytes through one
175
+ * deterministic GNU tar stream. Only the aggregate is returned; paths and file
176
+ * contents never enter logs or durable metadata. */
177
+ export async function fingerprintSandboxWorkspace(
178
+ session: unknown,
179
+ ): Promise<WorkspaceTreeFingerprint> {
180
+ const target = session as WorkspaceSession;
181
+ const run = target.exec ?? target.execCommand;
182
+ if (!run) {
183
+ throw new WorkspaceArchiveIntegrityError(
184
+ "workspace_fingerprint_unavailable",
185
+ "sandbox session cannot execute the workspace fingerprint probe",
186
+ );
187
+ }
188
+ const result = await run.call(target, {
189
+ cmd: WORKSPACE_FINGERPRINT_COMMAND,
190
+ yieldTimeMs: 120_000,
191
+ maxOutputTokens: 1_000,
192
+ });
193
+ const match = stdoutFromExecResult(result)
194
+ .trim()
195
+ .match(/^OPENGENI_WORKSPACE_FINGERPRINT_V1 ([a-f0-9]{64}) ([0-9]+) ([0-9]+) ([0-9]+)$/);
196
+ if (!match) {
197
+ throw new WorkspaceArchiveIntegrityError(
198
+ "workspace_fingerprint_unavailable",
199
+ "sandbox workspace fingerprint probe returned no valid digest",
200
+ { retryable: true },
201
+ );
202
+ }
203
+ const entryCount = Number(match[2]);
204
+ const fileCount = Number(match[3]);
205
+ const totalFileBytes = Number(match[4]);
206
+ if (![entryCount, fileCount, totalFileBytes].every(Number.isSafeInteger)) {
207
+ throw new WorkspaceArchiveIntegrityError(
208
+ "workspace_fingerprint_unavailable",
209
+ "sandbox workspace fingerprint counters exceeded the supported range",
210
+ );
211
+ }
212
+ return {
213
+ algorithm: "sha256",
214
+ sha256: match[1]!,
215
+ entryCount,
216
+ fileCount,
217
+ totalFileBytes,
218
+ };
219
+ }
220
+
221
+ function fingerprintsEqual(a: WorkspaceTreeFingerprint, b: WorkspaceTreeFingerprint): boolean {
222
+ return (
223
+ a.sha256 === b.sha256 &&
224
+ a.entryCount === b.entryCount &&
225
+ a.fileCount === b.fileCount &&
226
+ a.totalFileBytes === b.totalFileBytes
227
+ );
228
+ }
229
+
230
+ /** Capture is accepted only when the complete tree is byte-identical immediately
231
+ * before and after the provider snapshot. A concurrent mutation makes the
232
+ * candidate non-durable and it is never folded onto the lease. */
233
+ export async function captureVerifiedWorkspaceArchive(
234
+ session: unknown,
235
+ capturedAtMs = Date.now(),
236
+ ): Promise<VerifiedWorkspaceArchive> {
237
+ const target = session as WorkspaceSession;
238
+ if (typeof target.persistWorkspace !== "function") {
239
+ throw new WorkspaceArchiveIntegrityError(
240
+ "workspace_fingerprint_unavailable",
241
+ "sandbox session does not support workspace persistence",
242
+ );
243
+ }
244
+ const before = await fingerprintSandboxWorkspace(target);
245
+ const bytes = await target.persistWorkspace();
246
+ if (!bytes || bytes.length === 0) {
247
+ throw new WorkspaceArchiveIntegrityError(
248
+ "archive_base64_invalid",
249
+ "sandbox workspace persistence returned an empty archive",
250
+ { retryable: true },
251
+ );
252
+ }
253
+ const after = await fingerprintSandboxWorkspace(target);
254
+ if (!fingerprintsEqual(before, after)) {
255
+ throw new WorkspaceArchiveIntegrityError(
256
+ "workspace_changed_during_capture",
257
+ "workspace changed while its durable archive was being captured",
258
+ { retryable: true },
259
+ );
260
+ }
261
+ const archiveSha256 = sha256(bytes);
262
+ const descriptor: WorkspaceArchiveDescriptor = {
263
+ version: WORKSPACE_ARCHIVE_DESCRIPTOR_VERSION,
264
+ revision: `wa1:${String(capturedAtMs).padStart(13, "0")}:${archiveSha256}`,
265
+ archiveSha256,
266
+ archiveBytes: bytes.length,
267
+ capturedAt: new Date(capturedAtMs).toISOString(),
268
+ workspace: before,
269
+ };
270
+ return {
271
+ bytes,
272
+ base64: Buffer.from(bytes).toString("base64"),
273
+ descriptor,
274
+ };
275
+ }
276
+
277
+ export async function verifyRestoredWorkspace(
278
+ session: unknown,
279
+ descriptor: WorkspaceArchiveDescriptor,
280
+ ): Promise<WorkspaceTreeFingerprint> {
281
+ const actual = await fingerprintSandboxWorkspace(session);
282
+ if (!fingerprintsEqual(actual, descriptor.workspace)) {
283
+ throw new WorkspaceArchiveIntegrityError(
284
+ "workspace_fingerprint_mismatch",
285
+ `restored workspace does not match selected archive revision ${descriptor.revision}`,
286
+ );
287
+ }
288
+ return actual;
289
+ }
@@ -1279,7 +1279,9 @@ export class ComputerUseCapability extends Capability {
1279
1279
  // any computer-use-only recording. One SandboxComputer instance caches
1280
1280
  // this for the rest of the turn.
1281
1281
  prepare: async (preparedSession) => {
1282
- await ensureDisplayStack(preparedSession);
1282
+ await ensureDisplayStack(preparedSession, {
1283
+ telemetryContext: { callerKind: "computer" },
1284
+ });
1283
1285
  await this.args.onReady?.(preparedSession);
1284
1286
  },
1285
1287
  });