@opengeni/runtime 0.7.1 → 0.11.0
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/THIRD_PARTY_NOTICES +32 -0
- package/dist/agent-UMUIWWD2.js +8 -0
- package/dist/agent-UMUIWWD2.js.map +1 -0
- package/dist/chunk-4YJ5Z52Z.js +1789 -0
- package/dist/chunk-4YJ5Z52Z.js.map +1 -0
- package/dist/chunk-DPULLAJG.js +215 -0
- package/dist/chunk-DPULLAJG.js.map +1 -0
- package/dist/chunk-K4V4QYCL.js +37683 -0
- package/dist/chunk-K4V4QYCL.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-YUAFQEFY.js +21522 -0
- package/dist/chunk-YUAFQEFY.js.map +1 -0
- package/dist/{index-DIiiwQY2.d.ts → index-D6yi-JcU.d.ts} +138 -15
- package/dist/index.d.ts +329 -121
- package/dist/index.js +8149 -307
- package/dist/index.js.map +1 -1
- package/dist/localSources-T3HZ6KY4.js +15 -0
- package/dist/localSources-T3HZ6KY4.js.map +1 -0
- package/dist/sandbox/index.d.ts +1 -1
- package/dist/sandbox/index.js +42 -5
- package/dist/skill-library.d.ts +74 -0
- package/dist/skill-library.js +18 -0
- package/dist/skill-library.js.map +1 -0
- package/package.json +13 -5
- package/src/bundled_hashicorp_terraform_skills/README.md +3 -3
- package/src/bundled_hashicorp_terraform_skills/checkov/SKILL.md +1 -1
- package/src/context-compaction.ts +421 -24
- package/src/history-sanitizer.ts +14 -5
- package/src/index.ts +1335 -280
- package/src/sandbox/channel-a.ts +535 -97
- package/src/sandbox/index.ts +36 -2
- package/src/sandbox/routing/routing-session.ts +10 -2
- package/src/sandbox/run-credentials.ts +818 -0
- package/src/sandbox/select.ts +3 -3
- package/src/sandbox/selfhosted/capabilities.ts +1 -1
- package/src/sandbox/selfhosted/op-stream.ts +40 -0
- package/src/sandbox/selfhosted/op-testing.ts +50 -3
- package/src/sandbox/selfhosted/session.ts +17 -4
- package/src/sandbox/selfhosted/testing.ts +1 -1
- package/src/sandbox/stream-port.ts +1 -1
- package/src/sandbox/stream-token.ts +1 -1
- package/src/sandbox/terminal-server.ts +6 -2
- package/src/sandbox/toolspace-token.ts +147 -0
- package/src/sandbox/turn-tool-cancellation.ts +940 -0
- package/src/skill-library.ts +323 -0
- package/dist/chunk-RYDCVTMW.js +0 -5531
- package/dist/chunk-RYDCVTMW.js.map +0 -1
- /package/src/{bundled_hashicorp_terraform_skills → bundled_skill_library}/azure-verified-modules/SKILL.md +0 -0
|
@@ -0,0 +1,818 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RunCredentialAuthNeeded,
|
|
3
|
+
RunCredentialFile,
|
|
4
|
+
RunCredentialRedaction,
|
|
5
|
+
RunCredentialsResolution,
|
|
6
|
+
} from "@opengeni/contracts";
|
|
7
|
+
import type {
|
|
8
|
+
ExecCommandArgs,
|
|
9
|
+
SandboxClient,
|
|
10
|
+
SandboxSessionLike,
|
|
11
|
+
SandboxSessionState,
|
|
12
|
+
} from "@openai/agents/sandbox";
|
|
13
|
+
|
|
14
|
+
const MAX_ENVIRONMENT_ENTRIES = 256;
|
|
15
|
+
const MAX_ENVIRONMENT_VALUE_BYTES = 64 * 1024;
|
|
16
|
+
const MAX_CREDENTIAL_FILES = 64;
|
|
17
|
+
const MAX_CREDENTIAL_FILE_BYTES = 1024 * 1024;
|
|
18
|
+
const MAX_TOTAL_MATERIAL_BYTES = 4 * 1024 * 1024;
|
|
19
|
+
const MAX_AUTH_NEEDED_NOTICES = 32;
|
|
20
|
+
const MAX_REDACTION_VALUES = 256;
|
|
21
|
+
const WRITE_CHUNK_BYTES = 24 * 1024;
|
|
22
|
+
const COMMAND_OK_MARKER = "__OPENGENI_RUN_CREDENTIAL_COMMAND_OK__";
|
|
23
|
+
const AUTH_NEEDED_REASONS = new Set([
|
|
24
|
+
"missing_connection",
|
|
25
|
+
"expired",
|
|
26
|
+
"insufficient_scope",
|
|
27
|
+
"refresh_failed",
|
|
28
|
+
]);
|
|
29
|
+
const PORTABLE_LOCK_STALE_MINUTES = 2;
|
|
30
|
+
|
|
31
|
+
export type RunCredentialExpectedScope = {
|
|
32
|
+
accountId: string;
|
|
33
|
+
workspaceId: string;
|
|
34
|
+
sessionId: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type NormalizedRunCredentialMaterial = {
|
|
38
|
+
environment: Record<string, string>;
|
|
39
|
+
files: RunCredentialFile[];
|
|
40
|
+
fileEnvironment: Record<string, string>;
|
|
41
|
+
expiresAt: Date | null;
|
|
42
|
+
authNeeded: RunCredentialAuthNeeded[];
|
|
43
|
+
redactions: RunCredentialRedaction[];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type RunCredentialCommandSession = Pick<SandboxSessionLike, "exec" | "execCommand">;
|
|
47
|
+
export type RunCredentialCommandRunner = (
|
|
48
|
+
session: RunCredentialCommandSession,
|
|
49
|
+
args: ExecCommandArgs,
|
|
50
|
+
) => Promise<unknown>;
|
|
51
|
+
export type RunCredentialSessionReady = (
|
|
52
|
+
session: RunCredentialCommandSession,
|
|
53
|
+
) => Promise<void> | void;
|
|
54
|
+
|
|
55
|
+
export class RunCredentialValidationError extends Error {
|
|
56
|
+
constructor(message: string) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.name = "RunCredentialValidationError";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function byteLength(value: string): number {
|
|
63
|
+
return new TextEncoder().encode(value).byteLength;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
67
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function assertBoundedString(value: unknown, label: string, maxBytes: number): void {
|
|
71
|
+
if (value === undefined) return;
|
|
72
|
+
if (typeof value !== "string") {
|
|
73
|
+
throw new RunCredentialValidationError(`${label} must be a string`);
|
|
74
|
+
}
|
|
75
|
+
if (byteLength(value) > maxBytes) {
|
|
76
|
+
throw new RunCredentialValidationError(`${label} exceeds ${maxBytes} bytes`);
|
|
77
|
+
}
|
|
78
|
+
if (value?.includes("\0")) {
|
|
79
|
+
throw new RunCredentialValidationError(`${label} contains a NUL byte`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function requiredBoundedString(value: unknown, label: string, maxBytes: number): string {
|
|
84
|
+
if (typeof value !== "string") {
|
|
85
|
+
throw new RunCredentialValidationError(`${label} must be a string`);
|
|
86
|
+
}
|
|
87
|
+
assertBoundedString(value, label, maxBytes);
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function assertOptionalNonEmptyString(value: unknown, label: string, maxBytes: number): void {
|
|
92
|
+
assertBoundedString(value, label, maxBytes);
|
|
93
|
+
if (typeof value === "string" && !value.trim()) {
|
|
94
|
+
throw new RunCredentialValidationError(`${label} must not be empty`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function normalizeAuthNeeded(notices: unknown, required: boolean): RunCredentialAuthNeeded[] {
|
|
99
|
+
if (notices !== undefined && !Array.isArray(notices)) {
|
|
100
|
+
throw new RunCredentialValidationError("authNeeded must be an array");
|
|
101
|
+
}
|
|
102
|
+
const rawNotices = notices ?? [];
|
|
103
|
+
const normalized: RunCredentialAuthNeeded[] = [];
|
|
104
|
+
if (required && rawNotices.length === 0) {
|
|
105
|
+
throw new RunCredentialValidationError("auth_needed resolution must contain a notice");
|
|
106
|
+
}
|
|
107
|
+
if (rawNotices.length > MAX_AUTH_NEEDED_NOTICES) {
|
|
108
|
+
throw new RunCredentialValidationError(
|
|
109
|
+
`auth-needed notice count exceeds ${MAX_AUTH_NEEDED_NOTICES}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
for (const [index, notice] of rawNotices.entries()) {
|
|
113
|
+
if (!isRecord(notice)) {
|
|
114
|
+
throw new RunCredentialValidationError(`authNeeded[${index}] must be an object`);
|
|
115
|
+
}
|
|
116
|
+
const reason = notice.reason;
|
|
117
|
+
if (typeof reason !== "string" || !AUTH_NEEDED_REASONS.has(reason)) {
|
|
118
|
+
throw new RunCredentialValidationError(`authNeeded[${index}].reason is invalid`);
|
|
119
|
+
}
|
|
120
|
+
assertOptionalNonEmptyString(notice.providerDomain, `authNeeded[${index}].providerDomain`, 512);
|
|
121
|
+
assertOptionalNonEmptyString(notice.connectionId, `authNeeded[${index}].connectionId`, 512);
|
|
122
|
+
assertOptionalNonEmptyString(notice.resource, `authNeeded[${index}].resource`, 2_048);
|
|
123
|
+
assertOptionalNonEmptyString(notice.message, `authNeeded[${index}].message`, 2_048);
|
|
124
|
+
if (notice.scopes !== undefined && !Array.isArray(notice.scopes)) {
|
|
125
|
+
throw new RunCredentialValidationError(`authNeeded[${index}].scopes must be an array`);
|
|
126
|
+
}
|
|
127
|
+
const scopes = notice.scopes ?? [];
|
|
128
|
+
if (scopes.length > 128) {
|
|
129
|
+
throw new RunCredentialValidationError(`authNeeded[${index}].scopes exceeds 128 entries`);
|
|
130
|
+
}
|
|
131
|
+
for (const scope of scopes) {
|
|
132
|
+
requiredBoundedString(scope, `authNeeded[${index}].scope`, 512);
|
|
133
|
+
if (!scope.trim()) {
|
|
134
|
+
throw new RunCredentialValidationError(`authNeeded[${index}].scope must not be empty`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (notice.authorizationUrl !== undefined) {
|
|
138
|
+
const authorizationUrl = requiredBoundedString(
|
|
139
|
+
notice.authorizationUrl,
|
|
140
|
+
`authNeeded[${index}].authorizationUrl`,
|
|
141
|
+
4_096,
|
|
142
|
+
);
|
|
143
|
+
let url: URL;
|
|
144
|
+
try {
|
|
145
|
+
url = new URL(authorizationUrl);
|
|
146
|
+
} catch {
|
|
147
|
+
throw new RunCredentialValidationError(`authNeeded[${index}].authorizationUrl is invalid`);
|
|
148
|
+
}
|
|
149
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
150
|
+
throw new RunCredentialValidationError(
|
|
151
|
+
`authNeeded[${index}].authorizationUrl must use http or https`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
normalized.push({
|
|
156
|
+
reason: reason as RunCredentialAuthNeeded["reason"],
|
|
157
|
+
...(typeof notice.providerDomain === "string"
|
|
158
|
+
? { providerDomain: notice.providerDomain }
|
|
159
|
+
: {}),
|
|
160
|
+
...(typeof notice.connectionId === "string" ? { connectionId: notice.connectionId } : {}),
|
|
161
|
+
...(scopes.length > 0 ? { scopes: scopes as string[] } : {}),
|
|
162
|
+
...(typeof notice.resource === "string" ? { resource: notice.resource } : {}),
|
|
163
|
+
...(typeof notice.authorizationUrl === "string"
|
|
164
|
+
? { authorizationUrl: notice.authorizationUrl }
|
|
165
|
+
: {}),
|
|
166
|
+
...(typeof notice.message === "string" ? { message: notice.message } : {}),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return normalized;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function assertResolutionScope(
|
|
173
|
+
resolution: Record<string, unknown>,
|
|
174
|
+
expected: RunCredentialExpectedScope,
|
|
175
|
+
): void {
|
|
176
|
+
if (
|
|
177
|
+
resolution.accountId !== expected.accountId ||
|
|
178
|
+
resolution.workspaceId !== expected.workspaceId ||
|
|
179
|
+
resolution.sessionId !== expected.sessionId
|
|
180
|
+
) {
|
|
181
|
+
throw new RunCredentialValidationError("run credential provider returned a scope mismatch");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizeRelativeFilePath(path: string): string {
|
|
186
|
+
if (
|
|
187
|
+
path.length === 0 ||
|
|
188
|
+
path.length > 240 ||
|
|
189
|
+
path.startsWith("/") ||
|
|
190
|
+
path.includes("\\") ||
|
|
191
|
+
path.includes("\0")
|
|
192
|
+
) {
|
|
193
|
+
throw new RunCredentialValidationError(`invalid run credential file path: ${path}`);
|
|
194
|
+
}
|
|
195
|
+
const segments = path.split("/");
|
|
196
|
+
if (segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
|
|
197
|
+
throw new RunCredentialValidationError(`invalid run credential file path: ${path}`);
|
|
198
|
+
}
|
|
199
|
+
return path;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function assertEnvironmentName(name: string, label: string): void {
|
|
203
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
204
|
+
throw new RunCredentialValidationError(`invalid ${label}: ${name}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Validate the untrusted host-port response before any secret is written to a
|
|
210
|
+
* box. The host owns credential selection; OpenGeni owns scope checks, bounds,
|
|
211
|
+
* path safety, and the transport lifecycle.
|
|
212
|
+
*/
|
|
213
|
+
export function normalizeRunCredentialsResolution(
|
|
214
|
+
resolution: Extract<RunCredentialsResolution, { status: "not_applicable" }>,
|
|
215
|
+
expected: RunCredentialExpectedScope,
|
|
216
|
+
now?: Date,
|
|
217
|
+
): null;
|
|
218
|
+
export function normalizeRunCredentialsResolution(
|
|
219
|
+
resolution: Exclude<RunCredentialsResolution, { status: "not_applicable" }>,
|
|
220
|
+
expected: RunCredentialExpectedScope,
|
|
221
|
+
now?: Date,
|
|
222
|
+
): NormalizedRunCredentialMaterial;
|
|
223
|
+
export function normalizeRunCredentialsResolution(
|
|
224
|
+
resolution: RunCredentialsResolution,
|
|
225
|
+
expected: RunCredentialExpectedScope,
|
|
226
|
+
now?: Date,
|
|
227
|
+
): NormalizedRunCredentialMaterial | null;
|
|
228
|
+
export function normalizeRunCredentialsResolution(
|
|
229
|
+
resolution: RunCredentialsResolution,
|
|
230
|
+
expected: RunCredentialExpectedScope,
|
|
231
|
+
now: Date = new Date(),
|
|
232
|
+
): NormalizedRunCredentialMaterial | null {
|
|
233
|
+
if (!isRecord(resolution)) {
|
|
234
|
+
throw new RunCredentialValidationError("run credential resolution must be an object");
|
|
235
|
+
}
|
|
236
|
+
const candidate = resolution as unknown as Record<string, unknown>;
|
|
237
|
+
if (
|
|
238
|
+
candidate.status !== "ok" &&
|
|
239
|
+
candidate.status !== "auth_needed" &&
|
|
240
|
+
candidate.status !== "not_applicable"
|
|
241
|
+
) {
|
|
242
|
+
throw new RunCredentialValidationError("run credential resolution status is invalid");
|
|
243
|
+
}
|
|
244
|
+
assertResolutionScope(candidate, expected);
|
|
245
|
+
if (candidate.status === "not_applicable") return null;
|
|
246
|
+
const authNeeded = normalizeAuthNeeded(candidate.authNeeded, candidate.status === "auth_needed");
|
|
247
|
+
if (candidate.status === "auth_needed") {
|
|
248
|
+
return {
|
|
249
|
+
environment: {},
|
|
250
|
+
files: [],
|
|
251
|
+
fileEnvironment: {},
|
|
252
|
+
expiresAt: null,
|
|
253
|
+
authNeeded,
|
|
254
|
+
redactions: [],
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!isRecord(candidate.environment)) {
|
|
259
|
+
throw new RunCredentialValidationError("run credential environment must be an object");
|
|
260
|
+
}
|
|
261
|
+
const environmentEntries = Object.entries(candidate.environment);
|
|
262
|
+
if (environmentEntries.length > MAX_ENVIRONMENT_ENTRIES) {
|
|
263
|
+
throw new RunCredentialValidationError(
|
|
264
|
+
`run credential environment exceeds ${MAX_ENVIRONMENT_ENTRIES} entries`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
let totalBytes = 0;
|
|
268
|
+
const environment: Record<string, string> = {};
|
|
269
|
+
for (const [name, rawValue] of environmentEntries) {
|
|
270
|
+
assertEnvironmentName(name, "run credential environment name");
|
|
271
|
+
const value = requiredBoundedString(
|
|
272
|
+
rawValue,
|
|
273
|
+
`run credential environment value: ${name}`,
|
|
274
|
+
MAX_ENVIRONMENT_VALUE_BYTES,
|
|
275
|
+
);
|
|
276
|
+
const valueBytes = byteLength(value);
|
|
277
|
+
totalBytes += valueBytes;
|
|
278
|
+
Object.defineProperty(environment, name, {
|
|
279
|
+
value,
|
|
280
|
+
enumerable: true,
|
|
281
|
+
configurable: true,
|
|
282
|
+
writable: true,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (candidate.files !== undefined && !Array.isArray(candidate.files)) {
|
|
287
|
+
throw new RunCredentialValidationError("run credential files must be an array");
|
|
288
|
+
}
|
|
289
|
+
const files = candidate.files ?? [];
|
|
290
|
+
if (files.length > MAX_CREDENTIAL_FILES) {
|
|
291
|
+
throw new RunCredentialValidationError(
|
|
292
|
+
`run credential file count exceeds ${MAX_CREDENTIAL_FILES}`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
const paths = new Map<string, string>();
|
|
296
|
+
const normalizedFiles = files.map((file, index) => {
|
|
297
|
+
if (!isRecord(file)) {
|
|
298
|
+
throw new RunCredentialValidationError(`run credential file ${index} must be an object`);
|
|
299
|
+
}
|
|
300
|
+
const path = normalizeRelativeFilePath(
|
|
301
|
+
requiredBoundedString(file.path, `run credential file ${index} path`, 240),
|
|
302
|
+
);
|
|
303
|
+
const folded = path.toLocaleLowerCase("en-US");
|
|
304
|
+
const collision = paths.get(folded);
|
|
305
|
+
if (collision !== undefined) {
|
|
306
|
+
throw new RunCredentialValidationError(
|
|
307
|
+
`run credential file path collision: ${collision} and ${path}`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
paths.set(folded, path);
|
|
311
|
+
const content = requiredBoundedString(
|
|
312
|
+
file.content,
|
|
313
|
+
`run credential file content: ${path}`,
|
|
314
|
+
MAX_CREDENTIAL_FILE_BYTES,
|
|
315
|
+
);
|
|
316
|
+
const contentBytes = byteLength(content);
|
|
317
|
+
if (file.mode !== undefined && file.mode !== "0400" && file.mode !== "0600") {
|
|
318
|
+
throw new RunCredentialValidationError(`run credential file mode is invalid: ${path}`);
|
|
319
|
+
}
|
|
320
|
+
totalBytes += contentBytes;
|
|
321
|
+
return { path, content, mode: file.mode ?? "0600" } as RunCredentialFile;
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
if (candidate.fileEnvironment !== undefined && !isRecord(candidate.fileEnvironment)) {
|
|
325
|
+
throw new RunCredentialValidationError("run credential fileEnvironment must be an object");
|
|
326
|
+
}
|
|
327
|
+
const fileEnvironment: Record<string, string> = {};
|
|
328
|
+
for (const [name, unresolvedPath] of Object.entries(candidate.fileEnvironment ?? {})) {
|
|
329
|
+
assertEnvironmentName(name, "run credential file environment name");
|
|
330
|
+
if (Object.prototype.hasOwnProperty.call(environment, name)) {
|
|
331
|
+
throw new RunCredentialValidationError(
|
|
332
|
+
`run credential environment name is declared twice: ${name}`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
const path = normalizeRelativeFilePath(
|
|
336
|
+
requiredBoundedString(unresolvedPath, `run credential file environment path: ${name}`, 240),
|
|
337
|
+
);
|
|
338
|
+
if (!paths.has(path.toLocaleLowerCase("en-US"))) {
|
|
339
|
+
throw new RunCredentialValidationError(
|
|
340
|
+
`run credential file environment references an unknown file: ${path}`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
Object.defineProperty(fileEnvironment, name, {
|
|
344
|
+
value: path,
|
|
345
|
+
enumerable: true,
|
|
346
|
+
configurable: true,
|
|
347
|
+
writable: true,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
if (environmentEntries.length + Object.keys(fileEnvironment).length > MAX_ENVIRONMENT_ENTRIES) {
|
|
351
|
+
throw new RunCredentialValidationError(
|
|
352
|
+
`combined run credential environment exceeds ${MAX_ENVIRONMENT_ENTRIES} entries`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
if (totalBytes > MAX_TOTAL_MATERIAL_BYTES) {
|
|
356
|
+
throw new RunCredentialValidationError(
|
|
357
|
+
`run credential material exceeds ${MAX_TOTAL_MATERIAL_BYTES} bytes`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const redactions: RunCredentialRedaction[] = Object.entries(environment).map(([name, value]) => ({
|
|
362
|
+
name,
|
|
363
|
+
value,
|
|
364
|
+
}));
|
|
365
|
+
if (candidate.redactions !== undefined && !Array.isArray(candidate.redactions)) {
|
|
366
|
+
throw new RunCredentialValidationError("run credential redactions must be an array");
|
|
367
|
+
}
|
|
368
|
+
const additionalRedactions = candidate.redactions ?? [];
|
|
369
|
+
if (additionalRedactions.length > MAX_REDACTION_VALUES) {
|
|
370
|
+
throw new RunCredentialValidationError(
|
|
371
|
+
`run credential redactions exceed ${MAX_REDACTION_VALUES} entries`,
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
for (const [index, redaction] of additionalRedactions.entries()) {
|
|
375
|
+
if (!isRecord(redaction)) {
|
|
376
|
+
throw new RunCredentialValidationError(`redactions[${index}] must be an object`);
|
|
377
|
+
}
|
|
378
|
+
const name = requiredBoundedString(redaction.name, `redactions[${index}].name`, 128);
|
|
379
|
+
const value = requiredBoundedString(
|
|
380
|
+
redaction.value,
|
|
381
|
+
`redactions[${index}].value`,
|
|
382
|
+
MAX_ENVIRONMENT_VALUE_BYTES,
|
|
383
|
+
);
|
|
384
|
+
if (!name.trim() || !value) {
|
|
385
|
+
throw new RunCredentialValidationError(`redactions[${index}] must contain a name and value`);
|
|
386
|
+
}
|
|
387
|
+
redactions.push({ name, value });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
let expiresAt: Date | null = null;
|
|
391
|
+
if (candidate.expiresAt !== undefined && candidate.expiresAt !== null) {
|
|
392
|
+
const expiry = requiredBoundedString(candidate.expiresAt, "run credential expiry", 128);
|
|
393
|
+
expiresAt = new Date(expiry);
|
|
394
|
+
if (!Number.isFinite(expiresAt.getTime()) || expiresAt.getTime() <= now.getTime()) {
|
|
395
|
+
throw new RunCredentialValidationError("run credential expiry is invalid or already expired");
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
environment,
|
|
400
|
+
files: normalizedFiles,
|
|
401
|
+
fileEnvironment,
|
|
402
|
+
expiresAt,
|
|
403
|
+
authNeeded,
|
|
404
|
+
redactions,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function assertPathIdentity(value: string, label: string): string {
|
|
409
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/.test(value)) {
|
|
410
|
+
throw new RunCredentialValidationError(`${label} is not safe for a credential path`);
|
|
411
|
+
}
|
|
412
|
+
return value;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function runCredentialRoot(sessionId: string): string {
|
|
416
|
+
return `/tmp/opengeni-run-credentials/${assertPathIdentity(sessionId, "sessionId")}`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function runCredentialPointerFile(sessionId: string): string {
|
|
420
|
+
return `${runCredentialRoot(sessionId)}/current`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function shellQuote(value: string): string {
|
|
424
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function base64(value: string): string {
|
|
428
|
+
return Buffer.from(value, "utf8").toString("base64");
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function portableBase64DecodeCommand(): string {
|
|
432
|
+
return [
|
|
433
|
+
"{",
|
|
434
|
+
"if printf %s QQ== | base64 -d >/dev/null 2>&1; then base64 -d",
|
|
435
|
+
"elif printf %s QQ== | base64 -D >/dev/null 2>&1; then base64 -D",
|
|
436
|
+
"elif command -v openssl >/dev/null 2>&1; then openssl base64 -d -A",
|
|
437
|
+
"else exit 69",
|
|
438
|
+
"fi",
|
|
439
|
+
"}",
|
|
440
|
+
].join("\n");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Prefer util-linux flock; fall back to an atomic, stale-reaped mkdir lock. */
|
|
444
|
+
function pointerLockAcquireCommands(root: string): string[] {
|
|
445
|
+
const lockFile = shellQuote(`${root}/.pointer.lock`);
|
|
446
|
+
const lockDirectory = shellQuote(`${root}/.pointer.lockdir`);
|
|
447
|
+
return [
|
|
448
|
+
`if command -v flock >/dev/null 2>&1; then`,
|
|
449
|
+
` exec 9>${lockFile}`,
|
|
450
|
+
" flock -w 30 9",
|
|
451
|
+
" _opengeni_pointer_lock_kind=flock",
|
|
452
|
+
"else",
|
|
453
|
+
` _opengeni_pointer_lock_dir=${lockDirectory}`,
|
|
454
|
+
" _opengeni_pointer_lock_deadline=$(($(date +%s) + 30))",
|
|
455
|
+
' while ! mkdir "$_opengeni_pointer_lock_dir" 2>/dev/null; do',
|
|
456
|
+
` if find "$_opengeni_pointer_lock_dir" -prune -mmin +${PORTABLE_LOCK_STALE_MINUTES} -print 2>/dev/null | grep -q .; then`,
|
|
457
|
+
' _opengeni_stale_lock="${_opengeni_pointer_lock_dir}.stale.$$"',
|
|
458
|
+
' if mv "$_opengeni_pointer_lock_dir" "$_opengeni_stale_lock" 2>/dev/null; then',
|
|
459
|
+
' rm -rf -- "$_opengeni_stale_lock"',
|
|
460
|
+
" continue",
|
|
461
|
+
" fi",
|
|
462
|
+
" fi",
|
|
463
|
+
' [ "$(date +%s)" -lt "$_opengeni_pointer_lock_deadline" ] || exit 73',
|
|
464
|
+
" sleep 0.1",
|
|
465
|
+
" done",
|
|
466
|
+
" _opengeni_pointer_lock_kind=directory",
|
|
467
|
+
` trap 'rmdir ${lockDirectory} 2>/dev/null || :' EXIT HUP INT TERM`,
|
|
468
|
+
"fi",
|
|
469
|
+
];
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function pointerLockReleaseCommands(): string[] {
|
|
473
|
+
return [
|
|
474
|
+
'if [ "${_opengeni_pointer_lock_kind:-}" = directory ]; then',
|
|
475
|
+
' rmdir "$_opengeni_pointer_lock_dir" 2>/dev/null || :',
|
|
476
|
+
" trap - EXIT HUP INT TERM",
|
|
477
|
+
"fi",
|
|
478
|
+
];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function outputOf(result: unknown): string {
|
|
482
|
+
if (typeof result === "string") return result;
|
|
483
|
+
if (!result || typeof result !== "object") return "";
|
|
484
|
+
const candidate = result as {
|
|
485
|
+
output?: unknown;
|
|
486
|
+
stdout?: unknown;
|
|
487
|
+
stderr?: unknown;
|
|
488
|
+
};
|
|
489
|
+
return [candidate.output, candidate.stdout, candidate.stderr]
|
|
490
|
+
.filter((value): value is string => typeof value === "string")
|
|
491
|
+
.join("\n");
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function runCredentialCommand(
|
|
495
|
+
session: RunCredentialCommandSession,
|
|
496
|
+
cmd: string,
|
|
497
|
+
commandRunner?: RunCredentialCommandRunner,
|
|
498
|
+
): Promise<void> {
|
|
499
|
+
const marked = `set -eu\n${cmd}\nprintf '\\n${COMMAND_OK_MARKER}\\n'`;
|
|
500
|
+
const args: ExecCommandArgs = {
|
|
501
|
+
cmd: `env -u BASH_ENV bash --noprofile --norc -c ${shellQuote(marked)}`,
|
|
502
|
+
yieldTimeMs: 120_000,
|
|
503
|
+
maxOutputTokens: 4_000,
|
|
504
|
+
};
|
|
505
|
+
const result = commandRunner
|
|
506
|
+
? await commandRunner(session, args)
|
|
507
|
+
: session.exec
|
|
508
|
+
? await session.exec(args)
|
|
509
|
+
: session.execCommand
|
|
510
|
+
? await session.execCommand(args)
|
|
511
|
+
: undefined;
|
|
512
|
+
if (!outputOf(result).includes(COMMAND_OK_MARKER)) {
|
|
513
|
+
throw new Error("run credential materialization command failed");
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async function writeCredentialFile(
|
|
518
|
+
session: RunCredentialCommandSession,
|
|
519
|
+
path: string,
|
|
520
|
+
content: string,
|
|
521
|
+
commandRunner?: RunCredentialCommandRunner,
|
|
522
|
+
): Promise<void> {
|
|
523
|
+
await runCredentialCommand(session, `: > ${shellQuote(path)}`, commandRunner);
|
|
524
|
+
const encoded = base64(content);
|
|
525
|
+
for (let offset = 0; offset < encoded.length; offset += WRITE_CHUNK_BYTES) {
|
|
526
|
+
const chunk = encoded.slice(offset, offset + WRITE_CHUNK_BYTES);
|
|
527
|
+
await runCredentialCommand(
|
|
528
|
+
session,
|
|
529
|
+
`printf %s ${shellQuote(chunk)} | ${portableBase64DecodeCommand()} >> ${shellQuote(path)}`,
|
|
530
|
+
commandRunner,
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
await runCredentialCommand(
|
|
534
|
+
session,
|
|
535
|
+
`[ "$(wc -c < ${shellQuote(path)} | tr -d '[:space:]')" = ${shellQuote(String(byteLength(content)))} ]`,
|
|
536
|
+
commandRunner,
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export type MaterializeRunCredentialsOptions = {
|
|
541
|
+
sessionId: string;
|
|
542
|
+
attemptId: string;
|
|
543
|
+
executionGeneration: number;
|
|
544
|
+
/** Initial provision after recovery may prune generations from older attempts. */
|
|
545
|
+
pruneOtherAttempts?: boolean;
|
|
546
|
+
/** An all-auth-needed replacement removes every prior readable generation. */
|
|
547
|
+
prunePreviousGenerations?: boolean;
|
|
548
|
+
/** Renewal keeps the active and immediately prior same-attempt generations. */
|
|
549
|
+
pruneSupersededGenerations?: boolean;
|
|
550
|
+
commandRunner?: RunCredentialCommandRunner;
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
/** Materialize one complete immutable generation, then atomically flip current. */
|
|
554
|
+
export async function materializeRunCredentials(
|
|
555
|
+
session: RunCredentialCommandSession,
|
|
556
|
+
material: NormalizedRunCredentialMaterial,
|
|
557
|
+
options: MaterializeRunCredentialsOptions,
|
|
558
|
+
): Promise<void> {
|
|
559
|
+
const root = runCredentialRoot(options.sessionId);
|
|
560
|
+
const attemptId = assertPathIdentity(options.attemptId, "attemptId");
|
|
561
|
+
if (!Number.isSafeInteger(options.executionGeneration) || options.executionGeneration < 0) {
|
|
562
|
+
throw new RunCredentialValidationError("executionGeneration must be a non-negative integer");
|
|
563
|
+
}
|
|
564
|
+
const nonce = crypto.randomUUID();
|
|
565
|
+
const prefix = `${attemptId}-${options.executionGeneration}-`;
|
|
566
|
+
const versionName = `${attemptId}-${options.executionGeneration}-${nonce}`;
|
|
567
|
+
const stage = `${root}/versions/.stage-${versionName}`;
|
|
568
|
+
const version = `${root}/versions/${versionName}`;
|
|
569
|
+
for (const file of material.files) {
|
|
570
|
+
const mode = file.mode ?? "0600";
|
|
571
|
+
if (mode !== "0400" && mode !== "0600") {
|
|
572
|
+
throw new RunCredentialValidationError(`run credential file mode is invalid: ${file.path}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
await runCredentialCommand(
|
|
576
|
+
session,
|
|
577
|
+
[
|
|
578
|
+
"umask 077",
|
|
579
|
+
`mkdir -p -- ${shellQuote(`${root}/versions`)}`,
|
|
580
|
+
`chmod 0700 -- ${shellQuote(root)} ${shellQuote(`${root}/versions`)}`,
|
|
581
|
+
`rm -rf -- ${shellQuote(stage)}`,
|
|
582
|
+
`mkdir -p -- ${shellQuote(`${stage}/files`)}`,
|
|
583
|
+
].join("\n"),
|
|
584
|
+
options.commandRunner,
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
const environmentLines = [
|
|
588
|
+
...Object.entries(material.environment).map(
|
|
589
|
+
([name, value]) => `export ${name}=${shellQuote(value)}`,
|
|
590
|
+
),
|
|
591
|
+
...Object.entries(material.fileEnvironment).map(
|
|
592
|
+
([name, path]) => `export ${name}=${shellQuote(`${version}/files/${path}`)}`,
|
|
593
|
+
),
|
|
594
|
+
];
|
|
595
|
+
await writeCredentialFile(
|
|
596
|
+
session,
|
|
597
|
+
`${stage}/env`,
|
|
598
|
+
environmentLines.length > 0 ? `${environmentLines.join("\n")}\n` : "",
|
|
599
|
+
options.commandRunner,
|
|
600
|
+
);
|
|
601
|
+
await runCredentialCommand(
|
|
602
|
+
session,
|
|
603
|
+
`chmod 0600 -- ${shellQuote(`${stage}/env`)}`,
|
|
604
|
+
options.commandRunner,
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
for (const file of material.files) {
|
|
608
|
+
const target = `${stage}/files/${file.path}`;
|
|
609
|
+
const parent = target.slice(0, target.lastIndexOf("/"));
|
|
610
|
+
await runCredentialCommand(session, `mkdir -p -- ${shellQuote(parent)}`, options.commandRunner);
|
|
611
|
+
await writeCredentialFile(session, target, file.content, options.commandRunner);
|
|
612
|
+
await runCredentialCommand(
|
|
613
|
+
session,
|
|
614
|
+
`chmod ${shellQuote(file.mode ?? "0600")} -- ${shellQuote(target)}`,
|
|
615
|
+
options.commandRunner,
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
await runCredentialCommand(
|
|
620
|
+
session,
|
|
621
|
+
[
|
|
622
|
+
...pointerLockAcquireCommands(root),
|
|
623
|
+
`previous=$(cat -- ${shellQuote(`${root}/current`)} 2>/dev/null || :)`,
|
|
624
|
+
`mv -- ${shellQuote(stage)} ${shellQuote(version)}`,
|
|
625
|
+
`printf '%s\\n' ${shellQuote(versionName)} > ${shellQuote(`${root}/.next`)}`,
|
|
626
|
+
`mv -f -- ${shellQuote(`${root}/.next`)} ${shellQuote(`${root}/current`)}`,
|
|
627
|
+
...(options.prunePreviousGenerations
|
|
628
|
+
? [
|
|
629
|
+
`find ${shellQuote(`${root}/versions`)} -mindepth 1 -maxdepth 1 ! -name ${shellQuote(versionName)} -exec rm -rf -- {} +`,
|
|
630
|
+
]
|
|
631
|
+
: options.pruneOtherAttempts
|
|
632
|
+
? [
|
|
633
|
+
`find ${shellQuote(`${root}/versions`)} -mindepth 1 -maxdepth 1 ! -name ${shellQuote(`${attemptId}-${options.executionGeneration}-*`)} -exec rm -rf -- {} +`,
|
|
634
|
+
]
|
|
635
|
+
: options.pruneSupersededGenerations
|
|
636
|
+
? [
|
|
637
|
+
`for candidate in ${shellQuote(`${root}/versions`)}/${prefix}*; do`,
|
|
638
|
+
` [ -e "$candidate" ] || continue`,
|
|
639
|
+
` name=\${candidate##*/}`,
|
|
640
|
+
` [ "$name" = ${shellQuote(versionName)} ] || [ "$name" = "$previous" ] || rm -rf -- "$candidate"`,
|
|
641
|
+
"done",
|
|
642
|
+
]
|
|
643
|
+
: []),
|
|
644
|
+
...pointerLockReleaseCommands(),
|
|
645
|
+
].join("\n"),
|
|
646
|
+
options.commandRunner,
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
export async function clearRunCredentials(
|
|
651
|
+
session: RunCredentialCommandSession,
|
|
652
|
+
sessionId: string,
|
|
653
|
+
commandRunner?: RunCredentialCommandRunner,
|
|
654
|
+
): Promise<void> {
|
|
655
|
+
const root = runCredentialRoot(sessionId);
|
|
656
|
+
await runCredentialCommand(
|
|
657
|
+
session,
|
|
658
|
+
[
|
|
659
|
+
`if [ -d ${shellQuote(root)} ]; then`,
|
|
660
|
+
...pointerLockAcquireCommands(root).map((line) => ` ${line}`),
|
|
661
|
+
` rm -f -- ${shellQuote(`${root}/current`)} ${shellQuote(`${root}/.next`)}`,
|
|
662
|
+
` rm -rf -- ${shellQuote(`${root}/versions`)}`,
|
|
663
|
+
...pointerLockReleaseCommands().map((line) => ` ${line}`),
|
|
664
|
+
"fi",
|
|
665
|
+
].join("\n"),
|
|
666
|
+
commandRunner,
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Remove only generations owned by one attempt. A successor may already have
|
|
672
|
+
* atomically activated its own pointer after queue admission; this cleanup must
|
|
673
|
+
* never clear or overwrite that newer generation.
|
|
674
|
+
*/
|
|
675
|
+
export async function clearRunCredentialsForAttempt(
|
|
676
|
+
session: RunCredentialCommandSession,
|
|
677
|
+
options: Pick<
|
|
678
|
+
MaterializeRunCredentialsOptions,
|
|
679
|
+
"sessionId" | "attemptId" | "executionGeneration"
|
|
680
|
+
>,
|
|
681
|
+
commandRunner?: RunCredentialCommandRunner,
|
|
682
|
+
): Promise<void> {
|
|
683
|
+
const root = runCredentialRoot(options.sessionId);
|
|
684
|
+
const attemptId = assertPathIdentity(options.attemptId, "attemptId");
|
|
685
|
+
if (!Number.isSafeInteger(options.executionGeneration) || options.executionGeneration < 0) {
|
|
686
|
+
throw new RunCredentialValidationError("executionGeneration must be a non-negative integer");
|
|
687
|
+
}
|
|
688
|
+
const prefix = `${attemptId}-${options.executionGeneration}-`;
|
|
689
|
+
await runCredentialCommand(
|
|
690
|
+
session,
|
|
691
|
+
[
|
|
692
|
+
`if [ -d ${shellQuote(root)} ]; then`,
|
|
693
|
+
...pointerLockAcquireCommands(root).map((line) => ` ${line}`),
|
|
694
|
+
` if [ -r ${shellQuote(`${root}/current`)} ]; then`,
|
|
695
|
+
` current=$(cat -- ${shellQuote(`${root}/current`)} 2>/dev/null || :)`,
|
|
696
|
+
` case "$current" in ${shellQuote(prefix)}*) rm -f -- ${shellQuote(`${root}/current`)} ;; esac`,
|
|
697
|
+
" fi",
|
|
698
|
+
` if [ -d ${shellQuote(`${root}/versions`)} ]; then`,
|
|
699
|
+
` find ${shellQuote(`${root}/versions`)} -mindepth 1 -maxdepth 1 -name ${shellQuote(`${prefix}*`)} -exec rm -rf -- {} +`,
|
|
700
|
+
` find ${shellQuote(`${root}/versions`)} -mindepth 1 -maxdepth 1 -name ${shellQuote(`.stage-${prefix}*`)} -exec rm -rf -- {} +`,
|
|
701
|
+
" fi",
|
|
702
|
+
...pointerLockReleaseCommands().map((line) => ` ${line}`),
|
|
703
|
+
"fi",
|
|
704
|
+
].join("\n"),
|
|
705
|
+
commandRunner,
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** Prefix one sandbox command so every new process observes the active generation. */
|
|
710
|
+
export function withRunCredentialEnvironment(cmd: string, sessionId: string): string {
|
|
711
|
+
const root = runCredentialRoot(sessionId);
|
|
712
|
+
const pointerFile = runCredentialPointerFile(sessionId);
|
|
713
|
+
return [
|
|
714
|
+
`if [ -r ${shellQuote(pointerFile)} ]; then`,
|
|
715
|
+
` _opengeni_credential_version=$(cat -- ${shellQuote(pointerFile)} 2>/dev/null || :)`,
|
|
716
|
+
' case "$_opengeni_credential_version" in',
|
|
717
|
+
" ''|*[!A-Za-z0-9_-]*) ;;",
|
|
718
|
+
" *)",
|
|
719
|
+
` _opengeni_credential_env=${shellQuote(`${root}/versions`)}/"$_opengeni_credential_version/env"`,
|
|
720
|
+
' if [ -r "$_opengeni_credential_env" ]; then',
|
|
721
|
+
" set -a",
|
|
722
|
+
' . "$_opengeni_credential_env"',
|
|
723
|
+
" set +a",
|
|
724
|
+
" fi",
|
|
725
|
+
" unset _opengeni_credential_env",
|
|
726
|
+
" ;;",
|
|
727
|
+
" esac",
|
|
728
|
+
" unset _opengeni_credential_version",
|
|
729
|
+
"fi",
|
|
730
|
+
cmd,
|
|
731
|
+
].join("\n");
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Preserve the provider session identity/capabilities while decorating only
|
|
736
|
+
* command creation. Non-command methods stay bound to the original instance.
|
|
737
|
+
*/
|
|
738
|
+
export function withRunCredentialsSession<T extends object>(session: T, sessionId: string): T {
|
|
739
|
+
return new Proxy(session, {
|
|
740
|
+
get(target, property, receiver) {
|
|
741
|
+
if (property === "exec" || property === "execCommand") {
|
|
742
|
+
const command = Reflect.get(target, property, target) as
|
|
743
|
+
| ((args: ExecCommandArgs) => Promise<unknown>)
|
|
744
|
+
| undefined;
|
|
745
|
+
if (!command) return undefined;
|
|
746
|
+
return async (args: ExecCommandArgs) =>
|
|
747
|
+
await command.call(target, {
|
|
748
|
+
...args,
|
|
749
|
+
cmd: withRunCredentialEnvironment(args.cmd, sessionId),
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
const value = Reflect.get(target, property, receiver) as unknown;
|
|
753
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
754
|
+
},
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/** Decorate every client-created/resumed session after the host seeds it. */
|
|
759
|
+
export function withRunCredentialsClient(
|
|
760
|
+
client: SandboxClient,
|
|
761
|
+
sessionId: string,
|
|
762
|
+
onSessionReady?: RunCredentialSessionReady,
|
|
763
|
+
): SandboxClient {
|
|
764
|
+
const decorated = new WeakMap<object, SandboxSessionLike>();
|
|
765
|
+
const wrap = async <T extends SandboxSessionLike>(session: T): Promise<T> => {
|
|
766
|
+
const existing = decorated.get(session);
|
|
767
|
+
if (existing) return existing as T;
|
|
768
|
+
await onSessionReady?.(session);
|
|
769
|
+
const wrapped = withRunCredentialsSession(session, sessionId);
|
|
770
|
+
decorated.set(session, wrapped);
|
|
771
|
+
return wrapped;
|
|
772
|
+
};
|
|
773
|
+
return {
|
|
774
|
+
backendId: client.backendId,
|
|
775
|
+
...(client.supportsDefaultOptions !== undefined
|
|
776
|
+
? { supportsDefaultOptions: client.supportsDefaultOptions }
|
|
777
|
+
: {}),
|
|
778
|
+
...(client.create
|
|
779
|
+
? {
|
|
780
|
+
create: async (...args: any[]) => await wrap(await (client.create as any)(...args)),
|
|
781
|
+
}
|
|
782
|
+
: {}),
|
|
783
|
+
...(client.resume
|
|
784
|
+
? {
|
|
785
|
+
resume: async (...args: any[]) => await wrap(await (client.resume as any)(...args)),
|
|
786
|
+
}
|
|
787
|
+
: {}),
|
|
788
|
+
...(client.delete
|
|
789
|
+
? {
|
|
790
|
+
delete: async (state: SandboxSessionState) => await client.delete!(state),
|
|
791
|
+
}
|
|
792
|
+
: {}),
|
|
793
|
+
...(client.serializeSessionState
|
|
794
|
+
? {
|
|
795
|
+
serializeSessionState: async (state: SandboxSessionState, options) =>
|
|
796
|
+
await client.serializeSessionState!(state, options),
|
|
797
|
+
}
|
|
798
|
+
: {}),
|
|
799
|
+
...(client.canPersistOwnedSessionState
|
|
800
|
+
? {
|
|
801
|
+
canPersistOwnedSessionState: async (state: SandboxSessionState) =>
|
|
802
|
+
await client.canPersistOwnedSessionState!(state),
|
|
803
|
+
}
|
|
804
|
+
: {}),
|
|
805
|
+
...(client.canReusePreservedOwnedSession
|
|
806
|
+
? {
|
|
807
|
+
canReusePreservedOwnedSession: async (state: SandboxSessionState) =>
|
|
808
|
+
await client.canReusePreservedOwnedSession!(state),
|
|
809
|
+
}
|
|
810
|
+
: {}),
|
|
811
|
+
...(client.deserializeSessionState
|
|
812
|
+
? {
|
|
813
|
+
deserializeSessionState: async (state: Record<string, unknown>) =>
|
|
814
|
+
await client.deserializeSessionState!(state),
|
|
815
|
+
}
|
|
816
|
+
: {}),
|
|
817
|
+
};
|
|
818
|
+
}
|