@yolk-sdk/sandbox 0.1.0-canary.18
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 +60 -0
- package/dist/agent.d.mts +48 -0
- package/dist/agent.d.mts.map +1 -0
- package/dist/agent.mjs +173 -0
- package/dist/agent.mjs.map +1 -0
- package/dist/errors.d.mts +44 -0
- package/dist/errors.d.mts.map +1 -0
- package/dist/errors.mjs +40 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +7 -0
- package/dist/lifecycle.d.mts +52 -0
- package/dist/lifecycle.d.mts.map +1 -0
- package/dist/lifecycle.mjs +118 -0
- package/dist/lifecycle.mjs.map +1 -0
- package/dist/model.d.mts +84 -0
- package/dist/model.d.mts.map +1 -0
- package/dist/model.mjs +72 -0
- package/dist/model.mjs.map +1 -0
- package/dist/name.d.mts +5 -0
- package/dist/name.d.mts.map +1 -0
- package/dist/name.mjs +19 -0
- package/dist/name.mjs.map +1 -0
- package/dist/service.d.mts +15 -0
- package/dist/service.d.mts.map +1 -0
- package/dist/service.mjs +7 -0
- package/dist/service.mjs.map +1 -0
- package/dist/state-store.d.mts +18 -0
- package/dist/state-store.d.mts.map +1 -0
- package/dist/state-store.mjs +7 -0
- package/dist/state-store.mjs.map +1 -0
- package/dist/testing/index.d.mts +21 -0
- package/dist/testing/index.d.mts.map +1 -0
- package/dist/testing/index.mjs +25 -0
- package/dist/testing/index.mjs.map +1 -0
- package/dist/vercel/client.d.mts +70 -0
- package/dist/vercel/client.d.mts.map +1 -0
- package/dist/vercel/client.mjs +113 -0
- package/dist/vercel/client.mjs.map +1 -0
- package/dist/vercel/index.d.mts +3 -0
- package/dist/vercel/index.mjs +3 -0
- package/dist/vercel/layer.d.mts +23 -0
- package/dist/vercel/layer.d.mts.map +1 -0
- package/dist/vercel/layer.mjs +250 -0
- package/dist/vercel/layer.mjs.map +1 -0
- package/package.json +72 -0
- package/src/agent.ts +304 -0
- package/src/errors.ts +71 -0
- package/src/index.ts +52 -0
- package/src/lifecycle.ts +186 -0
- package/src/model.ts +122 -0
- package/src/name.ts +23 -0
- package/src/service.ts +19 -0
- package/src/state-store.ts +17 -0
- package/src/testing/index.ts +63 -0
- package/src/vercel/client.ts +223 -0
- package/src/vercel/index.ts +24 -0
- package/src/vercel/layer.ts +396 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { SandboxProviderError, unknownToMessage } from "../errors.mjs";
|
|
2
|
+
import { SandboxCommandResult, SandboxPreviewUrl, SandboxResources } from "../model.mjs";
|
|
3
|
+
import { absoluteSandboxCwd, backgroundSandboxProbeMs, defaultSandboxPorts, initialSandboxState, normalizeWorkspaceCwd, sandboxCommandTimeoutMs, sandboxLifecycleOrDefault, sandboxSourceOrDefault, sandboxStateDecision, sandboxTimeoutExtendDeltaMs, touchSandboxState, validateSandboxCommand } from "../lifecycle.mjs";
|
|
4
|
+
import { Sandbox } from "../service.mjs";
|
|
5
|
+
import { makeVercelSandboxName } from "../name.mjs";
|
|
6
|
+
import { SandboxStateStore } from "../state-store.mjs";
|
|
7
|
+
import { VercelSandboxClient, VercelSandboxClientLive } from "./client.mjs";
|
|
8
|
+
import { Array, Clock, Duration, Effect, Layer, Option, Ref, Semaphore } from "effect";
|
|
9
|
+
//#region src/vercel/layer.ts
|
|
10
|
+
const defaultVercelResources = SandboxResources.make({ vcpus: 2 });
|
|
11
|
+
const defaultVercelRuntime = "node24";
|
|
12
|
+
const providerError = (operation, error) => new SandboxProviderError({
|
|
13
|
+
provider: "vercel",
|
|
14
|
+
operation,
|
|
15
|
+
message: `Vercel Sandbox ${operation} failed: ${unknownToMessage(error)}`,
|
|
16
|
+
underlying: error
|
|
17
|
+
});
|
|
18
|
+
const tryProvider = (operation, body) => Effect.tryPromise({
|
|
19
|
+
try: body,
|
|
20
|
+
catch: (error) => providerError(operation, error)
|
|
21
|
+
});
|
|
22
|
+
const lifecycleTimeoutMs = (lifecycle) => {
|
|
23
|
+
switch (lifecycle._tag) {
|
|
24
|
+
case "Disposable": return Math.min(lifecycle.idleTtlMs, lifecycle.maxLifetimeMs);
|
|
25
|
+
case "Persistent": return lifecycle.idleTtlMs;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const persistentFlag = (lifecycle) => lifecycle._tag === "Persistent";
|
|
29
|
+
const snapshotExpirationMs = (lifecycle) => lifecycle._tag === "Persistent" ? lifecycle.snapshotExpirationMs : void 0;
|
|
30
|
+
const keepLastSnapshots = (lifecycle) => lifecycle._tag === "Persistent" ? lifecycle.keepLastSnapshots : void 0;
|
|
31
|
+
const previewUrls = (handle, ports) => Effect.forEach(ports, (port) => Effect.try({
|
|
32
|
+
try: () => handle.domain(port),
|
|
33
|
+
catch: (error) => error
|
|
34
|
+
}).pipe(Effect.match({
|
|
35
|
+
onFailure: () => Option.none(),
|
|
36
|
+
onSuccess: (url) => Option.some(SandboxPreviewUrl.make({
|
|
37
|
+
port,
|
|
38
|
+
url
|
|
39
|
+
}))
|
|
40
|
+
}))).pipe(Effect.map(Array.getSomes));
|
|
41
|
+
const commandOutputOrEmpty = (command) => tryProvider("command.output", command.output).pipe(Effect.catchTag("SandboxProviderError", (error) => Effect.succeed({
|
|
42
|
+
stdout: "",
|
|
43
|
+
stderr: error.message
|
|
44
|
+
})));
|
|
45
|
+
const finishedCommandOutputOrEmpty = (command) => tryProvider("command.output", command.output).pipe(Effect.catchTag("SandboxProviderError", (error) => Effect.succeed({
|
|
46
|
+
stdout: "",
|
|
47
|
+
stderr: error.message
|
|
48
|
+
})));
|
|
49
|
+
const waitForegroundCommand = (command, timeoutMs) => tryProvider("command.wait", command.wait).pipe(Effect.flatMap((finished) => finishedCommandOutputOrEmpty(finished).pipe(Effect.map((output) => ({
|
|
50
|
+
exitCode: finished.exitCode,
|
|
51
|
+
stdout: output.stdout,
|
|
52
|
+
stderr: output.stderr,
|
|
53
|
+
timedOut: false
|
|
54
|
+
})))), Effect.timeoutOrElse({
|
|
55
|
+
duration: Duration.millis(timeoutMs),
|
|
56
|
+
orElse: () => Effect.gen(function* () {
|
|
57
|
+
yield* tryProvider("command.kill", () => command.kill("SIGKILL")).pipe(Effect.catchTag("SandboxProviderError", () => Effect.void));
|
|
58
|
+
const output = yield* commandOutputOrEmpty(command);
|
|
59
|
+
return {
|
|
60
|
+
exitCode: null,
|
|
61
|
+
stdout: output.stdout,
|
|
62
|
+
stderr: output.stderr,
|
|
63
|
+
timedOut: true
|
|
64
|
+
};
|
|
65
|
+
})
|
|
66
|
+
}));
|
|
67
|
+
const probeBackgroundCommand = (handle, command) => Effect.sleep(Duration.millis(backgroundSandboxProbeMs)).pipe(Effect.flatMap(() => tryProvider("command.get", () => handle.getCommand(command.id))), Effect.flatMap((current) => {
|
|
68
|
+
if (current.exitCode === null) return Effect.succeed({
|
|
69
|
+
exitCode: null,
|
|
70
|
+
stdout: "",
|
|
71
|
+
stderr: "",
|
|
72
|
+
timedOut: false,
|
|
73
|
+
backgroundId: command.id
|
|
74
|
+
});
|
|
75
|
+
return commandOutputOrEmpty(current).pipe(Effect.map((output) => ({
|
|
76
|
+
exitCode: current.exitCode,
|
|
77
|
+
stdout: output.stdout,
|
|
78
|
+
stderr: output.stderr,
|
|
79
|
+
timedOut: false
|
|
80
|
+
})));
|
|
81
|
+
}));
|
|
82
|
+
const createCommandFiles = (input) => {
|
|
83
|
+
const commandDir = `${input.workspaceRoot}/.yolk/commands/${input.commandId}`;
|
|
84
|
+
const commandPath = `${commandDir}/command.sh`;
|
|
85
|
+
const stdinPath = `${commandDir}/stdin.txt`;
|
|
86
|
+
const wrapperPath = `${commandDir}/wrapper.sh`;
|
|
87
|
+
return {
|
|
88
|
+
wrapperPath,
|
|
89
|
+
files: [
|
|
90
|
+
{
|
|
91
|
+
path: commandPath,
|
|
92
|
+
content: `#!/usr/bin/env bash\n${input.command}\n`,
|
|
93
|
+
mode: 493
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
path: stdinPath,
|
|
97
|
+
content: input.stdin ?? ""
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
path: wrapperPath,
|
|
101
|
+
content: `#!/usr/bin/env bash\nbash ${JSON.stringify(commandPath)} < ${JSON.stringify(stdinPath)}\n`,
|
|
102
|
+
mode: 493
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
const makeVercelSandboxLayerWithClient = (config) => Layer.effect(Sandbox, Effect.gen(function* () {
|
|
108
|
+
const store = yield* SandboxStateStore;
|
|
109
|
+
const client = yield* VercelSandboxClient;
|
|
110
|
+
const semaphore = yield* Semaphore.make(1);
|
|
111
|
+
const commandCounter = yield* Ref.make(0);
|
|
112
|
+
const lifecycle = sandboxLifecycleOrDefault(config.lifecycle);
|
|
113
|
+
const source = sandboxSourceOrDefault(config.source);
|
|
114
|
+
const ports = config.ports ?? defaultSandboxPorts;
|
|
115
|
+
const resources = config.resources ?? defaultVercelResources;
|
|
116
|
+
const runtime = config.runtime ?? defaultVercelRuntime;
|
|
117
|
+
const workspaceRoot = config.workspaceRoot ?? "/vercel/sandbox";
|
|
118
|
+
const name = makeVercelSandboxName(config.sandboxSessionId);
|
|
119
|
+
const deleteNamedSandbox = (sandboxName) => tryProvider("get", () => client.get({ name: sandboxName })).pipe(Effect.flatMap((handle) => handle === null ? Effect.void : tryProvider("delete", () => handle.delete())));
|
|
120
|
+
const createSandbox = (input) => Effect.gen(function* () {
|
|
121
|
+
const handle = yield* tryProvider("create", () => client.create({
|
|
122
|
+
name,
|
|
123
|
+
source,
|
|
124
|
+
ports,
|
|
125
|
+
timeoutMs: lifecycleTimeoutMs(lifecycle),
|
|
126
|
+
resources,
|
|
127
|
+
runtime,
|
|
128
|
+
env: config.env,
|
|
129
|
+
persistent: persistentFlag(lifecycle),
|
|
130
|
+
snapshotExpirationMs: snapshotExpirationMs(lifecycle),
|
|
131
|
+
keepLastSnapshots: keepLastSnapshots(lifecycle),
|
|
132
|
+
networkPolicy: config.networkPolicy
|
|
133
|
+
}));
|
|
134
|
+
const state = initialSandboxState({
|
|
135
|
+
name,
|
|
136
|
+
nowMs: input.nowMs,
|
|
137
|
+
lifecycle
|
|
138
|
+
});
|
|
139
|
+
yield* store.save({
|
|
140
|
+
sandboxSessionId: config.sandboxSessionId,
|
|
141
|
+
state
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
handle,
|
|
145
|
+
state,
|
|
146
|
+
workspaceReset: input.workspaceReset
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
const touchExistingSandbox = (input) => Effect.gen(function* () {
|
|
150
|
+
const touched = touchSandboxState({
|
|
151
|
+
state: input.state,
|
|
152
|
+
nowMs: input.nowMs,
|
|
153
|
+
lifecycle
|
|
154
|
+
});
|
|
155
|
+
const deltaMs = sandboxTimeoutExtendDeltaMs({
|
|
156
|
+
before: input.state,
|
|
157
|
+
after: touched
|
|
158
|
+
});
|
|
159
|
+
if (deltaMs > 0) yield* tryProvider("extendTimeout", () => input.handle.extendTimeout(deltaMs));
|
|
160
|
+
yield* store.save({
|
|
161
|
+
sandboxSessionId: config.sandboxSessionId,
|
|
162
|
+
state: touched
|
|
163
|
+
});
|
|
164
|
+
return {
|
|
165
|
+
handle: input.handle,
|
|
166
|
+
state: touched,
|
|
167
|
+
workspaceReset: input.workspaceReset
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
const ensureSandbox = (nowMs) => Effect.gen(function* () {
|
|
171
|
+
const loaded = yield* store.load(config.sandboxSessionId);
|
|
172
|
+
const decision = sandboxStateDecision({
|
|
173
|
+
state: loaded,
|
|
174
|
+
name,
|
|
175
|
+
nowMs
|
|
176
|
+
});
|
|
177
|
+
if (decision._tag === "Create") {
|
|
178
|
+
if (Option.isSome(loaded) && decision.workspaceReset) {
|
|
179
|
+
yield* deleteNamedSandbox(loaded.value.name);
|
|
180
|
+
yield* store.clear(config.sandboxSessionId);
|
|
181
|
+
}
|
|
182
|
+
return yield* createSandbox({
|
|
183
|
+
nowMs,
|
|
184
|
+
workspaceReset: decision.workspaceReset
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const handle = yield* tryProvider("get", () => client.get({ name }));
|
|
188
|
+
if (handle === null) {
|
|
189
|
+
yield* store.clear(config.sandboxSessionId);
|
|
190
|
+
return yield* createSandbox({
|
|
191
|
+
nowMs,
|
|
192
|
+
workspaceReset: true
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return yield* touchExistingSandbox({
|
|
196
|
+
handle,
|
|
197
|
+
state: decision.state,
|
|
198
|
+
nowMs,
|
|
199
|
+
workspaceReset: false
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
const nextCommandId = Ref.updateAndGet(commandCounter, (value) => value + 1).pipe(Effect.map((value) => `command-${value}`));
|
|
203
|
+
const runUnlocked = (input) => Effect.gen(function* () {
|
|
204
|
+
yield* validateSandboxCommand(input.command);
|
|
205
|
+
const normalizedCwd = yield* normalizeWorkspaceCwd(input.cwd);
|
|
206
|
+
const timeoutMs = sandboxCommandTimeoutMs(input.timeoutMs);
|
|
207
|
+
const active = yield* ensureSandbox(yield* Clock.currentTimeMillis);
|
|
208
|
+
const commandFiles = createCommandFiles({
|
|
209
|
+
commandId: yield* nextCommandId,
|
|
210
|
+
workspaceRoot,
|
|
211
|
+
command: input.command,
|
|
212
|
+
stdin: input.stdin
|
|
213
|
+
});
|
|
214
|
+
yield* tryProvider("writeFiles", () => active.handle.writeFiles(commandFiles.files));
|
|
215
|
+
const startedAtMs = yield* Clock.currentTimeMillis;
|
|
216
|
+
const command = yield* tryProvider("runCommand", () => active.handle.runDetachedCommand({
|
|
217
|
+
cmd: "bash",
|
|
218
|
+
args: [commandFiles.wrapperPath],
|
|
219
|
+
cwd: absoluteSandboxCwd(workspaceRoot, normalizedCwd)
|
|
220
|
+
}));
|
|
221
|
+
const output = yield* input.background === true ? probeBackgroundCommand(active.handle, command) : waitForegroundCommand(command, timeoutMs);
|
|
222
|
+
const endedAtMs = yield* Clock.currentTimeMillis;
|
|
223
|
+
const urls = yield* previewUrls(active.handle, ports);
|
|
224
|
+
return SandboxCommandResult.make({
|
|
225
|
+
exitCode: output.exitCode,
|
|
226
|
+
stdout: output.stdout,
|
|
227
|
+
stderr: output.stderr,
|
|
228
|
+
durationMs: Math.max(0, endedAtMs - startedAtMs),
|
|
229
|
+
timedOut: output.timedOut,
|
|
230
|
+
workspaceReset: active.workspaceReset,
|
|
231
|
+
...output.backgroundId === void 0 ? {} : { backgroundId: output.backgroundId },
|
|
232
|
+
previewUrls: urls,
|
|
233
|
+
state: active.state
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
return Sandbox.of({
|
|
237
|
+
run: (input) => semaphore.withPermits(1)(runUnlocked(input)),
|
|
238
|
+
currentState: store.load(config.sandboxSessionId),
|
|
239
|
+
delete: Effect.gen(function* () {
|
|
240
|
+
const loaded = yield* store.load(config.sandboxSessionId);
|
|
241
|
+
if (Option.isSome(loaded)) yield* deleteNamedSandbox(loaded.value.name);
|
|
242
|
+
yield* store.clear(config.sandboxSessionId);
|
|
243
|
+
})
|
|
244
|
+
});
|
|
245
|
+
}));
|
|
246
|
+
const makeVercelSandboxLayer = (config) => makeVercelSandboxLayerWithClient(config).pipe(Layer.provide(VercelSandboxClientLive));
|
|
247
|
+
//#endregion
|
|
248
|
+
export { makeVercelSandboxLayer, makeVercelSandboxLayerWithClient };
|
|
249
|
+
|
|
250
|
+
//# sourceMappingURL=layer.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layer.mjs","names":["Arr"],"sources":["../../src/vercel/layer.ts"],"sourcesContent":["import { Array as Arr, Clock, Duration, Effect, Layer, Option, Ref, Semaphore } from 'effect'\nimport {\n absoluteSandboxCwd,\n backgroundSandboxProbeMs,\n defaultSandboxPorts,\n defaultSandboxWorkspaceRoot,\n initialSandboxState,\n normalizeWorkspaceCwd,\n sandboxCommandTimeoutMs,\n sandboxLifecycleOrDefault,\n sandboxSourceOrDefault,\n sandboxStateDecision,\n sandboxTimeoutExtendDeltaMs,\n touchSandboxState,\n validateSandboxCommand\n} from '../lifecycle.ts'\nimport { makeVercelSandboxName } from '../name.ts'\nimport {\n SandboxCommandResult,\n type SandboxCommandInput,\n SandboxPreviewUrl,\n SandboxResources,\n type SandboxInitialSource,\n type SandboxLifecycle,\n type SandboxState\n} from '../model.ts'\nimport { Sandbox } from '../service.ts'\nimport { SandboxStateStore } from '../state-store.ts'\nimport { SandboxProviderError, unknownToMessage } from '../errors.ts'\nimport type { SandboxError } from '../errors.ts'\nimport {\n VercelSandboxClient,\n VercelSandboxClientLive,\n type VercelDetachedCommand,\n type VercelNetworkPolicy,\n type VercelSandboxHandle,\n type VercelSandboxRuntime\n} from './client.ts'\n\nexport type VercelSandboxLayerConfig = {\n readonly sandboxSessionId: string\n readonly lifecycle?: SandboxLifecycle\n readonly source?: SandboxInitialSource\n readonly env?: Readonly<Record<string, string>>\n readonly ports?: ReadonlyArray<number>\n readonly resources?: SandboxResources\n readonly runtime?: VercelSandboxRuntime\n readonly networkPolicy?: VercelNetworkPolicy\n readonly workspaceRoot?: string\n}\n\ntype ActiveSandbox = {\n readonly handle: VercelSandboxHandle\n readonly state: SandboxState\n readonly workspaceReset: boolean\n}\n\ntype CommandRunOutput = {\n readonly exitCode: number | null\n readonly stdout: string\n readonly stderr: string\n readonly timedOut: boolean\n readonly backgroundId?: string\n}\n\nconst defaultVercelResources = SandboxResources.make({ vcpus: 2 })\nconst defaultVercelRuntime: VercelSandboxRuntime = 'node24'\n\nconst providerError = (operation: string, error: unknown) =>\n new SandboxProviderError({\n provider: 'vercel',\n operation,\n message: `Vercel Sandbox ${operation} failed: ${unknownToMessage(error)}`,\n underlying: error\n })\n\nconst tryProvider = <A>(operation: string, body: () => Promise<A>) =>\n Effect.tryPromise({\n try: body,\n catch: error => providerError(operation, error)\n })\n\nconst lifecycleTimeoutMs = (lifecycle: SandboxLifecycle) => {\n switch (lifecycle._tag) {\n case 'Disposable':\n return Math.min(lifecycle.idleTtlMs, lifecycle.maxLifetimeMs)\n case 'Persistent':\n return lifecycle.idleTtlMs\n }\n}\n\nconst persistentFlag = (lifecycle: SandboxLifecycle) => lifecycle._tag === 'Persistent'\n\nconst snapshotExpirationMs = (lifecycle: SandboxLifecycle) =>\n lifecycle._tag === 'Persistent' ? lifecycle.snapshotExpirationMs : undefined\n\nconst keepLastSnapshots = (lifecycle: SandboxLifecycle) =>\n lifecycle._tag === 'Persistent' ? lifecycle.keepLastSnapshots : undefined\n\nconst previewUrls = (handle: VercelSandboxHandle, ports: ReadonlyArray<number>) =>\n Effect.forEach(ports, port =>\n Effect.try({\n try: () => handle.domain(port),\n catch: error => error\n }).pipe(\n Effect.match({\n onFailure: () => Option.none<SandboxPreviewUrl>(),\n onSuccess: url => Option.some(SandboxPreviewUrl.make({ port, url }))\n })\n )\n ).pipe(Effect.map(Arr.getSomes))\n\nconst commandOutputOrEmpty = (command: VercelDetachedCommand) =>\n tryProvider('command.output', command.output).pipe(\n Effect.catchTag('SandboxProviderError', error =>\n Effect.succeed({\n stdout: '',\n stderr: error.message\n })\n )\n )\n\nconst finishedCommandOutputOrEmpty = (command: {\n readonly output: () => Promise<{ readonly stdout: string; readonly stderr: string }>\n}) =>\n tryProvider('command.output', command.output).pipe(\n Effect.catchTag('SandboxProviderError', error =>\n Effect.succeed({\n stdout: '',\n stderr: error.message\n })\n )\n )\n\nconst waitForegroundCommand = (\n command: VercelDetachedCommand,\n timeoutMs: number\n): Effect.Effect<CommandRunOutput, SandboxProviderError> =>\n tryProvider('command.wait', command.wait).pipe(\n Effect.flatMap(finished =>\n finishedCommandOutputOrEmpty(finished).pipe(\n Effect.map(output => ({\n exitCode: finished.exitCode,\n stdout: output.stdout,\n stderr: output.stderr,\n timedOut: false\n }))\n )\n ),\n Effect.timeoutOrElse({\n duration: Duration.millis(timeoutMs),\n orElse: () =>\n Effect.gen(function* () {\n yield* tryProvider('command.kill', () => command.kill('SIGKILL')).pipe(\n Effect.catchTag('SandboxProviderError', () => Effect.void)\n )\n const output = yield* commandOutputOrEmpty(command)\n\n return {\n exitCode: null,\n stdout: output.stdout,\n stderr: output.stderr,\n timedOut: true\n }\n })\n })\n )\n\nconst probeBackgroundCommand = (\n handle: VercelSandboxHandle,\n command: VercelDetachedCommand\n): Effect.Effect<CommandRunOutput, SandboxProviderError> =>\n Effect.sleep(Duration.millis(backgroundSandboxProbeMs)).pipe(\n Effect.flatMap(() => tryProvider('command.get', () => handle.getCommand(command.id))),\n Effect.flatMap(current => {\n if (current.exitCode === null) {\n return Effect.succeed({\n exitCode: null,\n stdout: '',\n stderr: '',\n timedOut: false,\n backgroundId: command.id\n })\n }\n\n return commandOutputOrEmpty(current).pipe(\n Effect.map(output => ({\n exitCode: current.exitCode,\n stdout: output.stdout,\n stderr: output.stderr,\n timedOut: false\n }))\n )\n })\n )\n\nconst createCommandFiles = (input: {\n readonly commandId: string\n readonly workspaceRoot: string\n readonly command: string\n readonly stdin?: string\n}) => {\n const commandDir = `${input.workspaceRoot}/.yolk/commands/${input.commandId}`\n const commandPath = `${commandDir}/command.sh`\n const stdinPath = `${commandDir}/stdin.txt`\n const wrapperPath = `${commandDir}/wrapper.sh`\n\n return {\n wrapperPath,\n files: [\n {\n path: commandPath,\n content: `#!/usr/bin/env bash\\n${input.command}\\n`,\n mode: 0o755\n },\n {\n path: stdinPath,\n content: input.stdin ?? ''\n },\n {\n path: wrapperPath,\n content: `#!/usr/bin/env bash\\nbash ${JSON.stringify(commandPath)} < ${JSON.stringify(stdinPath)}\\n`,\n mode: 0o755\n }\n ]\n }\n}\n\nexport const makeVercelSandboxLayerWithClient = (config: VercelSandboxLayerConfig) =>\n Layer.effect(\n Sandbox,\n Effect.gen(function* () {\n const store = yield* SandboxStateStore\n const client = yield* VercelSandboxClient\n const semaphore = yield* Semaphore.make(1)\n const commandCounter = yield* Ref.make(0)\n const lifecycle = sandboxLifecycleOrDefault(config.lifecycle)\n const source = sandboxSourceOrDefault(config.source)\n const ports = config.ports ?? defaultSandboxPorts\n const resources = config.resources ?? defaultVercelResources\n const runtime = config.runtime ?? defaultVercelRuntime\n const workspaceRoot = config.workspaceRoot ?? defaultSandboxWorkspaceRoot\n const name = makeVercelSandboxName(config.sandboxSessionId)\n\n const deleteNamedSandbox = (sandboxName: string) =>\n tryProvider('get', () => client.get({ name: sandboxName })).pipe(\n Effect.flatMap(handle =>\n handle === null\n ? Effect.void\n : tryProvider('delete', () => handle.delete())\n )\n )\n\n const createSandbox = (input: { readonly nowMs: number; readonly workspaceReset: boolean }) =>\n Effect.gen(function* () {\n const handle = yield* tryProvider('create', () =>\n client.create({\n name,\n source,\n ports,\n timeoutMs: lifecycleTimeoutMs(lifecycle),\n resources,\n runtime,\n env: config.env,\n persistent: persistentFlag(lifecycle),\n snapshotExpirationMs: snapshotExpirationMs(lifecycle),\n keepLastSnapshots: keepLastSnapshots(lifecycle),\n networkPolicy: config.networkPolicy\n })\n )\n const state = initialSandboxState({ name, nowMs: input.nowMs, lifecycle })\n yield* store.save({ sandboxSessionId: config.sandboxSessionId, state })\n\n return { handle, state, workspaceReset: input.workspaceReset }\n })\n\n const touchExistingSandbox = (input: {\n readonly handle: VercelSandboxHandle\n readonly state: SandboxState\n readonly nowMs: number\n readonly workspaceReset: boolean\n }) =>\n Effect.gen(function* () {\n const touched = touchSandboxState({\n state: input.state,\n nowMs: input.nowMs,\n lifecycle\n })\n const deltaMs = sandboxTimeoutExtendDeltaMs({ before: input.state, after: touched })\n\n if (deltaMs > 0) {\n yield* tryProvider('extendTimeout', () => input.handle.extendTimeout(deltaMs))\n }\n\n yield* store.save({ sandboxSessionId: config.sandboxSessionId, state: touched })\n\n return {\n handle: input.handle,\n state: touched,\n workspaceReset: input.workspaceReset\n }\n })\n\n const ensureSandbox = (nowMs: number): Effect.Effect<ActiveSandbox, SandboxError> =>\n Effect.gen(function* () {\n const loaded = yield* store.load(config.sandboxSessionId)\n const decision = sandboxStateDecision({ state: loaded, name, nowMs })\n\n if (decision._tag === 'Create') {\n if (Option.isSome(loaded) && decision.workspaceReset) {\n yield* deleteNamedSandbox(loaded.value.name)\n yield* store.clear(config.sandboxSessionId)\n }\n\n return yield* createSandbox({ nowMs, workspaceReset: decision.workspaceReset })\n }\n\n const handle = yield* tryProvider('get', () => client.get({ name }))\n\n if (handle === null) {\n yield* store.clear(config.sandboxSessionId)\n return yield* createSandbox({ nowMs, workspaceReset: true })\n }\n\n return yield* touchExistingSandbox({\n handle,\n state: decision.state,\n nowMs,\n workspaceReset: false\n })\n })\n\n const nextCommandId = Ref.updateAndGet(commandCounter, value => value + 1).pipe(\n Effect.map(value => `command-${value}`)\n )\n\n const runUnlocked = (input: SandboxCommandInput) =>\n Effect.gen(function* () {\n yield* validateSandboxCommand(input.command)\n const normalizedCwd = yield* normalizeWorkspaceCwd(input.cwd)\n const timeoutMs = sandboxCommandTimeoutMs(input.timeoutMs)\n const nowMs = yield* Clock.currentTimeMillis\n const active = yield* ensureSandbox(nowMs)\n const id = yield* nextCommandId\n const commandFiles = createCommandFiles({\n commandId: id,\n workspaceRoot,\n command: input.command,\n stdin: input.stdin\n })\n\n yield* tryProvider('writeFiles', () => active.handle.writeFiles(commandFiles.files))\n\n const startedAtMs = yield* Clock.currentTimeMillis\n const command = yield* tryProvider('runCommand', () =>\n active.handle.runDetachedCommand({\n cmd: 'bash',\n args: [commandFiles.wrapperPath],\n cwd: absoluteSandboxCwd(workspaceRoot, normalizedCwd)\n })\n )\n const output = yield* (input.background === true\n ? probeBackgroundCommand(active.handle, command)\n : waitForegroundCommand(command, timeoutMs))\n const endedAtMs = yield* Clock.currentTimeMillis\n const urls = yield* previewUrls(active.handle, ports)\n\n return SandboxCommandResult.make({\n exitCode: output.exitCode,\n stdout: output.stdout,\n stderr: output.stderr,\n durationMs: Math.max(0, endedAtMs - startedAtMs),\n timedOut: output.timedOut,\n workspaceReset: active.workspaceReset,\n ...(output.backgroundId === undefined ? {} : { backgroundId: output.backgroundId }),\n previewUrls: urls,\n state: active.state\n })\n })\n\n return Sandbox.of({\n run: input => semaphore.withPermits(1)(runUnlocked(input)),\n currentState: store.load(config.sandboxSessionId),\n delete: Effect.gen(function* () {\n const loaded = yield* store.load(config.sandboxSessionId)\n if (Option.isSome(loaded)) {\n yield* deleteNamedSandbox(loaded.value.name)\n }\n yield* store.clear(config.sandboxSessionId)\n })\n })\n })\n )\n\nexport const makeVercelSandboxLayer = (config: VercelSandboxLayerConfig) =>\n makeVercelSandboxLayerWithClient(config).pipe(Layer.provide(VercelSandboxClientLive))\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yolk-sdk/sandbox",
|
|
3
|
+
"version": "0.1.0-canary.18",
|
|
4
|
+
"description": "Effect-native sandbox execution plane primitives and Vercel Sandbox adapter for Yolk agents.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/magoz/yolk-sdk.git",
|
|
11
|
+
"directory": "packages/sandbox"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/magoz/yolk-sdk/issues"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/magoz/yolk-sdk#readme",
|
|
17
|
+
"keywords": [
|
|
18
|
+
"sandbox",
|
|
19
|
+
"agents",
|
|
20
|
+
"execution",
|
|
21
|
+
"vercel",
|
|
22
|
+
"effect"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=22"
|
|
26
|
+
},
|
|
27
|
+
"exports": {
|
|
28
|
+
"./package.json": "./package.json",
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.mts",
|
|
31
|
+
"import": "./dist/index.mjs",
|
|
32
|
+
"default": "./dist/index.mjs"
|
|
33
|
+
},
|
|
34
|
+
"./agent": {
|
|
35
|
+
"types": "./dist/agent.d.mts",
|
|
36
|
+
"import": "./dist/agent.mjs",
|
|
37
|
+
"default": "./dist/agent.mjs"
|
|
38
|
+
},
|
|
39
|
+
"./testing": {
|
|
40
|
+
"types": "./dist/testing/index.d.mts",
|
|
41
|
+
"import": "./dist/testing/index.mjs",
|
|
42
|
+
"default": "./dist/testing/index.mjs"
|
|
43
|
+
},
|
|
44
|
+
"./vercel": {
|
|
45
|
+
"types": "./dist/vercel/index.d.mts",
|
|
46
|
+
"import": "./dist/vercel/index.mjs",
|
|
47
|
+
"default": "./dist/vercel/index.mjs"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"files": [
|
|
51
|
+
"src/**/*.ts",
|
|
52
|
+
"!src/**/*.test.ts",
|
|
53
|
+
"!src/**/*.test.tsx",
|
|
54
|
+
"dist/**/*",
|
|
55
|
+
"README.md"
|
|
56
|
+
],
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public",
|
|
59
|
+
"provenance": true
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"@vercel/sandbox": "2.2.1",
|
|
63
|
+
"effect": "4.0.0-beta.65",
|
|
64
|
+
"@yolk-sdk/agent": "^0.1.0-canary.18"
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"build": "tsdown",
|
|
68
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
69
|
+
"test": "vitest run --passWithNoTests",
|
|
70
|
+
"test:run": "vitest run --passWithNoTests"
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/agent.ts
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { Effect } from 'effect'
|
|
2
|
+
import * as Schema from 'effect/Schema'
|
|
3
|
+
import { ToolError } from '@yolk-sdk/agent/loop'
|
|
4
|
+
import { ToolResult, type ToolCall } from '@yolk-sdk/agent/protocol'
|
|
5
|
+
import {
|
|
6
|
+
makeTool,
|
|
7
|
+
modelVisibleToolError,
|
|
8
|
+
type ModelVisibleToolError,
|
|
9
|
+
type ToolModule,
|
|
10
|
+
type ToolRegistryError
|
|
11
|
+
} from '@yolk-sdk/agent/tools'
|
|
12
|
+
import {
|
|
13
|
+
maxSandboxCommandTimeoutMs,
|
|
14
|
+
normalizeWorkspaceCwd,
|
|
15
|
+
sandboxCommandTimeoutMs,
|
|
16
|
+
sandboxToolOutputLimit,
|
|
17
|
+
validateSandboxCommand
|
|
18
|
+
} from './lifecycle.ts'
|
|
19
|
+
import type { SandboxError, SandboxExpiredError, SandboxInputError } from './errors.ts'
|
|
20
|
+
import type { SandboxApi } from './service.ts'
|
|
21
|
+
import { Sandbox } from './service.ts'
|
|
22
|
+
import type { SandboxCommandResult } from './model.ts'
|
|
23
|
+
|
|
24
|
+
export const sandboxToolName = 'sandbox'
|
|
25
|
+
|
|
26
|
+
const SandboxToolParams = Schema.Struct({
|
|
27
|
+
command: Schema.String.pipe(
|
|
28
|
+
Schema.annotate({ description: 'Shell command to run. Multiline commands are allowed.' })
|
|
29
|
+
),
|
|
30
|
+
cwd: Schema.optional(
|
|
31
|
+
Schema.NullOr(
|
|
32
|
+
Schema.String.pipe(
|
|
33
|
+
Schema.annotate({ description: 'Workspace-relative working directory. Absolute paths are rejected.' })
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
),
|
|
37
|
+
stdin: Schema.optional(
|
|
38
|
+
Schema.NullOr(
|
|
39
|
+
Schema.String.pipe(
|
|
40
|
+
Schema.annotate({ description: 'Optional stdin text passed to the command.' })
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
),
|
|
44
|
+
timeoutSeconds: Schema.optional(
|
|
45
|
+
Schema.NullOr(
|
|
46
|
+
Schema.Number.pipe(
|
|
47
|
+
Schema.annotate({ description: 'Foreground timeout in seconds. Default 120, max 600.' })
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
),
|
|
51
|
+
background: Schema.optional(
|
|
52
|
+
Schema.NullOr(
|
|
53
|
+
Schema.Boolean.pipe(
|
|
54
|
+
Schema.annotate({ description: 'Start command in background and return after a quick probe.' })
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
export type SandboxToolParams = typeof SandboxToolParams.Type
|
|
61
|
+
|
|
62
|
+
export type SandboxToolModuleOptions<Context> = {
|
|
63
|
+
readonly capabilities?: ReadonlyArray<string>
|
|
64
|
+
readonly workspaceDescription?: string
|
|
65
|
+
readonly lifecycleDescription?: string
|
|
66
|
+
readonly previewPorts?: ReadonlyArray<number>
|
|
67
|
+
readonly isEnabled?: (context: Context) => Effect.Effect<boolean, ToolRegistryError>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type SandboxToolStructuredContent = {
|
|
71
|
+
readonly exitCode: number | null
|
|
72
|
+
readonly durationMs: number
|
|
73
|
+
readonly timedOut: boolean
|
|
74
|
+
readonly truncated: boolean
|
|
75
|
+
readonly workspaceReset: boolean
|
|
76
|
+
readonly backgroundId?: string
|
|
77
|
+
readonly previewUrls: SandboxCommandResult['previewUrls']
|
|
78
|
+
readonly state: SandboxCommandResult['state']
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type OutputSlice = {
|
|
82
|
+
readonly stdout: string
|
|
83
|
+
readonly stderr: string
|
|
84
|
+
readonly truncated: boolean
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const nullToUndefined = <A>(value: A | null | undefined) => value === null ? undefined : value
|
|
88
|
+
|
|
89
|
+
const toolError = (message: string, cause: ToolError['cause']) =>
|
|
90
|
+
new ToolError({
|
|
91
|
+
tool: sandboxToolName,
|
|
92
|
+
message,
|
|
93
|
+
cause
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const modelVisibleSandboxError = (
|
|
97
|
+
error: SandboxInputError | SandboxExpiredError
|
|
98
|
+
): ModelVisibleToolError => {
|
|
99
|
+
switch (error._tag) {
|
|
100
|
+
case 'SandboxInputError':
|
|
101
|
+
return modelVisibleToolError({
|
|
102
|
+
tool: sandboxToolName,
|
|
103
|
+
message: error.message,
|
|
104
|
+
reason: 'invalid_input'
|
|
105
|
+
})
|
|
106
|
+
case 'SandboxExpiredError':
|
|
107
|
+
return modelVisibleToolError({
|
|
108
|
+
tool: sandboxToolName,
|
|
109
|
+
message: error.message,
|
|
110
|
+
reason: 'unavailable',
|
|
111
|
+
details: { expiredAtMs: error.expiredAtMs }
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const sandboxInfraToolError = (error: Exclude<SandboxError, SandboxInputError | SandboxExpiredError>) => {
|
|
117
|
+
switch (error._tag) {
|
|
118
|
+
case 'SandboxConfigError':
|
|
119
|
+
return toolError(error.message, 'invalid_input')
|
|
120
|
+
case 'SandboxProviderError':
|
|
121
|
+
return toolError(error.message, 'execution')
|
|
122
|
+
case 'SandboxStateError':
|
|
123
|
+
case 'SandboxStateStoreError':
|
|
124
|
+
return toolError(error.message, 'unavailable')
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const normalizeTimeoutMs = (timeoutSeconds: number | null | undefined) => {
|
|
129
|
+
const value = nullToUndefined(timeoutSeconds)
|
|
130
|
+
|
|
131
|
+
if (value === undefined) {
|
|
132
|
+
return Effect.succeed(sandboxCommandTimeoutMs(undefined))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
136
|
+
return Effect.fail(
|
|
137
|
+
modelVisibleToolError({
|
|
138
|
+
tool: sandboxToolName,
|
|
139
|
+
message: 'timeoutSeconds must be greater than 0',
|
|
140
|
+
reason: 'invalid_input'
|
|
141
|
+
})
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return Effect.succeed(sandboxCommandTimeoutMs(Math.floor(value * 1_000)))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const normalizeToolParams = (params: SandboxToolParams) =>
|
|
149
|
+
Effect.gen(function* () {
|
|
150
|
+
const command = yield* validateSandboxCommand(params.command).pipe(
|
|
151
|
+
Effect.mapError(modelVisibleSandboxError)
|
|
152
|
+
)
|
|
153
|
+
const cwd = yield* normalizeWorkspaceCwd(params.cwd).pipe(
|
|
154
|
+
Effect.mapError(modelVisibleSandboxError)
|
|
155
|
+
)
|
|
156
|
+
const timeoutMs = yield* normalizeTimeoutMs(params.timeoutSeconds)
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
command,
|
|
160
|
+
cwd,
|
|
161
|
+
stdin: nullToUndefined(params.stdin),
|
|
162
|
+
timeoutMs,
|
|
163
|
+
background: nullToUndefined(params.background)
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const truncateOutputs = (stdout: string, stderr: string, limit: number): OutputSlice => {
|
|
168
|
+
if (stdout.length + stderr.length <= limit) {
|
|
169
|
+
return { stdout, stderr, truncated: false }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const initialStderrBudget = Math.min(stderr.length, Math.floor(limit / 2))
|
|
173
|
+
const initialStdoutBudget = Math.min(stdout.length, limit - initialStderrBudget)
|
|
174
|
+
const unused = limit - initialStdoutBudget - initialStderrBudget
|
|
175
|
+
const stdoutBudget = Math.min(stdout.length, initialStdoutBudget + unused)
|
|
176
|
+
const stderrBudget = Math.min(stderr.length, limit - stdoutBudget)
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
stdout: stdout.slice(0, stdoutBudget),
|
|
180
|
+
stderr: stderr.slice(0, stderrBudget),
|
|
181
|
+
truncated: true
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const formatSandboxToolContent = (result: SandboxCommandResult, output: OutputSlice) =>
|
|
186
|
+
[
|
|
187
|
+
`exit_code: ${result.exitCode === null ? 'null' : String(result.exitCode)}`,
|
|
188
|
+
`duration_ms: ${result.durationMs}`,
|
|
189
|
+
`truncated: ${output.truncated}`,
|
|
190
|
+
`timed_out: ${result.timedOut}`,
|
|
191
|
+
`workspace_reset: ${result.workspaceReset}`,
|
|
192
|
+
result.backgroundId === undefined ? undefined : `background_id: ${result.backgroundId}`,
|
|
193
|
+
'<stdout>',
|
|
194
|
+
output.stdout,
|
|
195
|
+
'</stdout>',
|
|
196
|
+
'<stderr>',
|
|
197
|
+
output.stderr,
|
|
198
|
+
'</stderr>'
|
|
199
|
+
].filter(line => line !== undefined).join('\n')
|
|
200
|
+
|
|
201
|
+
const structuredContent = (
|
|
202
|
+
result: SandboxCommandResult,
|
|
203
|
+
truncated: boolean
|
|
204
|
+
): SandboxToolStructuredContent => ({
|
|
205
|
+
exitCode: result.exitCode,
|
|
206
|
+
durationMs: result.durationMs,
|
|
207
|
+
timedOut: result.timedOut,
|
|
208
|
+
truncated,
|
|
209
|
+
workspaceReset: result.workspaceReset,
|
|
210
|
+
...(result.backgroundId === undefined ? {} : { backgroundId: result.backgroundId }),
|
|
211
|
+
previewUrls: result.previewUrls,
|
|
212
|
+
state: result.state
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
export const makeSandboxToolResult = (input: {
|
|
216
|
+
readonly callId: string
|
|
217
|
+
readonly result: SandboxCommandResult
|
|
218
|
+
}) => {
|
|
219
|
+
const output = truncateOutputs(input.result.stdout, input.result.stderr, sandboxToolOutputLimit)
|
|
220
|
+
|
|
221
|
+
return ToolResult.make({
|
|
222
|
+
toolCallId: input.callId,
|
|
223
|
+
content: formatSandboxToolContent(input.result, output),
|
|
224
|
+
isError: input.result.timedOut || (input.result.exitCode !== null && input.result.exitCode !== 0),
|
|
225
|
+
structuredContent: structuredContent(input.result, output.truncated)
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const sandboxToolDescription = <Context>(options: SandboxToolModuleOptions<Context>) => {
|
|
230
|
+
const capabilities = options.capabilities ?? [
|
|
231
|
+
'run project commands such as typechecks, lint, tests, and builds',
|
|
232
|
+
'edit files with shell tools such as apply_patch when available',
|
|
233
|
+
'start dev servers and use preview ports',
|
|
234
|
+
'run browser checks through agent-browser when available'
|
|
235
|
+
]
|
|
236
|
+
const ports = options.previewPorts ?? []
|
|
237
|
+
|
|
238
|
+
return [
|
|
239
|
+
'Run one non-interactive bash command inside a real sandbox workspace.',
|
|
240
|
+
options.workspaceDescription ?? 'The working directory is the sandbox workspace root.',
|
|
241
|
+
options.lifecycleDescription ?? 'The sandbox is disposable and may reset after idle or max lifetime expiry.',
|
|
242
|
+
`Foreground timeout defaults to 120s and is capped at ${Math.floor(maxSandboxCommandTimeoutMs / 1_000)}s.`,
|
|
243
|
+
'Use workspace-relative cwd only; absolute paths and workspace escapes are rejected.',
|
|
244
|
+
'Use stdin for large patches, scripts, or data.',
|
|
245
|
+
'Set background=true for long-running servers; the tool returns after a quick probe.',
|
|
246
|
+
ports.length === 0 ? 'Preview URLs are returned when configured by the host.' : `Preview ports: ${ports.join(', ')}.`,
|
|
247
|
+
`Available capabilities:\n${capabilities.map(capability => `- ${capability}`).join('\n')}`
|
|
248
|
+
].join('\n\n')
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export const makeSandboxToolModuleFromApi = <Context>(
|
|
252
|
+
sandbox: SandboxApi,
|
|
253
|
+
options: SandboxToolModuleOptions<Context> = {}
|
|
254
|
+
): ToolModule<Context> => ({
|
|
255
|
+
id: 'sandbox',
|
|
256
|
+
tools: [
|
|
257
|
+
makeTool({
|
|
258
|
+
name: sandboxToolName,
|
|
259
|
+
description: sandboxToolDescription(options),
|
|
260
|
+
parameters: SandboxToolParams,
|
|
261
|
+
access: 'destructive',
|
|
262
|
+
isEnabled: options.isEnabled,
|
|
263
|
+
invalidParamsMessage: error => `Invalid sandbox arguments: ${error instanceof Error ? error.message : String(error)}`,
|
|
264
|
+
execute: ({ call, params }) =>
|
|
265
|
+
Effect.gen(function* () {
|
|
266
|
+
if (call.name !== sandboxToolName) {
|
|
267
|
+
return yield* Effect.fail(toolError(`Tool is not configured: ${call.name}`, 'not_found'))
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const normalized = yield* normalizeToolParams(params)
|
|
271
|
+
const result = yield* sandbox.run(normalized).pipe(
|
|
272
|
+
Effect.mapError(error => {
|
|
273
|
+
switch (error._tag) {
|
|
274
|
+
case 'SandboxInputError':
|
|
275
|
+
case 'SandboxExpiredError':
|
|
276
|
+
return modelVisibleSandboxError(error)
|
|
277
|
+
case 'SandboxConfigError':
|
|
278
|
+
case 'SandboxProviderError':
|
|
279
|
+
case 'SandboxStateError':
|
|
280
|
+
case 'SandboxStateStoreError':
|
|
281
|
+
return sandboxInfraToolError(error)
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
return makeSandboxToolResult({ callId: call.id, result })
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
]
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
export const makeSandboxToolModule = <Context>(
|
|
293
|
+
options: SandboxToolModuleOptions<Context> = {}
|
|
294
|
+
): Effect.Effect<ToolModule<Context>, never, Sandbox> =>
|
|
295
|
+
Effect.gen(function* () {
|
|
296
|
+
const sandbox = yield* Sandbox
|
|
297
|
+
return makeSandboxToolModuleFromApi(sandbox, options)
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
export type SandboxToolExecutionInput<Context> = {
|
|
301
|
+
readonly call: ToolCall
|
|
302
|
+
readonly context: Context
|
|
303
|
+
readonly params: SandboxToolParams
|
|
304
|
+
}
|