@zocomputer/agent-sdk 0.5.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/LICENSE +21 -0
- package/README.md +673 -0
- package/dist/attachments.js +52 -0
- package/dist/gateway-fetch.js +67 -0
- package/dist/index.js +4169 -0
- package/dist/initiator-auth.js +49 -0
- package/dist/platform/agent-sandbox/index.js +691 -0
- package/dist/platform/cloud-tools/image.js +498 -0
- package/dist/platform/cloud-tools/index.js +507 -0
- package/dist/platform/cloud-tools/web-search.js +87 -0
- package/dist/platform/runtime-ai/gateway.js +87 -0
- package/dist/platform/runtime-ai/index.js +91 -0
- package/dist/platform/runtime-ai/register.js +88 -0
- package/dist/platform/runtime-ai/session-fetch.js +54 -0
- package/dist/platform/runtime-auth/index.js +141 -0
- package/dist/state-files.js +287 -0
- package/dist/state-sandbox.js +383 -0
- package/dist/state.js +29 -0
- package/dist/steer-inbox.js +84 -0
- package/dist/steer.js +83 -0
- package/package.json +143 -0
- package/platform/agent-sandbox/api-client.ts +196 -0
- package/platform/agent-sandbox/index.ts +27 -0
- package/platform/agent-sandbox/pure.ts +83 -0
- package/platform/agent-sandbox/sftp.ts +141 -0
- package/platform/agent-sandbox/ssh-connection.ts +165 -0
- package/platform/agent-sandbox/ssh-exec.ts +98 -0
- package/platform/agent-sandbox/ssh-session.ts +487 -0
- package/platform/agent-sandbox/zo-backend.ts +88 -0
- package/platform/agent-sandbox/zo-sandbox.ts +39 -0
- package/platform/cloud-tools/image-path.ts +44 -0
- package/platform/cloud-tools/image.ts +225 -0
- package/platform/cloud-tools/index.ts +22 -0
- package/platform/cloud-tools/state-files.ts +368 -0
- package/platform/cloud-tools/tool-meta.ts +32 -0
- package/platform/cloud-tools/web-search.ts +15 -0
- package/platform/runtime-ai/gateway.ts +76 -0
- package/platform/runtime-ai/index.ts +20 -0
- package/platform/runtime-ai/register.ts +26 -0
- package/platform/runtime-ai/session-fetch.ts +124 -0
- package/platform/runtime-auth/index.ts +331 -0
- package/src/async-tasks.ts +273 -0
- package/src/attachments.ts +109 -0
- package/src/backgroundable.ts +88 -0
- package/src/bounded-output.ts +159 -0
- package/src/dir-conventions.ts +238 -0
- package/src/extract/cache.ts +40 -0
- package/src/extract/docx.ts +18 -0
- package/src/extract/pdf.ts +54 -0
- package/src/extract/sheet.ts +56 -0
- package/src/file-kind.ts +258 -0
- package/src/file-view.ts +80 -0
- package/src/gateway-fetch.ts +115 -0
- package/src/glob-match.ts +13 -0
- package/src/hooks.ts +213 -0
- package/src/index.ts +419 -0
- package/src/initiator-auth.ts +81 -0
- package/src/instructions.ts +224 -0
- package/src/list-files.ts +41 -0
- package/src/mock-model.ts +572 -0
- package/src/park-delivery.ts +247 -0
- package/src/path-locks.ts +52 -0
- package/src/read-file-content.ts +142 -0
- package/src/read-text.ts +40 -0
- package/src/redeliver.ts +155 -0
- package/src/run.ts +159 -0
- package/src/sandbox-io.ts +414 -0
- package/src/state-files.ts +460 -0
- package/src/state-sandbox.ts +710 -0
- package/src/state.ts +96 -0
- package/src/steer-inbox.ts +105 -0
- package/src/steer-tool.ts +83 -0
- package/src/steer.ts +146 -0
- package/src/task.ts +320 -0
- package/src/tools/bash.ts +143 -0
- package/src/tools/edit.ts +58 -0
- package/src/tools/glob.ts +56 -0
- package/src/tools/grep.ts +188 -0
- package/src/tools/read.ts +319 -0
- package/src/tools/tasks.ts +241 -0
- package/src/tools/webfetch.ts +368 -0
- package/src/tools/write.ts +42 -0
- package/src/walk.ts +112 -0
- package/src/watch-output.ts +104 -0
- package/src/web-fetch.ts +324 -0
- package/src/web-page.ts +179 -0
- package/src/workspace-io.ts +225 -0
- package/src/workspace.ts +41 -0
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
/** Runtime client contract for external-state sandbox `exec` + `files` access. */
|
|
2
|
+
|
|
3
|
+
import { normalizeStateFilePath } from "./state-files";
|
|
4
|
+
|
|
5
|
+
export type StateSandboxAccess = "r" | "rw";
|
|
6
|
+
export type StateSandboxInterface = "exec" | "files";
|
|
7
|
+
export type StateSandboxEngine = "sandbox-daytona";
|
|
8
|
+
export type StateSandboxPartition = "none" | "team" | "user" | "session";
|
|
9
|
+
export type StateSandboxLifecycle = "ready" | "resuming";
|
|
10
|
+
|
|
11
|
+
export const STATE_SANDBOX_HANDLE_PATH = "/state/handles";
|
|
12
|
+
export const ZO_AGENT_TOKEN_HEADER = "x-zo-agent-token";
|
|
13
|
+
export const ZO_EVE_SESSION_HEADER = "x-zo-eve-session";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Short-lived SSH access for a broker-owned sandbox state instance.
|
|
17
|
+
*
|
|
18
|
+
* `sshUser` is a scoped bearer credential, not a human username; do not log full handles.
|
|
19
|
+
*/
|
|
20
|
+
export interface StateSandboxSshAccess {
|
|
21
|
+
readonly sandboxId: string;
|
|
22
|
+
readonly sshHost: string;
|
|
23
|
+
readonly sshUser: string;
|
|
24
|
+
readonly expiresAt: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Broker response for a sandbox state instance.
|
|
29
|
+
*
|
|
30
|
+
* IDs, `partition`, `rootPath`, and `lifecycle` are safe for diagnostics. Treat `ssh` as a
|
|
31
|
+
* bearer secret; it grants temporary access to the VM.
|
|
32
|
+
*/
|
|
33
|
+
export interface StateSandboxHandle {
|
|
34
|
+
readonly handleId: string;
|
|
35
|
+
readonly declarationName: string;
|
|
36
|
+
readonly interface: StateSandboxInterface;
|
|
37
|
+
readonly access: StateSandboxAccess;
|
|
38
|
+
readonly engine: StateSandboxEngine;
|
|
39
|
+
readonly storeId: string;
|
|
40
|
+
readonly stateInstanceId: string;
|
|
41
|
+
readonly partition: StateSandboxPartition;
|
|
42
|
+
readonly sandboxResourceId: string;
|
|
43
|
+
readonly rootPath: string;
|
|
44
|
+
readonly lifecycle: StateSandboxLifecycle;
|
|
45
|
+
readonly sandbox: StateSandboxSshAccess;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface StateSandboxSuggestedDefaults {
|
|
49
|
+
readonly engine?: StateSandboxEngine;
|
|
50
|
+
readonly partition?: StateSandboxPartition;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface StateSandboxHandleRequest {
|
|
54
|
+
readonly declarationName: string;
|
|
55
|
+
readonly interface: StateSandboxInterface;
|
|
56
|
+
readonly access: StateSandboxAccess;
|
|
57
|
+
readonly suggestedDefaults: StateSandboxSuggestedDefaults;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface StateSandboxHandleFetch {
|
|
61
|
+
(input: string | URL | Request, init?: RequestInit): Promise<Response>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type StateSandboxHeadersInit =
|
|
65
|
+
| Headers
|
|
66
|
+
| ReadonlyArray<readonly [string, string]>
|
|
67
|
+
| Readonly<Record<string, string>>;
|
|
68
|
+
|
|
69
|
+
export interface RequestStateSandboxHandleOptions {
|
|
70
|
+
readonly fetch: StateSandboxHandleFetch;
|
|
71
|
+
readonly apiBaseUrl: string | URL;
|
|
72
|
+
readonly declarationName: string;
|
|
73
|
+
readonly interface: StateSandboxInterface;
|
|
74
|
+
readonly access: StateSandboxAccess;
|
|
75
|
+
/** Agent bearer token sent to the runtime broker as `x-zo-agent-token`. */
|
|
76
|
+
readonly agentToken?: string;
|
|
77
|
+
/** eve session key sent as `x-zo-eve-session`; the route derives resolver session identity from auth context. */
|
|
78
|
+
readonly eveSessionKey?: string;
|
|
79
|
+
/**
|
|
80
|
+
* Declaration defaults from `defineExternalState`. The sandbox client sends
|
|
81
|
+
* `engine: "sandbox-daytona"` by default so unbound exec declarations do
|
|
82
|
+
* not fall through to the broker's R2 zero-config default.
|
|
83
|
+
*/
|
|
84
|
+
readonly suggestedDefaults?: StateSandboxSuggestedDefaults;
|
|
85
|
+
/** Extra headers; cannot override the SDK-managed content type or Zo auth headers. */
|
|
86
|
+
readonly headers?: StateSandboxHeadersInit;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class StateSandboxHandleError extends Error {
|
|
90
|
+
readonly status: number;
|
|
91
|
+
readonly code: string | null;
|
|
92
|
+
|
|
93
|
+
constructor(
|
|
94
|
+
message: string,
|
|
95
|
+
options: { status: number; code?: string | null },
|
|
96
|
+
) {
|
|
97
|
+
super(message);
|
|
98
|
+
this.name = "StateSandboxHandleError";
|
|
99
|
+
this.status = options.status;
|
|
100
|
+
this.code = options.code ?? null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function requestStateSandboxHandle(
|
|
105
|
+
options: RequestStateSandboxHandleOptions,
|
|
106
|
+
): Promise<StateSandboxHandle> {
|
|
107
|
+
const response = await options.fetch(
|
|
108
|
+
buildStateSandboxHandleUrl(options.apiBaseUrl),
|
|
109
|
+
{
|
|
110
|
+
method: "POST",
|
|
111
|
+
headers: buildStateSandboxHandleHeaders(options),
|
|
112
|
+
body: JSON.stringify(buildStateSandboxHandleRequest(options)),
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
const json: unknown = await response.json().catch(() => null);
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
const error = parseStateSandboxBrokerError(json);
|
|
118
|
+
throw new StateSandboxHandleError(error.message, {
|
|
119
|
+
status: response.status,
|
|
120
|
+
code: error.code,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const handle = parseStateSandboxHandle(json);
|
|
124
|
+
if (handle === null) {
|
|
125
|
+
throw new StateSandboxHandleError(
|
|
126
|
+
"state sandbox broker returned a malformed handle",
|
|
127
|
+
{
|
|
128
|
+
status: response.status,
|
|
129
|
+
code: "malformed_handle",
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return handle;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function parseStateSandboxHandle(
|
|
137
|
+
value: unknown,
|
|
138
|
+
): StateSandboxHandle | null {
|
|
139
|
+
if (!isRecord(value)) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const access = parseStateSandboxAccess(value.access);
|
|
143
|
+
const iface = parseStateSandboxInterface(value.interface);
|
|
144
|
+
const partition = parseStateSandboxPartition(value.partition);
|
|
145
|
+
const lifecycle = parseStateSandboxLifecycle(value.lifecycle ?? value.status);
|
|
146
|
+
const sandbox = parseStateSandboxSshAccess(
|
|
147
|
+
value.sandbox ?? value.ssh ?? value.accessHandle ?? value.sshAccess,
|
|
148
|
+
);
|
|
149
|
+
if (
|
|
150
|
+
access === null ||
|
|
151
|
+
iface === null ||
|
|
152
|
+
partition === null ||
|
|
153
|
+
lifecycle === null ||
|
|
154
|
+
sandbox === null ||
|
|
155
|
+
value.engine !== "sandbox-daytona"
|
|
156
|
+
) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const handleId = readString(value, "handleId");
|
|
160
|
+
const declarationName = readString(value, "declarationName");
|
|
161
|
+
const storeId = readString(value, "storeId");
|
|
162
|
+
const stateInstanceId = readString(value, "stateInstanceId");
|
|
163
|
+
const sandboxResourceId = readString(value, "sandboxResourceId");
|
|
164
|
+
const rootPath = readString(value, "rootPath");
|
|
165
|
+
if (
|
|
166
|
+
handleId === null ||
|
|
167
|
+
declarationName === null ||
|
|
168
|
+
storeId === null ||
|
|
169
|
+
stateInstanceId === null ||
|
|
170
|
+
sandboxResourceId === null ||
|
|
171
|
+
rootPath === null ||
|
|
172
|
+
!rootPath.startsWith("/")
|
|
173
|
+
) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return Object.freeze({
|
|
177
|
+
handleId,
|
|
178
|
+
declarationName,
|
|
179
|
+
interface: iface,
|
|
180
|
+
access,
|
|
181
|
+
engine: "sandbox-daytona",
|
|
182
|
+
storeId,
|
|
183
|
+
stateInstanceId,
|
|
184
|
+
partition,
|
|
185
|
+
sandboxResourceId,
|
|
186
|
+
rootPath,
|
|
187
|
+
lifecycle,
|
|
188
|
+
sandbox,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface StateSandboxRunOptions {
|
|
193
|
+
readonly workingDirectory?: string;
|
|
194
|
+
/** Extra env for this command. Ambient process env is never read implicitly. */
|
|
195
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
196
|
+
readonly abortSignal?: AbortSignal;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface StateSandboxRunResult {
|
|
200
|
+
readonly exitCode: number;
|
|
201
|
+
readonly stdout: string;
|
|
202
|
+
readonly stderr: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface StateSandboxSpawnedProcess {
|
|
206
|
+
readonly stdout: AsyncIterable<Uint8Array>;
|
|
207
|
+
readonly stderr: AsyncIterable<Uint8Array>;
|
|
208
|
+
readonly exitCode: Promise<number>;
|
|
209
|
+
kill(signal?: string): Promise<void> | void;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface StateSandboxSessionLike {
|
|
213
|
+
run(options: {
|
|
214
|
+
readonly command: string;
|
|
215
|
+
readonly workingDirectory?: string;
|
|
216
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
217
|
+
readonly abortSignal?: AbortSignal;
|
|
218
|
+
}): PromiseLike<StateSandboxRunResult>;
|
|
219
|
+
spawn(options: {
|
|
220
|
+
readonly command: string;
|
|
221
|
+
readonly workingDirectory?: string;
|
|
222
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
223
|
+
readonly abortSignal?: AbortSignal;
|
|
224
|
+
}): PromiseLike<StateSandboxSpawnedProcess>;
|
|
225
|
+
readBinaryFile(options: {
|
|
226
|
+
readonly path: string;
|
|
227
|
+
}): PromiseLike<Uint8Array | null>;
|
|
228
|
+
writeBinaryFile(options: {
|
|
229
|
+
readonly path: string;
|
|
230
|
+
readonly content: Uint8Array;
|
|
231
|
+
}): PromiseLike<void>;
|
|
232
|
+
removePath(options: {
|
|
233
|
+
readonly path: string;
|
|
234
|
+
readonly recursive?: boolean;
|
|
235
|
+
readonly force?: boolean;
|
|
236
|
+
}): PromiseLike<void>;
|
|
237
|
+
dispose?(): PromiseLike<void> | void;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export interface StateSandboxSessionFactory {
|
|
241
|
+
(handle: StateSandboxHandle): PromiseLike<StateSandboxSessionLike>;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export interface StateSandboxFilesClient {
|
|
245
|
+
read(path: string): Promise<Uint8Array | null>;
|
|
246
|
+
write(path: string, content: string | Uint8Array): Promise<void>;
|
|
247
|
+
delete(
|
|
248
|
+
path: string,
|
|
249
|
+
options?: { readonly recursive?: boolean; readonly force?: boolean },
|
|
250
|
+
): Promise<void>;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export type StateSandboxStatus =
|
|
254
|
+
| { readonly status: "idle" }
|
|
255
|
+
| { readonly status: "resuming"; readonly handleId: string }
|
|
256
|
+
| { readonly status: "ready"; readonly handleId: string };
|
|
257
|
+
|
|
258
|
+
export interface StateSandboxClient {
|
|
259
|
+
readonly files: StateSandboxFilesClient;
|
|
260
|
+
status(): StateSandboxStatus;
|
|
261
|
+
exec(
|
|
262
|
+
command: string,
|
|
263
|
+
options?: StateSandboxRunOptions,
|
|
264
|
+
): Promise<StateSandboxRunResult>;
|
|
265
|
+
spawn(
|
|
266
|
+
command: string,
|
|
267
|
+
options?: StateSandboxRunOptions,
|
|
268
|
+
): Promise<StateSandboxSpawnedProcess>;
|
|
269
|
+
dispose(): Promise<void>;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export interface CreateStateSandboxClientOptions {
|
|
273
|
+
readonly loadHandle: () => Promise<StateSandboxHandle>;
|
|
274
|
+
readonly createSession: StateSandboxSessionFactory;
|
|
275
|
+
readonly now?: () => Date;
|
|
276
|
+
/** Reload the handle and rebuild the session when it expires within this window. Defaults to 60 seconds. */
|
|
277
|
+
readonly refreshWindowMs?: number;
|
|
278
|
+
/**
|
|
279
|
+
* Ambient runtime env a caller wants to preserve for session-scratch exec.
|
|
280
|
+
* Durable/shared handles (`team`, `user`, `none`) never receive it.
|
|
281
|
+
*/
|
|
282
|
+
readonly ambientEnv?: Readonly<Record<string, string>>;
|
|
283
|
+
/**
|
|
284
|
+
* Session-partitioned scratch is the only state class allowed to inherit ambient env.
|
|
285
|
+
* Defaults to false so durable state clients are clean-env by construction.
|
|
286
|
+
*/
|
|
287
|
+
readonly passAmbientEnvToSessionPartition?: boolean;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function createStateSandboxClient(
|
|
291
|
+
options: CreateStateSandboxClientOptions,
|
|
292
|
+
): StateSandboxClient {
|
|
293
|
+
const now = options.now ?? (() => new Date());
|
|
294
|
+
const refreshWindowMs = options.refreshWindowMs ?? 60_000;
|
|
295
|
+
interface CachedStateSandboxSession {
|
|
296
|
+
readonly handle: StateSandboxHandle;
|
|
297
|
+
readonly session: StateSandboxSessionLike;
|
|
298
|
+
activeUses: number;
|
|
299
|
+
disposeWhenIdle: boolean;
|
|
300
|
+
}
|
|
301
|
+
let cached: CachedStateSandboxSession | null = null;
|
|
302
|
+
let pending: Promise<CachedStateSandboxSession> | null = null;
|
|
303
|
+
let pendingLeaseWaiters = 0;
|
|
304
|
+
let status: StateSandboxStatus = { status: "idle" };
|
|
305
|
+
|
|
306
|
+
async function disposeCachedSession(
|
|
307
|
+
record: CachedStateSandboxSession,
|
|
308
|
+
options: { readonly waitForPendingLeases?: boolean } = {},
|
|
309
|
+
): Promise<void> {
|
|
310
|
+
if (
|
|
311
|
+
record.activeUses > 0 ||
|
|
312
|
+
(options.waitForPendingLeases === true && pendingLeaseWaiters > 0)
|
|
313
|
+
) {
|
|
314
|
+
record.disposeWhenIdle = true;
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (cached === record) {
|
|
318
|
+
cached = null;
|
|
319
|
+
}
|
|
320
|
+
await record.session.dispose?.();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function releaseCachedSession(
|
|
324
|
+
record: CachedStateSandboxSession,
|
|
325
|
+
): Promise<void> {
|
|
326
|
+
record.activeUses -= 1;
|
|
327
|
+
if (record.activeUses === 0 && record.disposeWhenIdle) {
|
|
328
|
+
await disposeCachedSession(record);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function ensureSession(): Promise<CachedStateSandboxSession> {
|
|
333
|
+
const current = cached;
|
|
334
|
+
if (
|
|
335
|
+
current !== null &&
|
|
336
|
+
!shouldRefreshStateSandboxHandle(current.handle, now(), refreshWindowMs)
|
|
337
|
+
) {
|
|
338
|
+
return Promise.resolve(current);
|
|
339
|
+
}
|
|
340
|
+
if (pending !== null) return pending;
|
|
341
|
+
|
|
342
|
+
const previous = cached;
|
|
343
|
+
pending = (async () => {
|
|
344
|
+
try {
|
|
345
|
+
const handle = await options.loadHandle();
|
|
346
|
+
if (handle.lifecycle === "resuming") {
|
|
347
|
+
status = { status: "resuming", handleId: handle.handleId };
|
|
348
|
+
}
|
|
349
|
+
const session = await options.createSession(handle);
|
|
350
|
+
const next: CachedStateSandboxSession = {
|
|
351
|
+
handle,
|
|
352
|
+
session,
|
|
353
|
+
activeUses: 0,
|
|
354
|
+
disposeWhenIdle: false,
|
|
355
|
+
};
|
|
356
|
+
cached = next;
|
|
357
|
+
status = { status: "ready", handleId: handle.handleId };
|
|
358
|
+
if (previous !== null) {
|
|
359
|
+
await disposeCachedSession(previous, { waitForPendingLeases: false });
|
|
360
|
+
}
|
|
361
|
+
return next;
|
|
362
|
+
} finally {
|
|
363
|
+
pending = null;
|
|
364
|
+
}
|
|
365
|
+
})();
|
|
366
|
+
|
|
367
|
+
return pending;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function leaseSession(): Promise<{
|
|
371
|
+
handle: StateSandboxHandle;
|
|
372
|
+
session: StateSandboxSessionLike;
|
|
373
|
+
release: () => Promise<void>;
|
|
374
|
+
}> {
|
|
375
|
+
const sessionPromise = ensureSession();
|
|
376
|
+
pendingLeaseWaiters += 1;
|
|
377
|
+
let record: CachedStateSandboxSession;
|
|
378
|
+
try {
|
|
379
|
+
record = await sessionPromise;
|
|
380
|
+
} finally {
|
|
381
|
+
pendingLeaseWaiters -= 1;
|
|
382
|
+
}
|
|
383
|
+
record.activeUses += 1;
|
|
384
|
+
return {
|
|
385
|
+
handle: record.handle,
|
|
386
|
+
session: record.session,
|
|
387
|
+
release: () => releaseCachedSession(record),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function buildRemotePath(handle: StateSandboxHandle, path: string): string {
|
|
392
|
+
return `${handle.rootPath}/${normalizeStateFilePath(path)}`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function workingDirectoryFor(
|
|
396
|
+
handle: StateSandboxHandle,
|
|
397
|
+
explicit: string | undefined,
|
|
398
|
+
): string {
|
|
399
|
+
return explicit === undefined
|
|
400
|
+
? handle.rootPath
|
|
401
|
+
: buildRemotePath(handle, explicit);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function commandEnv(
|
|
405
|
+
handle: StateSandboxHandle,
|
|
406
|
+
explicit: Readonly<Record<string, string>> | undefined,
|
|
407
|
+
): Readonly<Record<string, string>> | undefined {
|
|
408
|
+
const base =
|
|
409
|
+
options.passAmbientEnvToSessionPartition === true &&
|
|
410
|
+
handle.partition === "session"
|
|
411
|
+
? options.ambientEnv
|
|
412
|
+
: undefined;
|
|
413
|
+
if (base === undefined) {
|
|
414
|
+
return explicit;
|
|
415
|
+
}
|
|
416
|
+
if (explicit === undefined) {
|
|
417
|
+
return base;
|
|
418
|
+
}
|
|
419
|
+
return { ...base, ...explicit };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function maybeEnv(
|
|
423
|
+
handle: StateSandboxHandle,
|
|
424
|
+
explicit: Readonly<Record<string, string>> | undefined,
|
|
425
|
+
): { readonly env?: Readonly<Record<string, string>> } {
|
|
426
|
+
const env = commandEnv(handle, explicit);
|
|
427
|
+
return env === undefined ? {} : { env };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function withSession<T>(
|
|
431
|
+
use: (resolved: {
|
|
432
|
+
handle: StateSandboxHandle;
|
|
433
|
+
session: StateSandboxSessionLike;
|
|
434
|
+
}) => Promise<T>,
|
|
435
|
+
): Promise<T> {
|
|
436
|
+
const resolved = await leaseSession();
|
|
437
|
+
try {
|
|
438
|
+
return await use(resolved);
|
|
439
|
+
} finally {
|
|
440
|
+
await resolved.release();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function withWriteSession<T>(
|
|
445
|
+
use: (resolved: {
|
|
446
|
+
handle: StateSandboxHandle;
|
|
447
|
+
session: StateSandboxSessionLike;
|
|
448
|
+
}) => Promise<T>,
|
|
449
|
+
): Promise<T> {
|
|
450
|
+
return await withSession(async (resolved) => {
|
|
451
|
+
if (resolved.handle.access !== "rw") {
|
|
452
|
+
throw new Error(
|
|
453
|
+
`state sandbox handle "${resolved.handle.handleId}" is read-only`,
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
return await use(resolved);
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return {
|
|
461
|
+
files: {
|
|
462
|
+
async read(path) {
|
|
463
|
+
return await withSession(
|
|
464
|
+
async ({ handle, session }) =>
|
|
465
|
+
await session.readBinaryFile({
|
|
466
|
+
path: buildRemotePath(handle, path),
|
|
467
|
+
}),
|
|
468
|
+
);
|
|
469
|
+
},
|
|
470
|
+
async write(path, content) {
|
|
471
|
+
await withWriteSession(async ({ handle, session }) => {
|
|
472
|
+
const bytes =
|
|
473
|
+
typeof content === "string"
|
|
474
|
+
? new TextEncoder().encode(content)
|
|
475
|
+
: content;
|
|
476
|
+
await session.writeBinaryFile({
|
|
477
|
+
path: buildRemotePath(handle, path),
|
|
478
|
+
content: bytes,
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
},
|
|
482
|
+
async delete(path, deleteOptions) {
|
|
483
|
+
await withWriteSession(async ({ handle, session }) => {
|
|
484
|
+
await session.removePath({
|
|
485
|
+
path: buildRemotePath(handle, path),
|
|
486
|
+
...(deleteOptions?.recursive === undefined
|
|
487
|
+
? {}
|
|
488
|
+
: { recursive: deleteOptions.recursive }),
|
|
489
|
+
...(deleteOptions?.force === undefined
|
|
490
|
+
? {}
|
|
491
|
+
: { force: deleteOptions.force }),
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
status: () => status,
|
|
497
|
+
async exec(command, runOptions) {
|
|
498
|
+
return await withWriteSession(
|
|
499
|
+
async ({ handle, session }) =>
|
|
500
|
+
await session.run({
|
|
501
|
+
command,
|
|
502
|
+
workingDirectory: workingDirectoryFor(
|
|
503
|
+
handle,
|
|
504
|
+
runOptions?.workingDirectory,
|
|
505
|
+
),
|
|
506
|
+
...maybeEnv(handle, runOptions?.env),
|
|
507
|
+
...(runOptions?.abortSignal === undefined
|
|
508
|
+
? {}
|
|
509
|
+
: { abortSignal: runOptions.abortSignal }),
|
|
510
|
+
}),
|
|
511
|
+
);
|
|
512
|
+
},
|
|
513
|
+
async spawn(command, runOptions) {
|
|
514
|
+
return await withWriteSession(
|
|
515
|
+
async ({ handle, session }) =>
|
|
516
|
+
await session.spawn({
|
|
517
|
+
command,
|
|
518
|
+
workingDirectory: workingDirectoryFor(
|
|
519
|
+
handle,
|
|
520
|
+
runOptions?.workingDirectory,
|
|
521
|
+
),
|
|
522
|
+
...maybeEnv(handle, runOptions?.env),
|
|
523
|
+
...(runOptions?.abortSignal === undefined
|
|
524
|
+
? {}
|
|
525
|
+
: { abortSignal: runOptions.abortSignal }),
|
|
526
|
+
}),
|
|
527
|
+
);
|
|
528
|
+
},
|
|
529
|
+
async dispose() {
|
|
530
|
+
const resolved = pending === null ? cached : await pending;
|
|
531
|
+
if (resolved !== null) {
|
|
532
|
+
await disposeCachedSession(resolved, { waitForPendingLeases: true });
|
|
533
|
+
}
|
|
534
|
+
status = { status: "idle" };
|
|
535
|
+
},
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export function shouldRefreshStateSandboxHandle(
|
|
540
|
+
handle: StateSandboxHandle,
|
|
541
|
+
now: Date,
|
|
542
|
+
refreshWindowMs = 60_000,
|
|
543
|
+
): boolean {
|
|
544
|
+
const expiresAtMs = Date.parse(handle.sandbox.expiresAt);
|
|
545
|
+
if (!Number.isFinite(expiresAtMs)) {
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
return expiresAtMs - now.getTime() <= refreshWindowMs;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function buildStateSandboxHandleRequest(
|
|
552
|
+
options: RequestStateSandboxHandleOptions,
|
|
553
|
+
): StateSandboxHandleRequest {
|
|
554
|
+
return {
|
|
555
|
+
declarationName: options.declarationName,
|
|
556
|
+
interface: options.interface,
|
|
557
|
+
access: options.access,
|
|
558
|
+
suggestedDefaults: {
|
|
559
|
+
...options.suggestedDefaults,
|
|
560
|
+
engine: options.suggestedDefaults?.engine ?? "sandbox-daytona",
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function buildStateSandboxHandleUrl(apiBaseUrl: string | URL): string {
|
|
566
|
+
const url = new URL(String(apiBaseUrl));
|
|
567
|
+
url.pathname = joinUrlPath(url.pathname, STATE_SANDBOX_HANDLE_PATH);
|
|
568
|
+
url.search = "";
|
|
569
|
+
url.hash = "";
|
|
570
|
+
return url.toString();
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function joinUrlPath(basePath: string, childPath: string): string {
|
|
574
|
+
const base = basePath.replace(/\/+$/, "");
|
|
575
|
+
const child = childPath.replace(/^\/+/, "");
|
|
576
|
+
return `${base}/${child}`;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function buildStateSandboxHandleHeaders(
|
|
580
|
+
options: RequestStateSandboxHandleOptions,
|
|
581
|
+
): Headers {
|
|
582
|
+
const headers = createHeaders(options.headers);
|
|
583
|
+
headers.set("content-type", "application/json");
|
|
584
|
+
if (options.agentToken !== undefined) {
|
|
585
|
+
headers.set(ZO_AGENT_TOKEN_HEADER, options.agentToken);
|
|
586
|
+
}
|
|
587
|
+
if (options.eveSessionKey !== undefined) {
|
|
588
|
+
headers.set(ZO_EVE_SESSION_HEADER, options.eveSessionKey);
|
|
589
|
+
}
|
|
590
|
+
return headers;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function createHeaders(init: StateSandboxHeadersInit | undefined): Headers {
|
|
594
|
+
const headers = new Headers();
|
|
595
|
+
if (init === undefined) {
|
|
596
|
+
return headers;
|
|
597
|
+
}
|
|
598
|
+
if (init instanceof Headers) {
|
|
599
|
+
init.forEach((value, key) => headers.set(key, value));
|
|
600
|
+
return headers;
|
|
601
|
+
}
|
|
602
|
+
if (Array.isArray(init)) {
|
|
603
|
+
for (const [key, value] of init) {
|
|
604
|
+
headers.set(key, value);
|
|
605
|
+
}
|
|
606
|
+
return headers;
|
|
607
|
+
}
|
|
608
|
+
for (const [key, value] of Object.entries(init)) {
|
|
609
|
+
headers.set(key, value);
|
|
610
|
+
}
|
|
611
|
+
return headers;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function parseStateSandboxBrokerError(value: unknown): {
|
|
615
|
+
message: string;
|
|
616
|
+
code: string | null;
|
|
617
|
+
} {
|
|
618
|
+
if (!isRecord(value)) {
|
|
619
|
+
return { message: "state sandbox broker request failed", code: null };
|
|
620
|
+
}
|
|
621
|
+
const routeError = readString(value, "error");
|
|
622
|
+
if (routeError !== null) {
|
|
623
|
+
return {
|
|
624
|
+
message:
|
|
625
|
+
readString(value, "message") ?? "state sandbox broker request failed",
|
|
626
|
+
code: routeError,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
const error = isRecord(value.error) ? value.error : value;
|
|
630
|
+
const message =
|
|
631
|
+
readString(error, "message") ?? "state sandbox broker request failed";
|
|
632
|
+
const code = readString(error, "code") ?? readString(error, "error");
|
|
633
|
+
return { message, code };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function parseStateSandboxSshAccess(
|
|
637
|
+
value: unknown,
|
|
638
|
+
): StateSandboxSshAccess | null {
|
|
639
|
+
if (!isRecord(value)) {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
const sandboxId = readString(value, "sandboxId");
|
|
643
|
+
const sshHost = readString(value, "sshHost");
|
|
644
|
+
const sshUser = readString(value, "sshUser");
|
|
645
|
+
const expiresAt = readString(value, "expiresAt");
|
|
646
|
+
if (
|
|
647
|
+
sandboxId === null ||
|
|
648
|
+
sshHost === null ||
|
|
649
|
+
sshUser === null ||
|
|
650
|
+
expiresAt === null ||
|
|
651
|
+
!Number.isFinite(Date.parse(expiresAt))
|
|
652
|
+
) {
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
return Object.freeze({ sandboxId, sshHost, sshUser, expiresAt });
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function parseStateSandboxPartition(
|
|
659
|
+
value: unknown,
|
|
660
|
+
): StateSandboxPartition | null {
|
|
661
|
+
if (
|
|
662
|
+
value === "none" ||
|
|
663
|
+
value === "team" ||
|
|
664
|
+
value === "user" ||
|
|
665
|
+
value === "session"
|
|
666
|
+
) {
|
|
667
|
+
return value;
|
|
668
|
+
}
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function parseStateSandboxAccess(value: unknown): StateSandboxAccess | null {
|
|
673
|
+
if (value === "r" || value === "rw") {
|
|
674
|
+
return value;
|
|
675
|
+
}
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function parseStateSandboxInterface(
|
|
680
|
+
value: unknown,
|
|
681
|
+
): StateSandboxInterface | null {
|
|
682
|
+
if (value === "exec" || value === "files") {
|
|
683
|
+
return value;
|
|
684
|
+
}
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function parseStateSandboxLifecycle(
|
|
689
|
+
value: unknown,
|
|
690
|
+
): StateSandboxLifecycle | null {
|
|
691
|
+
if (value === "ready" || value === undefined || value === null) {
|
|
692
|
+
return "ready";
|
|
693
|
+
}
|
|
694
|
+
if (value === "resuming") {
|
|
695
|
+
return "resuming";
|
|
696
|
+
}
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
701
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function readString(
|
|
705
|
+
record: Record<string, unknown>,
|
|
706
|
+
key: string,
|
|
707
|
+
): string | null {
|
|
708
|
+
const value = record[key];
|
|
709
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
710
|
+
}
|