@hasna/sandboxes 0.1.22 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +195 -2
- package/dist/index.js +168 -2
- package/dist/lib/agents/claude.d.ts +9 -0
- package/dist/lib/archive.d.ts +31 -0
- package/dist/lib/secrets.d.ts +26 -0
- package/dist/mcp/index.js +208 -3
- package/dist/providers/daytona.d.ts +2 -1
- package/dist/providers/e2b.d.ts +2 -1
- package/dist/providers/modal.d.ts +2 -1
- package/dist/providers/types.d.ts +3 -1
- package/dist/sdk.d.ts +7 -1
- package/dist/server/index.js +104 -0
- package/dist/types/index.d.ts +9 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -12059,6 +12059,50 @@ var init_config2 = __esm(() => {
|
|
|
12059
12059
|
};
|
|
12060
12060
|
});
|
|
12061
12061
|
|
|
12062
|
+
// src/lib/archive.ts
|
|
12063
|
+
import { existsSync as existsSync7, statSync } from "fs";
|
|
12064
|
+
function shellQuote(value) {
|
|
12065
|
+
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
12066
|
+
}
|
|
12067
|
+
async function tarDirectory(localDir, opts) {
|
|
12068
|
+
if (!existsSync7(localDir) || !statSync(localDir).isDirectory()) {
|
|
12069
|
+
throw new Error(`tarDirectory: not a directory: ${localDir}`);
|
|
12070
|
+
}
|
|
12071
|
+
const excludes = opts?.exclude ?? DEFAULT_UPLOAD_EXCLUDES;
|
|
12072
|
+
const args = ["-czf", "-"];
|
|
12073
|
+
for (const ex of excludes)
|
|
12074
|
+
args.push(`--exclude=${ex}`);
|
|
12075
|
+
args.push("-C", localDir, ".");
|
|
12076
|
+
const proc = Bun.spawn(["tar", ...args], { stdout: "pipe", stderr: "pipe" });
|
|
12077
|
+
const [buf, stderr, exitCode] = await Promise.all([
|
|
12078
|
+
new Response(proc.stdout).arrayBuffer(),
|
|
12079
|
+
new Response(proc.stderr).text(),
|
|
12080
|
+
proc.exited
|
|
12081
|
+
]);
|
|
12082
|
+
if (exitCode !== 0) {
|
|
12083
|
+
throw new Error(`tarDirectory: tar exited ${exitCode}: ${stderr.trim()}`);
|
|
12084
|
+
}
|
|
12085
|
+
return Buffer.from(buf);
|
|
12086
|
+
}
|
|
12087
|
+
function buildUntarCommand(remoteTarPath, remoteDir) {
|
|
12088
|
+
const tar = shellQuote(remoteTarPath);
|
|
12089
|
+
const dir = shellQuote(remoteDir);
|
|
12090
|
+
return `mkdir -p ${dir} && tar -xzf ${tar} -C ${dir} && rm -f ${tar}`;
|
|
12091
|
+
}
|
|
12092
|
+
var DEFAULT_UPLOAD_EXCLUDES;
|
|
12093
|
+
var init_archive = __esm(() => {
|
|
12094
|
+
DEFAULT_UPLOAD_EXCLUDES = [
|
|
12095
|
+
"node_modules",
|
|
12096
|
+
".git",
|
|
12097
|
+
"dist",
|
|
12098
|
+
".next",
|
|
12099
|
+
".turbo",
|
|
12100
|
+
".cache",
|
|
12101
|
+
".venv",
|
|
12102
|
+
"__pycache__"
|
|
12103
|
+
];
|
|
12104
|
+
});
|
|
12105
|
+
|
|
12062
12106
|
// src/providers/e2b.ts
|
|
12063
12107
|
var exports_e2b = {};
|
|
12064
12108
|
__export(exports_e2b, {
|
|
@@ -12207,6 +12251,22 @@ class E2BProvider {
|
|
|
12207
12251
|
throw new ProviderError("e2b", `Failed to list files at ${path}: ${err.message}`);
|
|
12208
12252
|
}
|
|
12209
12253
|
}
|
|
12254
|
+
async uploadDir(sandboxId, localDir, remoteDir, opts) {
|
|
12255
|
+
const sandbox = await this.getInstance(sandboxId);
|
|
12256
|
+
try {
|
|
12257
|
+
const archive = await tarDirectory(localDir, opts);
|
|
12258
|
+
const remoteTar = `/tmp/sandboxes-upload-${Date.now()}.tar.gz`;
|
|
12259
|
+
const data = archive.buffer.slice(archive.byteOffset, archive.byteOffset + archive.byteLength);
|
|
12260
|
+
await sandbox.files.write(remoteTar, data);
|
|
12261
|
+
const result = await sandbox.commands.run(buildUntarCommand(remoteTar, remoteDir));
|
|
12262
|
+
if ((result.exitCode ?? 0) !== 0) {
|
|
12263
|
+
throw new Error(result.stderr || `untar exited with code ${result.exitCode}`);
|
|
12264
|
+
}
|
|
12265
|
+
return { bytes: archive.length };
|
|
12266
|
+
} catch (err) {
|
|
12267
|
+
throw new ProviderError("e2b", `Failed to upload directory to ${remoteDir}: ${err.message}`);
|
|
12268
|
+
}
|
|
12269
|
+
}
|
|
12210
12270
|
async stop(sandboxId) {
|
|
12211
12271
|
const sandbox = await this.getInstance(sandboxId);
|
|
12212
12272
|
try {
|
|
@@ -12257,6 +12317,7 @@ class E2BProvider {
|
|
|
12257
12317
|
var instanceCache;
|
|
12258
12318
|
var init_e2b = __esm(() => {
|
|
12259
12319
|
init_types2();
|
|
12320
|
+
init_archive();
|
|
12260
12321
|
instanceCache = new Map;
|
|
12261
12322
|
});
|
|
12262
12323
|
|
|
@@ -12387,6 +12448,21 @@ class DaytonaProvider {
|
|
|
12387
12448
|
throw new ProviderError("daytona", `Failed to list files at ${path}: ${err.message}`);
|
|
12388
12449
|
}
|
|
12389
12450
|
}
|
|
12451
|
+
async uploadDir(sandboxId, localDir, remoteDir, opts) {
|
|
12452
|
+
const sandbox = await this.getInstance(sandboxId);
|
|
12453
|
+
try {
|
|
12454
|
+
const archive = await tarDirectory(localDir, opts);
|
|
12455
|
+
const remoteTar = `/tmp/sandboxes-upload-${Date.now()}.tar.gz`;
|
|
12456
|
+
await sandbox.fs.uploadFile(archive, remoteTar);
|
|
12457
|
+
const result = await sandbox.process.executeCommand(buildUntarCommand(remoteTar, remoteDir));
|
|
12458
|
+
if (result.exitCode !== 0) {
|
|
12459
|
+
throw new Error(result.result || `untar exited with code ${result.exitCode}`);
|
|
12460
|
+
}
|
|
12461
|
+
return { bytes: archive.length };
|
|
12462
|
+
} catch (err) {
|
|
12463
|
+
throw new ProviderError("daytona", `Failed to upload directory to ${remoteDir}: ${err.message}`);
|
|
12464
|
+
}
|
|
12465
|
+
}
|
|
12390
12466
|
async stop(sandboxId) {
|
|
12391
12467
|
const sandbox = await this.getInstance(sandboxId);
|
|
12392
12468
|
try {
|
|
@@ -12427,6 +12503,7 @@ class DaytonaProvider {
|
|
|
12427
12503
|
var instanceCache2;
|
|
12428
12504
|
var init_daytona = __esm(() => {
|
|
12429
12505
|
init_types2();
|
|
12506
|
+
init_archive();
|
|
12430
12507
|
instanceCache2 = new Map;
|
|
12431
12508
|
});
|
|
12432
12509
|
|
|
@@ -12624,6 +12701,32 @@ class ModalProvider {
|
|
|
12624
12701
|
throw new ProviderError("modal", `Failed to list files at ${path}: ${err.message}`);
|
|
12625
12702
|
}
|
|
12626
12703
|
}
|
|
12704
|
+
async uploadDir(sandboxId, localDir, remoteDir, opts) {
|
|
12705
|
+
try {
|
|
12706
|
+
const archive = await tarDirectory(localDir, opts);
|
|
12707
|
+
const b64 = archive.toString("base64");
|
|
12708
|
+
const remoteTar = `/tmp/sandboxes-upload-${Date.now()}.tar.gz`;
|
|
12709
|
+
const remoteB64 = `${remoteTar}.b64`;
|
|
12710
|
+
const execChecked = async (cmd) => {
|
|
12711
|
+
const result = await this.exec(sandboxId, `sh -c ${this.shellEscape(cmd)}`);
|
|
12712
|
+
if (result.exit_code !== 0) {
|
|
12713
|
+
throw new Error(result.stderr || `command exited with code ${result.exit_code}`);
|
|
12714
|
+
}
|
|
12715
|
+
};
|
|
12716
|
+
await execChecked(`: > ${remoteB64}`);
|
|
12717
|
+
const CHUNK = 60000;
|
|
12718
|
+
for (let i = 0;i < b64.length; i += CHUNK) {
|
|
12719
|
+
const chunk = b64.slice(i, i + CHUNK);
|
|
12720
|
+
await execChecked(`printf '%s' ${this.shellEscape(chunk)} >> ${remoteB64}`);
|
|
12721
|
+
}
|
|
12722
|
+
await execChecked(`base64 -d ${remoteB64} > ${remoteTar} && ${buildUntarCommand(remoteTar, remoteDir)} && rm -f ${remoteB64}`);
|
|
12723
|
+
return { bytes: archive.length };
|
|
12724
|
+
} catch (err) {
|
|
12725
|
+
if (err instanceof ProviderError)
|
|
12726
|
+
throw err;
|
|
12727
|
+
throw new ProviderError("modal", `Failed to upload directory to ${remoteDir}: ${err.message}`);
|
|
12728
|
+
}
|
|
12729
|
+
}
|
|
12627
12730
|
async stop(sandboxId) {
|
|
12628
12731
|
const sandbox = this.getSandbox(sandboxId);
|
|
12629
12732
|
try {
|
|
@@ -12690,6 +12793,7 @@ class ModalProvider {
|
|
|
12690
12793
|
var sandboxCache;
|
|
12691
12794
|
var init_modal = __esm(() => {
|
|
12692
12795
|
init_types2();
|
|
12796
|
+
init_archive();
|
|
12693
12797
|
sandboxCache = new Map;
|
|
12694
12798
|
});
|
|
12695
12799
|
|
|
@@ -12809,6 +12913,34 @@ var init_runtime_state = __esm(() => {
|
|
|
12809
12913
|
init_sandboxes();
|
|
12810
12914
|
});
|
|
12811
12915
|
|
|
12916
|
+
// src/lib/agents/claude.ts
|
|
12917
|
+
class ClaudeDriver {
|
|
12918
|
+
name = "claude";
|
|
12919
|
+
requiredEnvVars = ["ANTHROPIC_API_KEY"];
|
|
12920
|
+
async install(provider, providerSandboxId) {
|
|
12921
|
+
const check = await provider.exec(providerSandboxId, "which claude 2>/dev/null || echo MISSING");
|
|
12922
|
+
if (check.stdout.trim() !== "MISSING")
|
|
12923
|
+
return;
|
|
12924
|
+
const bunCheck = await provider.exec(providerSandboxId, "which bun 2>/dev/null || echo MISSING");
|
|
12925
|
+
if (bunCheck.stdout.trim() !== "MISSING") {
|
|
12926
|
+
await provider.exec(providerSandboxId, "bun install -g @anthropic-ai/claude-code 2>&1");
|
|
12927
|
+
} else {
|
|
12928
|
+
await provider.exec(providerSandboxId, "npm install -g @anthropic-ai/claude-code 2>&1 || sudo npm install -g @anthropic-ai/claude-code 2>&1");
|
|
12929
|
+
}
|
|
12930
|
+
}
|
|
12931
|
+
async configure(provider, providerSandboxId, _envVars) {
|
|
12932
|
+
const config = JSON.stringify({
|
|
12933
|
+
hasCompletedOnboarding: true,
|
|
12934
|
+
hasTrustDialogAccepted: true,
|
|
12935
|
+
bypassPermissionsModeAccepted: true
|
|
12936
|
+
});
|
|
12937
|
+
await provider.exec(providerSandboxId, `mkdir -p ~/.claude && echo '${config}' > ~/.claude.json`);
|
|
12938
|
+
}
|
|
12939
|
+
buildCommand(prompt) {
|
|
12940
|
+
return `claude --dangerously-skip-permissions -p ${JSON.stringify(prompt)}`;
|
|
12941
|
+
}
|
|
12942
|
+
}
|
|
12943
|
+
|
|
12812
12944
|
// src/lib/agents/codex.ts
|
|
12813
12945
|
class CodexDriver {
|
|
12814
12946
|
name = "codex";
|
|
@@ -12921,6 +13053,7 @@ function getAgentDriver(name) {
|
|
|
12921
13053
|
var DRIVERS, DRIVER_MAP;
|
|
12922
13054
|
var init_agents = __esm(() => {
|
|
12923
13055
|
DRIVERS = [
|
|
13056
|
+
new ClaudeDriver,
|
|
12924
13057
|
new CodexDriver,
|
|
12925
13058
|
new GeminiDriver,
|
|
12926
13059
|
new OpenCodeDriver,
|
|
@@ -13034,7 +13167,7 @@ async function stopAgent(sandboxId) {
|
|
|
13034
13167
|
return;
|
|
13035
13168
|
const provider = await getProvider(sandbox.provider);
|
|
13036
13169
|
try {
|
|
13037
|
-
await provider.exec(sandbox.provider_sandbox_id, "pkill -f 'codex\\|gemini\\|opencode\\|pi\\|takumi' || true");
|
|
13170
|
+
await provider.exec(sandbox.provider_sandbox_id, "pkill -f 'claude\\|codex\\|gemini\\|opencode\\|pi\\|takumi' || true");
|
|
13038
13171
|
} catch {}
|
|
13039
13172
|
emitLifecycleEvent(sandbox.id, "Agent stopped by user");
|
|
13040
13173
|
}
|
|
@@ -13047,6 +13180,49 @@ var init_agent_runner = __esm(() => {
|
|
|
13047
13180
|
init_runtime_state();
|
|
13048
13181
|
});
|
|
13049
13182
|
|
|
13183
|
+
// src/lib/secrets.ts
|
|
13184
|
+
var exports_secrets = {};
|
|
13185
|
+
__export(exports_secrets, {
|
|
13186
|
+
resolveSecretSpecs: () => resolveSecretSpecs,
|
|
13187
|
+
resolveSecretEnv: () => resolveSecretEnv,
|
|
13188
|
+
parseSecretMapping: () => parseSecretMapping,
|
|
13189
|
+
cliSecretResolver: () => cliSecretResolver
|
|
13190
|
+
});
|
|
13191
|
+
function parseSecretMapping(spec) {
|
|
13192
|
+
const idx = spec.indexOf("=");
|
|
13193
|
+
if (idx <= 0) {
|
|
13194
|
+
throw new Error(`Invalid secret mapping "${spec}" (expected ENV_NAME=vault/key)`);
|
|
13195
|
+
}
|
|
13196
|
+
const env = spec.slice(0, idx).trim();
|
|
13197
|
+
const key = spec.slice(idx + 1).trim();
|
|
13198
|
+
if (!env || !key) {
|
|
13199
|
+
throw new Error(`Invalid secret mapping "${spec}" (expected ENV_NAME=vault/key)`);
|
|
13200
|
+
}
|
|
13201
|
+
return { env, key };
|
|
13202
|
+
}
|
|
13203
|
+
async function resolveSecretEnv(mappings, resolver = cliSecretResolver) {
|
|
13204
|
+
const env = {};
|
|
13205
|
+
for (const mapping of mappings) {
|
|
13206
|
+
env[mapping.env] = await resolver(mapping.key);
|
|
13207
|
+
}
|
|
13208
|
+
return env;
|
|
13209
|
+
}
|
|
13210
|
+
async function resolveSecretSpecs(specs, resolver = cliSecretResolver) {
|
|
13211
|
+
return resolveSecretEnv(specs.map(parseSecretMapping), resolver);
|
|
13212
|
+
}
|
|
13213
|
+
var cliSecretResolver = async (key) => {
|
|
13214
|
+
const proc = Bun.spawn(["secrets", "get", key], { stdout: "pipe", stderr: "pipe" });
|
|
13215
|
+
const [out, errText, code] = await Promise.all([
|
|
13216
|
+
new Response(proc.stdout).text(),
|
|
13217
|
+
new Response(proc.stderr).text(),
|
|
13218
|
+
proc.exited
|
|
13219
|
+
]);
|
|
13220
|
+
if (code !== 0) {
|
|
13221
|
+
throw new Error(`secrets get ${key} failed: ${errText.trim() || `exit ${code}`}`);
|
|
13222
|
+
}
|
|
13223
|
+
return out.replace(/\r?\n$/, "");
|
|
13224
|
+
};
|
|
13225
|
+
|
|
13050
13226
|
// node_modules/commander/esm.mjs
|
|
13051
13227
|
var import__ = __toESM(require_commander(), 1);
|
|
13052
13228
|
var {
|
|
@@ -13457,15 +13633,32 @@ filesCmd.command("write <id> <path>").description("Write content to a file in a
|
|
|
13457
13633
|
handleError(err);
|
|
13458
13634
|
}
|
|
13459
13635
|
});
|
|
13636
|
+
filesCmd.command("sync <id> <localDir> <remoteDir>").description("Upload a local directory into a sandbox (fast archive, no git clone)").option("--exclude <patterns>", "Comma-separated exclude patterns (default: node_modules,.git,dist,\u2026)").action(async (id, localDir, remoteDir, opts) => {
|
|
13637
|
+
try {
|
|
13638
|
+
const sandbox = getSandbox(id);
|
|
13639
|
+
if (!sandbox.provider_sandbox_id) {
|
|
13640
|
+
console.error(chalk.red("Sandbox has no provider ID."));
|
|
13641
|
+
process.exit(1);
|
|
13642
|
+
}
|
|
13643
|
+
const p = await getProvider(sandbox.provider);
|
|
13644
|
+
const exclude = opts.exclude ? opts.exclude.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
|
|
13645
|
+
const result = await p.uploadDir(sandbox.provider_sandbox_id, localDir, remoteDir, exclude ? { exclude } : undefined);
|
|
13646
|
+
console.log(chalk.green(`Uploaded ${localDir} \u2192 ${remoteDir} (${result.bytes} bytes)`));
|
|
13647
|
+
} catch (err) {
|
|
13648
|
+
handleError(err);
|
|
13649
|
+
}
|
|
13650
|
+
});
|
|
13460
13651
|
var agentCmd = program2.command("agent").description("Run and manage AI agents in sandboxes");
|
|
13461
|
-
agentCmd.command("run <id>").description("Run an AI agent inside a sandbox").requiredOption("-t, --type <type>", "Agent type: takumi, codex, gemini, opencode, pi, custom").requiredOption("-p, --prompt <prompt>", "Prompt for the agent").option("-n, --name <name>", "Agent name").option("-c, --command <cmd>", "Custom command (for 'custom' type)").action(async (id, opts) => {
|
|
13652
|
+
agentCmd.command("run <id>").description("Run an AI agent inside a sandbox").requiredOption("-t, --type <type>", "Agent type: claude, takumi, codex, gemini, opencode, pi, custom").requiredOption("-p, --prompt <prompt>", "Prompt for the agent").option("-n, --name <name>", "Agent name").option("-c, --command <cmd>", "Custom command (for 'custom' type)").option("--secret <mapping>", "Inject a vault secret as an env var: ENV_NAME=vault/key (repeatable)", (val, prev) => [...prev, val], []).action(async (id, opts) => {
|
|
13462
13653
|
try {
|
|
13463
13654
|
const { runAgent: runAgent2 } = await Promise.resolve().then(() => (init_agent_runner(), exports_agent_runner));
|
|
13655
|
+
const callEnvVars = opts.secret.length ? await (await Promise.resolve().then(() => exports_secrets)).resolveSecretSpecs(opts.secret) : undefined;
|
|
13464
13656
|
const session = await runAgent2(id, {
|
|
13465
13657
|
agentType: opts.type,
|
|
13466
13658
|
prompt: opts.prompt,
|
|
13467
13659
|
agentName: opts.name,
|
|
13468
13660
|
command: opts.command,
|
|
13661
|
+
callEnvVars,
|
|
13469
13662
|
onStdout: (data) => process.stdout.write(data),
|
|
13470
13663
|
onStderr: (data) => process.stderr.write(data)
|
|
13471
13664
|
});
|
package/dist/index.js
CHANGED
|
@@ -34,7 +34,7 @@ var init_types = __esm(() => {
|
|
|
34
34
|
"failed",
|
|
35
35
|
"killed"
|
|
36
36
|
];
|
|
37
|
-
AGENT_TYPES = ["codex", "gemini", "opencode", "pi", "takumi", "custom"];
|
|
37
|
+
AGENT_TYPES = ["claude", "codex", "gemini", "opencode", "pi", "takumi", "custom"];
|
|
38
38
|
EVENT_TYPES = [
|
|
39
39
|
"stdout",
|
|
40
40
|
"stderr",
|
|
@@ -87,6 +87,50 @@ var init_types = __esm(() => {
|
|
|
87
87
|
};
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
// src/lib/archive.ts
|
|
91
|
+
import { existsSync as existsSync7, statSync } from "fs";
|
|
92
|
+
function shellQuote(value) {
|
|
93
|
+
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
94
|
+
}
|
|
95
|
+
async function tarDirectory(localDir, opts) {
|
|
96
|
+
if (!existsSync7(localDir) || !statSync(localDir).isDirectory()) {
|
|
97
|
+
throw new Error(`tarDirectory: not a directory: ${localDir}`);
|
|
98
|
+
}
|
|
99
|
+
const excludes = opts?.exclude ?? DEFAULT_UPLOAD_EXCLUDES;
|
|
100
|
+
const args = ["-czf", "-"];
|
|
101
|
+
for (const ex of excludes)
|
|
102
|
+
args.push(`--exclude=${ex}`);
|
|
103
|
+
args.push("-C", localDir, ".");
|
|
104
|
+
const proc = Bun.spawn(["tar", ...args], { stdout: "pipe", stderr: "pipe" });
|
|
105
|
+
const [buf, stderr, exitCode] = await Promise.all([
|
|
106
|
+
new Response(proc.stdout).arrayBuffer(),
|
|
107
|
+
new Response(proc.stderr).text(),
|
|
108
|
+
proc.exited
|
|
109
|
+
]);
|
|
110
|
+
if (exitCode !== 0) {
|
|
111
|
+
throw new Error(`tarDirectory: tar exited ${exitCode}: ${stderr.trim()}`);
|
|
112
|
+
}
|
|
113
|
+
return Buffer.from(buf);
|
|
114
|
+
}
|
|
115
|
+
function buildUntarCommand(remoteTarPath, remoteDir) {
|
|
116
|
+
const tar = shellQuote(remoteTarPath);
|
|
117
|
+
const dir = shellQuote(remoteDir);
|
|
118
|
+
return `mkdir -p ${dir} && tar -xzf ${tar} -C ${dir} && rm -f ${tar}`;
|
|
119
|
+
}
|
|
120
|
+
var DEFAULT_UPLOAD_EXCLUDES;
|
|
121
|
+
var init_archive = __esm(() => {
|
|
122
|
+
DEFAULT_UPLOAD_EXCLUDES = [
|
|
123
|
+
"node_modules",
|
|
124
|
+
".git",
|
|
125
|
+
"dist",
|
|
126
|
+
".next",
|
|
127
|
+
".turbo",
|
|
128
|
+
".cache",
|
|
129
|
+
".venv",
|
|
130
|
+
"__pycache__"
|
|
131
|
+
];
|
|
132
|
+
});
|
|
133
|
+
|
|
90
134
|
// src/providers/e2b.ts
|
|
91
135
|
var exports_e2b = {};
|
|
92
136
|
__export(exports_e2b, {
|
|
@@ -235,6 +279,22 @@ class E2BProvider {
|
|
|
235
279
|
throw new ProviderError("e2b", `Failed to list files at ${path}: ${err.message}`);
|
|
236
280
|
}
|
|
237
281
|
}
|
|
282
|
+
async uploadDir(sandboxId, localDir, remoteDir, opts) {
|
|
283
|
+
const sandbox = await this.getInstance(sandboxId);
|
|
284
|
+
try {
|
|
285
|
+
const archive = await tarDirectory(localDir, opts);
|
|
286
|
+
const remoteTar = `/tmp/sandboxes-upload-${Date.now()}.tar.gz`;
|
|
287
|
+
const data = archive.buffer.slice(archive.byteOffset, archive.byteOffset + archive.byteLength);
|
|
288
|
+
await sandbox.files.write(remoteTar, data);
|
|
289
|
+
const result = await sandbox.commands.run(buildUntarCommand(remoteTar, remoteDir));
|
|
290
|
+
if ((result.exitCode ?? 0) !== 0) {
|
|
291
|
+
throw new Error(result.stderr || `untar exited with code ${result.exitCode}`);
|
|
292
|
+
}
|
|
293
|
+
return { bytes: archive.length };
|
|
294
|
+
} catch (err) {
|
|
295
|
+
throw new ProviderError("e2b", `Failed to upload directory to ${remoteDir}: ${err.message}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
238
298
|
async stop(sandboxId) {
|
|
239
299
|
const sandbox = await this.getInstance(sandboxId);
|
|
240
300
|
try {
|
|
@@ -285,6 +345,7 @@ class E2BProvider {
|
|
|
285
345
|
var instanceCache;
|
|
286
346
|
var init_e2b = __esm(() => {
|
|
287
347
|
init_types();
|
|
348
|
+
init_archive();
|
|
288
349
|
instanceCache = new Map;
|
|
289
350
|
});
|
|
290
351
|
|
|
@@ -415,6 +476,21 @@ class DaytonaProvider {
|
|
|
415
476
|
throw new ProviderError("daytona", `Failed to list files at ${path}: ${err.message}`);
|
|
416
477
|
}
|
|
417
478
|
}
|
|
479
|
+
async uploadDir(sandboxId, localDir, remoteDir, opts) {
|
|
480
|
+
const sandbox = await this.getInstance(sandboxId);
|
|
481
|
+
try {
|
|
482
|
+
const archive = await tarDirectory(localDir, opts);
|
|
483
|
+
const remoteTar = `/tmp/sandboxes-upload-${Date.now()}.tar.gz`;
|
|
484
|
+
await sandbox.fs.uploadFile(archive, remoteTar);
|
|
485
|
+
const result = await sandbox.process.executeCommand(buildUntarCommand(remoteTar, remoteDir));
|
|
486
|
+
if (result.exitCode !== 0) {
|
|
487
|
+
throw new Error(result.result || `untar exited with code ${result.exitCode}`);
|
|
488
|
+
}
|
|
489
|
+
return { bytes: archive.length };
|
|
490
|
+
} catch (err) {
|
|
491
|
+
throw new ProviderError("daytona", `Failed to upload directory to ${remoteDir}: ${err.message}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
418
494
|
async stop(sandboxId) {
|
|
419
495
|
const sandbox = await this.getInstance(sandboxId);
|
|
420
496
|
try {
|
|
@@ -455,6 +531,7 @@ class DaytonaProvider {
|
|
|
455
531
|
var instanceCache2;
|
|
456
532
|
var init_daytona = __esm(() => {
|
|
457
533
|
init_types();
|
|
534
|
+
init_archive();
|
|
458
535
|
instanceCache2 = new Map;
|
|
459
536
|
});
|
|
460
537
|
|
|
@@ -652,6 +729,32 @@ class ModalProvider {
|
|
|
652
729
|
throw new ProviderError("modal", `Failed to list files at ${path}: ${err.message}`);
|
|
653
730
|
}
|
|
654
731
|
}
|
|
732
|
+
async uploadDir(sandboxId, localDir, remoteDir, opts) {
|
|
733
|
+
try {
|
|
734
|
+
const archive = await tarDirectory(localDir, opts);
|
|
735
|
+
const b64 = archive.toString("base64");
|
|
736
|
+
const remoteTar = `/tmp/sandboxes-upload-${Date.now()}.tar.gz`;
|
|
737
|
+
const remoteB64 = `${remoteTar}.b64`;
|
|
738
|
+
const execChecked = async (cmd) => {
|
|
739
|
+
const result = await this.exec(sandboxId, `sh -c ${this.shellEscape(cmd)}`);
|
|
740
|
+
if (result.exit_code !== 0) {
|
|
741
|
+
throw new Error(result.stderr || `command exited with code ${result.exit_code}`);
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
await execChecked(`: > ${remoteB64}`);
|
|
745
|
+
const CHUNK = 60000;
|
|
746
|
+
for (let i = 0;i < b64.length; i += CHUNK) {
|
|
747
|
+
const chunk = b64.slice(i, i + CHUNK);
|
|
748
|
+
await execChecked(`printf '%s' ${this.shellEscape(chunk)} >> ${remoteB64}`);
|
|
749
|
+
}
|
|
750
|
+
await execChecked(`base64 -d ${remoteB64} > ${remoteTar} && ${buildUntarCommand(remoteTar, remoteDir)} && rm -f ${remoteB64}`);
|
|
751
|
+
return { bytes: archive.length };
|
|
752
|
+
} catch (err) {
|
|
753
|
+
if (err instanceof ProviderError)
|
|
754
|
+
throw err;
|
|
755
|
+
throw new ProviderError("modal", `Failed to upload directory to ${remoteDir}: ${err.message}`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
655
758
|
async stop(sandboxId) {
|
|
656
759
|
const sandbox = this.getSandbox(sandboxId);
|
|
657
760
|
try {
|
|
@@ -718,6 +821,7 @@ class ModalProvider {
|
|
|
718
821
|
var sandboxCache;
|
|
719
822
|
var init_modal = __esm(() => {
|
|
720
823
|
init_types();
|
|
824
|
+
init_archive();
|
|
721
825
|
sandboxCache = new Map;
|
|
722
826
|
});
|
|
723
827
|
|
|
@@ -11242,6 +11346,34 @@ function finalizeSandboxProvisionFailure(sandboxId, error) {
|
|
|
11242
11346
|
return getErrorMessage(error);
|
|
11243
11347
|
}
|
|
11244
11348
|
|
|
11349
|
+
// src/lib/agents/claude.ts
|
|
11350
|
+
class ClaudeDriver {
|
|
11351
|
+
name = "claude";
|
|
11352
|
+
requiredEnvVars = ["ANTHROPIC_API_KEY"];
|
|
11353
|
+
async install(provider, providerSandboxId) {
|
|
11354
|
+
const check = await provider.exec(providerSandboxId, "which claude 2>/dev/null || echo MISSING");
|
|
11355
|
+
if (check.stdout.trim() !== "MISSING")
|
|
11356
|
+
return;
|
|
11357
|
+
const bunCheck = await provider.exec(providerSandboxId, "which bun 2>/dev/null || echo MISSING");
|
|
11358
|
+
if (bunCheck.stdout.trim() !== "MISSING") {
|
|
11359
|
+
await provider.exec(providerSandboxId, "bun install -g @anthropic-ai/claude-code 2>&1");
|
|
11360
|
+
} else {
|
|
11361
|
+
await provider.exec(providerSandboxId, "npm install -g @anthropic-ai/claude-code 2>&1 || sudo npm install -g @anthropic-ai/claude-code 2>&1");
|
|
11362
|
+
}
|
|
11363
|
+
}
|
|
11364
|
+
async configure(provider, providerSandboxId, _envVars) {
|
|
11365
|
+
const config = JSON.stringify({
|
|
11366
|
+
hasCompletedOnboarding: true,
|
|
11367
|
+
hasTrustDialogAccepted: true,
|
|
11368
|
+
bypassPermissionsModeAccepted: true
|
|
11369
|
+
});
|
|
11370
|
+
await provider.exec(providerSandboxId, `mkdir -p ~/.claude && echo '${config}' > ~/.claude.json`);
|
|
11371
|
+
}
|
|
11372
|
+
buildCommand(prompt) {
|
|
11373
|
+
return `claude --dangerously-skip-permissions -p ${JSON.stringify(prompt)}`;
|
|
11374
|
+
}
|
|
11375
|
+
}
|
|
11376
|
+
|
|
11245
11377
|
// src/lib/agents/codex.ts
|
|
11246
11378
|
class CodexDriver {
|
|
11247
11379
|
name = "codex";
|
|
@@ -11349,6 +11481,7 @@ class TakumiDriver {
|
|
|
11349
11481
|
|
|
11350
11482
|
// src/lib/agents/index.ts
|
|
11351
11483
|
var DRIVERS = [
|
|
11484
|
+
new ClaudeDriver,
|
|
11352
11485
|
new CodexDriver,
|
|
11353
11486
|
new GeminiDriver,
|
|
11354
11487
|
new OpenCodeDriver,
|
|
@@ -11363,6 +11496,27 @@ function listAgentDrivers() {
|
|
|
11363
11496
|
return DRIVERS;
|
|
11364
11497
|
}
|
|
11365
11498
|
|
|
11499
|
+
// src/lib/secrets.ts
|
|
11500
|
+
var cliSecretResolver = async (key) => {
|
|
11501
|
+
const proc = Bun.spawn(["secrets", "get", key], { stdout: "pipe", stderr: "pipe" });
|
|
11502
|
+
const [out, errText, code] = await Promise.all([
|
|
11503
|
+
new Response(proc.stdout).text(),
|
|
11504
|
+
new Response(proc.stderr).text(),
|
|
11505
|
+
proc.exited
|
|
11506
|
+
]);
|
|
11507
|
+
if (code !== 0) {
|
|
11508
|
+
throw new Error(`secrets get ${key} failed: ${errText.trim() || `exit ${code}`}`);
|
|
11509
|
+
}
|
|
11510
|
+
return out.replace(/\r?\n$/, "");
|
|
11511
|
+
};
|
|
11512
|
+
async function resolveSecretEnv(mappings, resolver = cliSecretResolver) {
|
|
11513
|
+
const env = {};
|
|
11514
|
+
for (const mapping of mappings) {
|
|
11515
|
+
env[mapping.env] = await resolver(mapping.key);
|
|
11516
|
+
}
|
|
11517
|
+
return env;
|
|
11518
|
+
}
|
|
11519
|
+
|
|
11366
11520
|
// src/sdk.ts
|
|
11367
11521
|
function isExecHandle(value) {
|
|
11368
11522
|
return typeof value.wait === "function";
|
|
@@ -11482,10 +11636,22 @@ class SandboxesSDK {
|
|
|
11482
11636
|
const provider = await this.getProvider(sandbox.provider);
|
|
11483
11637
|
return provider.listFiles(sandbox.provider_sandbox_id, path, opts);
|
|
11484
11638
|
}
|
|
11639
|
+
async uploadDir(sandboxId, localDir, remoteDir, opts) {
|
|
11640
|
+
const sandbox = this.requireProviderSandbox(sandboxId);
|
|
11641
|
+
const provider = await this.getProvider(sandbox.provider);
|
|
11642
|
+
const result = await provider.uploadDir(sandbox.provider_sandbox_id, localDir, remoteDir, opts);
|
|
11643
|
+
emitLifecycleEvent(sandbox.id, `uploaded ${localDir} -> ${remoteDir} (${result.bytes} bytes)`);
|
|
11644
|
+
return result;
|
|
11645
|
+
}
|
|
11485
11646
|
async runAgent(sandboxId, opts) {
|
|
11486
11647
|
const sandbox = this.requireProviderSandbox(sandboxId);
|
|
11487
11648
|
const provider = await this.getProvider(sandbox.provider);
|
|
11488
|
-
|
|
11649
|
+
let callEnvVars = opts.callEnvVars;
|
|
11650
|
+
if (opts.secrets && opts.secrets.length > 0) {
|
|
11651
|
+
const secretEnv = await resolveSecretEnv(opts.secrets, opts.secretResolver);
|
|
11652
|
+
callEnvVars = { ...secretEnv, ...opts.callEnvVars };
|
|
11653
|
+
}
|
|
11654
|
+
const env = mergeEnv(sandbox.env_vars, callEnvVars);
|
|
11489
11655
|
let command;
|
|
11490
11656
|
const driver = opts.agentType !== "custom" ? getAgentDriver(opts.agentType) : undefined;
|
|
11491
11657
|
if (opts.command) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SandboxProvider } from "../../providers/types.js";
|
|
2
|
+
import type { AgentDriver } from "./types.js";
|
|
3
|
+
export declare class ClaudeDriver implements AgentDriver {
|
|
4
|
+
readonly name = "claude";
|
|
5
|
+
readonly requiredEnvVars: string[];
|
|
6
|
+
install(provider: SandboxProvider, providerSandboxId: string): Promise<void>;
|
|
7
|
+
configure(provider: SandboxProvider, providerSandboxId: string, _envVars: Record<string, string>): Promise<void>;
|
|
8
|
+
buildCommand(prompt: string): string;
|
|
9
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Directories that are almost never worth shipping into a sandbox and that
|
|
3
|
+
* dominate upload size. Callers can override via `TarDirectoryOptions.exclude`.
|
|
4
|
+
*/
|
|
5
|
+
export declare const DEFAULT_UPLOAD_EXCLUDES: string[];
|
|
6
|
+
export interface TarDirectoryOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Path/glob patterns to exclude (matched by `tar --exclude`). A bare name
|
|
9
|
+
* like `node_modules` excludes that directory and its contents at any depth
|
|
10
|
+
* on both bsdtar (macOS) and GNU tar (Linux). Defaults to
|
|
11
|
+
* {@link DEFAULT_UPLOAD_EXCLUDES}; pass `[]` to include everything.
|
|
12
|
+
*/
|
|
13
|
+
exclude?: string[];
|
|
14
|
+
}
|
|
15
|
+
/** Single-quote a value for safe POSIX shell interpolation. */
|
|
16
|
+
export declare function shellQuote(value: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Create a gzipped tar of a local directory's contents and return it as a Buffer.
|
|
19
|
+
*
|
|
20
|
+
* The archive is rooted at the directory contents (members are relative, e.g.
|
|
21
|
+
* `./src/index.ts`), so it extracts cleanly into any target with
|
|
22
|
+
* `tar -xzf - -C <dir>`. Uses the system `tar`, which is present on macOS
|
|
23
|
+
* (bsdtar) and Linux (GNU tar).
|
|
24
|
+
*/
|
|
25
|
+
export declare function tarDirectory(localDir: string, opts?: TarDirectoryOptions): Promise<Buffer>;
|
|
26
|
+
/**
|
|
27
|
+
* Build the shell command that unpacks an uploaded tarball into `remoteDir`
|
|
28
|
+
* and removes the tarball afterward. Shared by providers that upload a single
|
|
29
|
+
* archive then extract it in-sandbox.
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildUntarCommand(remoteTarPath: string, remoteDir: string): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve credentials from the @hasna/secrets vault and inject them into agents
|
|
3
|
+
* as per-call environment variables — never persisted to the sandbox record.
|
|
4
|
+
*/
|
|
5
|
+
export type SecretResolver = (key: string) => Promise<string>;
|
|
6
|
+
export interface SecretMapping {
|
|
7
|
+
/** Environment variable name to expose inside the sandbox (e.g. ANTHROPIC_API_KEY). */
|
|
8
|
+
env: string;
|
|
9
|
+
/** Vault key to read (e.g. hasnaxyz/anthropic/live/api_key). */
|
|
10
|
+
key: string;
|
|
11
|
+
}
|
|
12
|
+
/** Parse an `ENV_NAME=vault/key` spec into a {@link SecretMapping}. */
|
|
13
|
+
export declare function parseSecretMapping(spec: string): SecretMapping;
|
|
14
|
+
/**
|
|
15
|
+
* Default resolver: read a secret value from the @hasna/secrets vault via the
|
|
16
|
+
* globally-installed `secrets` CLI (`secrets get <key>`).
|
|
17
|
+
*/
|
|
18
|
+
export declare const cliSecretResolver: SecretResolver;
|
|
19
|
+
/**
|
|
20
|
+
* Resolve secret mappings to an `{ ENV_NAME: value }` record using the vault.
|
|
21
|
+
* The returned record is meant to be passed as per-call env vars (callEnvVars),
|
|
22
|
+
* so resolved secret values are never written to the persisted sandbox record.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveSecretEnv(mappings: SecretMapping[], resolver?: SecretResolver): Promise<Record<string, string>>;
|
|
25
|
+
/** Convenience: parse `ENV=key` specs and resolve them in one step. */
|
|
26
|
+
export declare function resolveSecretSpecs(specs: string[], resolver?: SecretResolver): Promise<Record<string, string>>;
|
package/dist/mcp/index.js
CHANGED
|
@@ -60,6 +60,50 @@ var init_types2 = __esm(() => {
|
|
|
60
60
|
};
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
// src/lib/archive.ts
|
|
64
|
+
import { existsSync as existsSync7, statSync } from "fs";
|
|
65
|
+
function shellQuote(value) {
|
|
66
|
+
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
67
|
+
}
|
|
68
|
+
async function tarDirectory(localDir, opts) {
|
|
69
|
+
if (!existsSync7(localDir) || !statSync(localDir).isDirectory()) {
|
|
70
|
+
throw new Error(`tarDirectory: not a directory: ${localDir}`);
|
|
71
|
+
}
|
|
72
|
+
const excludes = opts?.exclude ?? DEFAULT_UPLOAD_EXCLUDES;
|
|
73
|
+
const args = ["-czf", "-"];
|
|
74
|
+
for (const ex of excludes)
|
|
75
|
+
args.push(`--exclude=${ex}`);
|
|
76
|
+
args.push("-C", localDir, ".");
|
|
77
|
+
const proc = Bun.spawn(["tar", ...args], { stdout: "pipe", stderr: "pipe" });
|
|
78
|
+
const [buf, stderr, exitCode] = await Promise.all([
|
|
79
|
+
new Response(proc.stdout).arrayBuffer(),
|
|
80
|
+
new Response(proc.stderr).text(),
|
|
81
|
+
proc.exited
|
|
82
|
+
]);
|
|
83
|
+
if (exitCode !== 0) {
|
|
84
|
+
throw new Error(`tarDirectory: tar exited ${exitCode}: ${stderr.trim()}`);
|
|
85
|
+
}
|
|
86
|
+
return Buffer.from(buf);
|
|
87
|
+
}
|
|
88
|
+
function buildUntarCommand(remoteTarPath, remoteDir) {
|
|
89
|
+
const tar = shellQuote(remoteTarPath);
|
|
90
|
+
const dir = shellQuote(remoteDir);
|
|
91
|
+
return `mkdir -p ${dir} && tar -xzf ${tar} -C ${dir} && rm -f ${tar}`;
|
|
92
|
+
}
|
|
93
|
+
var DEFAULT_UPLOAD_EXCLUDES;
|
|
94
|
+
var init_archive = __esm(() => {
|
|
95
|
+
DEFAULT_UPLOAD_EXCLUDES = [
|
|
96
|
+
"node_modules",
|
|
97
|
+
".git",
|
|
98
|
+
"dist",
|
|
99
|
+
".next",
|
|
100
|
+
".turbo",
|
|
101
|
+
".cache",
|
|
102
|
+
".venv",
|
|
103
|
+
"__pycache__"
|
|
104
|
+
];
|
|
105
|
+
});
|
|
106
|
+
|
|
63
107
|
// src/providers/e2b.ts
|
|
64
108
|
var exports_e2b = {};
|
|
65
109
|
__export(exports_e2b, {
|
|
@@ -208,6 +252,22 @@ class E2BProvider {
|
|
|
208
252
|
throw new ProviderError("e2b", `Failed to list files at ${path}: ${err.message}`);
|
|
209
253
|
}
|
|
210
254
|
}
|
|
255
|
+
async uploadDir(sandboxId, localDir, remoteDir, opts) {
|
|
256
|
+
const sandbox = await this.getInstance(sandboxId);
|
|
257
|
+
try {
|
|
258
|
+
const archive = await tarDirectory(localDir, opts);
|
|
259
|
+
const remoteTar = `/tmp/sandboxes-upload-${Date.now()}.tar.gz`;
|
|
260
|
+
const data = archive.buffer.slice(archive.byteOffset, archive.byteOffset + archive.byteLength);
|
|
261
|
+
await sandbox.files.write(remoteTar, data);
|
|
262
|
+
const result = await sandbox.commands.run(buildUntarCommand(remoteTar, remoteDir));
|
|
263
|
+
if ((result.exitCode ?? 0) !== 0) {
|
|
264
|
+
throw new Error(result.stderr || `untar exited with code ${result.exitCode}`);
|
|
265
|
+
}
|
|
266
|
+
return { bytes: archive.length };
|
|
267
|
+
} catch (err) {
|
|
268
|
+
throw new ProviderError("e2b", `Failed to upload directory to ${remoteDir}: ${err.message}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
211
271
|
async stop(sandboxId) {
|
|
212
272
|
const sandbox = await this.getInstance(sandboxId);
|
|
213
273
|
try {
|
|
@@ -258,6 +318,7 @@ class E2BProvider {
|
|
|
258
318
|
var instanceCache;
|
|
259
319
|
var init_e2b = __esm(() => {
|
|
260
320
|
init_types2();
|
|
321
|
+
init_archive();
|
|
261
322
|
instanceCache = new Map;
|
|
262
323
|
});
|
|
263
324
|
|
|
@@ -388,6 +449,21 @@ class DaytonaProvider {
|
|
|
388
449
|
throw new ProviderError("daytona", `Failed to list files at ${path}: ${err.message}`);
|
|
389
450
|
}
|
|
390
451
|
}
|
|
452
|
+
async uploadDir(sandboxId, localDir, remoteDir, opts) {
|
|
453
|
+
const sandbox = await this.getInstance(sandboxId);
|
|
454
|
+
try {
|
|
455
|
+
const archive = await tarDirectory(localDir, opts);
|
|
456
|
+
const remoteTar = `/tmp/sandboxes-upload-${Date.now()}.tar.gz`;
|
|
457
|
+
await sandbox.fs.uploadFile(archive, remoteTar);
|
|
458
|
+
const result = await sandbox.process.executeCommand(buildUntarCommand(remoteTar, remoteDir));
|
|
459
|
+
if (result.exitCode !== 0) {
|
|
460
|
+
throw new Error(result.result || `untar exited with code ${result.exitCode}`);
|
|
461
|
+
}
|
|
462
|
+
return { bytes: archive.length };
|
|
463
|
+
} catch (err) {
|
|
464
|
+
throw new ProviderError("daytona", `Failed to upload directory to ${remoteDir}: ${err.message}`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
391
467
|
async stop(sandboxId) {
|
|
392
468
|
const sandbox = await this.getInstance(sandboxId);
|
|
393
469
|
try {
|
|
@@ -428,6 +504,7 @@ class DaytonaProvider {
|
|
|
428
504
|
var instanceCache2;
|
|
429
505
|
var init_daytona = __esm(() => {
|
|
430
506
|
init_types2();
|
|
507
|
+
init_archive();
|
|
431
508
|
instanceCache2 = new Map;
|
|
432
509
|
});
|
|
433
510
|
|
|
@@ -625,6 +702,32 @@ class ModalProvider {
|
|
|
625
702
|
throw new ProviderError("modal", `Failed to list files at ${path}: ${err.message}`);
|
|
626
703
|
}
|
|
627
704
|
}
|
|
705
|
+
async uploadDir(sandboxId, localDir, remoteDir, opts) {
|
|
706
|
+
try {
|
|
707
|
+
const archive = await tarDirectory(localDir, opts);
|
|
708
|
+
const b64 = archive.toString("base64");
|
|
709
|
+
const remoteTar = `/tmp/sandboxes-upload-${Date.now()}.tar.gz`;
|
|
710
|
+
const remoteB64 = `${remoteTar}.b64`;
|
|
711
|
+
const execChecked = async (cmd) => {
|
|
712
|
+
const result = await this.exec(sandboxId, `sh -c ${this.shellEscape(cmd)}`);
|
|
713
|
+
if (result.exit_code !== 0) {
|
|
714
|
+
throw new Error(result.stderr || `command exited with code ${result.exit_code}`);
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
await execChecked(`: > ${remoteB64}`);
|
|
718
|
+
const CHUNK = 60000;
|
|
719
|
+
for (let i = 0;i < b64.length; i += CHUNK) {
|
|
720
|
+
const chunk = b64.slice(i, i + CHUNK);
|
|
721
|
+
await execChecked(`printf '%s' ${this.shellEscape(chunk)} >> ${remoteB64}`);
|
|
722
|
+
}
|
|
723
|
+
await execChecked(`base64 -d ${remoteB64} > ${remoteTar} && ${buildUntarCommand(remoteTar, remoteDir)} && rm -f ${remoteB64}`);
|
|
724
|
+
return { bytes: archive.length };
|
|
725
|
+
} catch (err) {
|
|
726
|
+
if (err instanceof ProviderError)
|
|
727
|
+
throw err;
|
|
728
|
+
throw new ProviderError("modal", `Failed to upload directory to ${remoteDir}: ${err.message}`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
628
731
|
async stop(sandboxId) {
|
|
629
732
|
const sandbox = this.getSandbox(sandboxId);
|
|
630
733
|
try {
|
|
@@ -691,9 +794,53 @@ class ModalProvider {
|
|
|
691
794
|
var sandboxCache;
|
|
692
795
|
var init_modal = __esm(() => {
|
|
693
796
|
init_types2();
|
|
797
|
+
init_archive();
|
|
694
798
|
sandboxCache = new Map;
|
|
695
799
|
});
|
|
696
800
|
|
|
801
|
+
// src/lib/secrets.ts
|
|
802
|
+
var exports_secrets = {};
|
|
803
|
+
__export(exports_secrets, {
|
|
804
|
+
resolveSecretSpecs: () => resolveSecretSpecs,
|
|
805
|
+
resolveSecretEnv: () => resolveSecretEnv,
|
|
806
|
+
parseSecretMapping: () => parseSecretMapping,
|
|
807
|
+
cliSecretResolver: () => cliSecretResolver
|
|
808
|
+
});
|
|
809
|
+
function parseSecretMapping(spec) {
|
|
810
|
+
const idx = spec.indexOf("=");
|
|
811
|
+
if (idx <= 0) {
|
|
812
|
+
throw new Error(`Invalid secret mapping "${spec}" (expected ENV_NAME=vault/key)`);
|
|
813
|
+
}
|
|
814
|
+
const env = spec.slice(0, idx).trim();
|
|
815
|
+
const key = spec.slice(idx + 1).trim();
|
|
816
|
+
if (!env || !key) {
|
|
817
|
+
throw new Error(`Invalid secret mapping "${spec}" (expected ENV_NAME=vault/key)`);
|
|
818
|
+
}
|
|
819
|
+
return { env, key };
|
|
820
|
+
}
|
|
821
|
+
async function resolveSecretEnv(mappings, resolver = cliSecretResolver) {
|
|
822
|
+
const env = {};
|
|
823
|
+
for (const mapping of mappings) {
|
|
824
|
+
env[mapping.env] = await resolver(mapping.key);
|
|
825
|
+
}
|
|
826
|
+
return env;
|
|
827
|
+
}
|
|
828
|
+
async function resolveSecretSpecs(specs, resolver = cliSecretResolver) {
|
|
829
|
+
return resolveSecretEnv(specs.map(parseSecretMapping), resolver);
|
|
830
|
+
}
|
|
831
|
+
var cliSecretResolver = async (key) => {
|
|
832
|
+
const proc = Bun.spawn(["secrets", "get", key], { stdout: "pipe", stderr: "pipe" });
|
|
833
|
+
const [out, errText, code] = await Promise.all([
|
|
834
|
+
new Response(proc.stdout).text(),
|
|
835
|
+
new Response(proc.stderr).text(),
|
|
836
|
+
proc.exited
|
|
837
|
+
]);
|
|
838
|
+
if (code !== 0) {
|
|
839
|
+
throw new Error(`secrets get ${key} failed: ${errText.trim() || `exit ${code}`}`);
|
|
840
|
+
}
|
|
841
|
+
return out.replace(/\r?\n$/, "");
|
|
842
|
+
};
|
|
843
|
+
|
|
697
844
|
// src/mcp/index.ts
|
|
698
845
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
699
846
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -15650,6 +15797,34 @@ function emitLifecycleEvent(sandboxId, message) {
|
|
|
15650
15797
|
notifyListeners(sandboxId, "lifecycle", message);
|
|
15651
15798
|
}
|
|
15652
15799
|
|
|
15800
|
+
// src/lib/agents/claude.ts
|
|
15801
|
+
class ClaudeDriver {
|
|
15802
|
+
name = "claude";
|
|
15803
|
+
requiredEnvVars = ["ANTHROPIC_API_KEY"];
|
|
15804
|
+
async install(provider, providerSandboxId) {
|
|
15805
|
+
const check = await provider.exec(providerSandboxId, "which claude 2>/dev/null || echo MISSING");
|
|
15806
|
+
if (check.stdout.trim() !== "MISSING")
|
|
15807
|
+
return;
|
|
15808
|
+
const bunCheck = await provider.exec(providerSandboxId, "which bun 2>/dev/null || echo MISSING");
|
|
15809
|
+
if (bunCheck.stdout.trim() !== "MISSING") {
|
|
15810
|
+
await provider.exec(providerSandboxId, "bun install -g @anthropic-ai/claude-code 2>&1");
|
|
15811
|
+
} else {
|
|
15812
|
+
await provider.exec(providerSandboxId, "npm install -g @anthropic-ai/claude-code 2>&1 || sudo npm install -g @anthropic-ai/claude-code 2>&1");
|
|
15813
|
+
}
|
|
15814
|
+
}
|
|
15815
|
+
async configure(provider, providerSandboxId, _envVars) {
|
|
15816
|
+
const config = JSON.stringify({
|
|
15817
|
+
hasCompletedOnboarding: true,
|
|
15818
|
+
hasTrustDialogAccepted: true,
|
|
15819
|
+
bypassPermissionsModeAccepted: true
|
|
15820
|
+
});
|
|
15821
|
+
await provider.exec(providerSandboxId, `mkdir -p ~/.claude && echo '${config}' > ~/.claude.json`);
|
|
15822
|
+
}
|
|
15823
|
+
buildCommand(prompt) {
|
|
15824
|
+
return `claude --dangerously-skip-permissions -p ${JSON.stringify(prompt)}`;
|
|
15825
|
+
}
|
|
15826
|
+
}
|
|
15827
|
+
|
|
15653
15828
|
// src/lib/agents/codex.ts
|
|
15654
15829
|
class CodexDriver {
|
|
15655
15830
|
name = "codex";
|
|
@@ -15757,6 +15932,7 @@ class TakumiDriver {
|
|
|
15757
15932
|
|
|
15758
15933
|
// src/lib/agents/index.ts
|
|
15759
15934
|
var DRIVERS = [
|
|
15935
|
+
new ClaudeDriver,
|
|
15760
15936
|
new CodexDriver,
|
|
15761
15937
|
new GeminiDriver,
|
|
15762
15938
|
new OpenCodeDriver,
|
|
@@ -15882,7 +16058,7 @@ async function stopAgent(sandboxId) {
|
|
|
15882
16058
|
return;
|
|
15883
16059
|
const provider = await getProvider(sandbox.provider);
|
|
15884
16060
|
try {
|
|
15885
|
-
await provider.exec(sandbox.provider_sandbox_id, "pkill -f 'codex\\|gemini\\|opencode\\|pi\\|takumi' || true");
|
|
16061
|
+
await provider.exec(sandbox.provider_sandbox_id, "pkill -f 'claude\\|codex\\|gemini\\|opencode\\|pi\\|takumi' || true");
|
|
15886
16062
|
} catch {}
|
|
15887
16063
|
emitLifecycleEvent(sandbox.id, "Agent stopped by user");
|
|
15888
16064
|
}
|
|
@@ -15969,6 +16145,7 @@ var TOOL_CATALOG = [
|
|
|
15969
16145
|
{ name: "read_file", description: "Read a file from a sandbox" },
|
|
15970
16146
|
{ name: "write_file", description: "Write a file to a sandbox" },
|
|
15971
16147
|
{ name: "list_files", description: "List files in a sandbox directory" },
|
|
16148
|
+
{ name: "upload_dir", description: "Upload a local directory into a sandbox (fast archive, no git clone)" },
|
|
15972
16149
|
{ name: "get_session", description: "Get session details and exit code (useful for background commands)" },
|
|
15973
16150
|
{ name: "get_logs", description: "Get sandbox/session event logs" },
|
|
15974
16151
|
{ name: "register_agent", description: "Register an agent (idempotent, auto-heartbeat)" },
|
|
@@ -16281,6 +16458,27 @@ server.tool("list_files", "List files in a sandbox directory", {
|
|
|
16281
16458
|
return err(e);
|
|
16282
16459
|
}
|
|
16283
16460
|
});
|
|
16461
|
+
server.tool("upload_dir", "Upload a local directory into a sandbox as a single archive (fast, no git clone)", {
|
|
16462
|
+
sandbox_id: exports_external2.string().describe("Sandbox ID or partial ID"),
|
|
16463
|
+
local_dir: exports_external2.string().describe("Local directory path on the host to upload"),
|
|
16464
|
+
remote_dir: exports_external2.string().describe("Destination directory inside the sandbox"),
|
|
16465
|
+
exclude: exports_external2.array(exports_external2.string()).optional().describe("Patterns to exclude (defaults to node_modules, .git, dist, \u2026)")
|
|
16466
|
+
}, async (params) => {
|
|
16467
|
+
try {
|
|
16468
|
+
const sandbox = getSandbox(params.sandbox_id);
|
|
16469
|
+
if (!sandbox.provider_sandbox_id)
|
|
16470
|
+
throw new Error("Sandbox has no provider ID");
|
|
16471
|
+
const provider = await getProvider(sandbox.provider);
|
|
16472
|
+
const result = await provider.uploadDir(sandbox.provider_sandbox_id, params.local_dir, params.remote_dir, params.exclude ? { exclude: params.exclude } : undefined);
|
|
16473
|
+
return ok({
|
|
16474
|
+
local_dir: params.local_dir,
|
|
16475
|
+
remote_dir: params.remote_dir,
|
|
16476
|
+
bytes: result.bytes
|
|
16477
|
+
});
|
|
16478
|
+
} catch (e) {
|
|
16479
|
+
return err(e);
|
|
16480
|
+
}
|
|
16481
|
+
});
|
|
16284
16482
|
server.tool("get_session", "Get session details and exit code (useful for polling background command results)", {
|
|
16285
16483
|
session_id: exports_external2.string().describe("Session ID")
|
|
16286
16484
|
}, async (params) => {
|
|
@@ -16408,21 +16606,28 @@ server.tool("search_tools", "Search tools by keyword", {
|
|
|
16408
16606
|
});
|
|
16409
16607
|
server.tool("run_agent", "Run an AI agent inside a sandbox", {
|
|
16410
16608
|
sandbox_id: exports_external2.string().describe("Sandbox ID"),
|
|
16411
|
-
agent_type: exports_external2.enum(["codex", "gemini", "opencode", "pi", "takumi", "custom"]).describe("Agent type"),
|
|
16609
|
+
agent_type: exports_external2.enum(["claude", "codex", "gemini", "opencode", "pi", "takumi", "custom"]).describe("Agent type"),
|
|
16412
16610
|
prompt: exports_external2.string().describe("Prompt for the agent"),
|
|
16413
16611
|
agent_name: exports_external2.string().optional().describe("Agent name"),
|
|
16414
16612
|
command: exports_external2.string().optional().describe("Custom command (for 'custom' type)"),
|
|
16415
16613
|
env_vars: exports_external2.record(exports_external2.string()).optional().describe("Per-call environment variables (merged with sandbox env_vars, not persisted)"),
|
|
16614
|
+
secrets: exports_external2.array(exports_external2.string()).optional().describe("Vault secrets to inject as env vars: array of 'ENV_NAME=vault/key' (resolved from @hasna/secrets at call time, never persisted)"),
|
|
16416
16615
|
webhook_url: exports_external2.string().optional().describe("URL to POST result to when agent finishes"),
|
|
16417
16616
|
webhook_events: exports_external2.array(exports_external2.enum(["start", "complete", "error"])).optional().describe("Which events to notify on (default: all)")
|
|
16418
16617
|
}, async (params) => {
|
|
16419
16618
|
try {
|
|
16619
|
+
let callEnvVars = params.env_vars;
|
|
16620
|
+
if (params.secrets && params.secrets.length > 0) {
|
|
16621
|
+
const { resolveSecretSpecs: resolveSecretSpecs2 } = await Promise.resolve().then(() => exports_secrets);
|
|
16622
|
+
const secretEnv = await resolveSecretSpecs2(params.secrets);
|
|
16623
|
+
callEnvVars = { ...secretEnv, ...params.env_vars };
|
|
16624
|
+
}
|
|
16420
16625
|
const session = await runAgent(params.sandbox_id, {
|
|
16421
16626
|
agentType: params.agent_type,
|
|
16422
16627
|
prompt: params.prompt,
|
|
16423
16628
|
agentName: params.agent_name,
|
|
16424
16629
|
command: params.command,
|
|
16425
|
-
callEnvVars
|
|
16630
|
+
callEnvVars,
|
|
16426
16631
|
webhookUrl: params.webhook_url,
|
|
16427
16632
|
webhookEvents: params.webhook_events
|
|
16428
16633
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExecResult, ExecHandle, FileInfo } from "../types/index.js";
|
|
1
|
+
import type { ExecResult, ExecHandle, FileInfo, UploadDirOptions, UploadDirResult } from "../types/index.js";
|
|
2
2
|
import type { SandboxProvider, ProviderSandbox, CreateSandboxOpts, ExecOptions } from "./types.js";
|
|
3
3
|
export declare class DaytonaProvider implements SandboxProvider {
|
|
4
4
|
readonly name = "daytona";
|
|
@@ -10,6 +10,7 @@ export declare class DaytonaProvider implements SandboxProvider {
|
|
|
10
10
|
readFile(sandboxId: string, path: string): Promise<string>;
|
|
11
11
|
writeFile(sandboxId: string, path: string, content: string): Promise<void>;
|
|
12
12
|
listFiles(sandboxId: string, path: string): Promise<FileInfo[]>;
|
|
13
|
+
uploadDir(sandboxId: string, localDir: string, remoteDir: string, opts?: UploadDirOptions): Promise<UploadDirResult>;
|
|
13
14
|
stop(sandboxId: string): Promise<void>;
|
|
14
15
|
delete(sandboxId: string): Promise<void>;
|
|
15
16
|
getPublicUrl(_sandboxId: string, _port: number, _protocol?: string): Promise<string>;
|
package/dist/providers/e2b.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExecResult, ExecHandle, FileInfo } from "../types/index.js";
|
|
1
|
+
import type { ExecResult, ExecHandle, FileInfo, UploadDirOptions, UploadDirResult } from "../types/index.js";
|
|
2
2
|
import type { SandboxProvider, ProviderSandbox, CreateSandboxOpts, ExecOptions } from "./types.js";
|
|
3
3
|
export declare class E2BProvider implements SandboxProvider {
|
|
4
4
|
readonly name = "e2b";
|
|
@@ -17,6 +17,7 @@ export declare class E2BProvider implements SandboxProvider {
|
|
|
17
17
|
recursive?: boolean;
|
|
18
18
|
glob?: string;
|
|
19
19
|
}): Promise<FileInfo[]>;
|
|
20
|
+
uploadDir(sandboxId: string, localDir: string, remoteDir: string, opts?: UploadDirOptions): Promise<UploadDirResult>;
|
|
20
21
|
stop(sandboxId: string): Promise<void>;
|
|
21
22
|
delete(sandboxId: string): Promise<void>;
|
|
22
23
|
pause(sandboxId: string): Promise<void>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExecResult, ExecHandle, FileInfo } from "../types/index.js";
|
|
1
|
+
import type { ExecResult, ExecHandle, FileInfo, UploadDirOptions, UploadDirResult } from "../types/index.js";
|
|
2
2
|
import type { SandboxProvider, ProviderSandbox, CreateSandboxOpts, ExecOptions } from "./types.js";
|
|
3
3
|
export declare class ModalProvider implements SandboxProvider {
|
|
4
4
|
readonly name = "modal";
|
|
@@ -13,6 +13,7 @@ export declare class ModalProvider implements SandboxProvider {
|
|
|
13
13
|
readFile(sandboxId: string, path: string): Promise<string>;
|
|
14
14
|
writeFile(sandboxId: string, path: string, content: string): Promise<void>;
|
|
15
15
|
listFiles(sandboxId: string, path: string): Promise<FileInfo[]>;
|
|
16
|
+
uploadDir(sandboxId: string, localDir: string, remoteDir: string, opts?: UploadDirOptions): Promise<UploadDirResult>;
|
|
16
17
|
stop(sandboxId: string): Promise<void>;
|
|
17
18
|
delete(sandboxId: string): Promise<void>;
|
|
18
19
|
getPublicUrl(_sandboxId: string, _port: number, _protocol?: string): Promise<string>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExecResult, ExecHandle, FileInfo } from "../types/index.js";
|
|
1
|
+
import type { ExecResult, ExecHandle, FileInfo, UploadDirOptions, UploadDirResult } from "../types/index.js";
|
|
2
2
|
export interface CreateSandboxOpts {
|
|
3
3
|
image?: string;
|
|
4
4
|
timeout?: number;
|
|
@@ -34,6 +34,8 @@ export interface SandboxProvider {
|
|
|
34
34
|
recursive?: boolean;
|
|
35
35
|
glob?: string;
|
|
36
36
|
}): Promise<FileInfo[]>;
|
|
37
|
+
/** Upload a local directory tree into the sandbox at `remoteDir`, fast (single archive), without a git clone. */
|
|
38
|
+
uploadDir(sandboxId: string, localDir: string, remoteDir: string, opts?: UploadDirOptions): Promise<UploadDirResult>;
|
|
37
39
|
stop(sandboxId: string): Promise<void>;
|
|
38
40
|
delete(sandboxId: string): Promise<void>;
|
|
39
41
|
keepAlive(sandboxId: string, durationMs?: number): Promise<void>;
|
package/dist/sdk.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { listSandboxes } from "./db/sandboxes.js";
|
|
|
2
2
|
import { listEvents } from "./db/events.js";
|
|
3
3
|
import type { SandboxProvider } from "./providers/types.js";
|
|
4
4
|
import type { ExecOptions } from "./providers/types.js";
|
|
5
|
-
import type {
|
|
5
|
+
import type { SecretMapping, SecretResolver } from "./lib/secrets.js";
|
|
6
|
+
import type { AgentType, CreateSandboxInput, ExecResult, FileInfo, Sandbox, SandboxEvent, SandboxProviderName, SandboxSession, UploadDirOptions, UploadDirResult } from "./types/index.js";
|
|
6
7
|
import type { StreamListener } from "./lib/stream.js";
|
|
7
8
|
export type ProviderFactory = (name: SandboxProviderName, apiKey?: string) => Promise<SandboxProvider>;
|
|
8
9
|
export interface SandboxesSDKOptions {
|
|
@@ -20,6 +21,10 @@ export interface RunAgentOptions {
|
|
|
20
21
|
agentName?: string;
|
|
21
22
|
command?: string;
|
|
22
23
|
callEnvVars?: Record<string, string>;
|
|
24
|
+
/** Secrets to resolve from the vault and inject as per-call env vars (never persisted). */
|
|
25
|
+
secrets?: SecretMapping[];
|
|
26
|
+
/** Override the secret resolver (defaults to the `secrets` CLI). Useful for tests. */
|
|
27
|
+
secretResolver?: SecretResolver;
|
|
23
28
|
onStdout?: (data: string) => void;
|
|
24
29
|
onStderr?: (data: string) => void;
|
|
25
30
|
}
|
|
@@ -48,6 +53,7 @@ export declare class SandboxesSDK {
|
|
|
48
53
|
recursive?: boolean;
|
|
49
54
|
glob?: string;
|
|
50
55
|
}): Promise<FileInfo[]>;
|
|
56
|
+
uploadDir(sandboxId: string, localDir: string, remoteDir: string, opts?: UploadDirOptions): Promise<UploadDirResult>;
|
|
51
57
|
runAgent(sandboxId: string, opts: RunAgentOptions): Promise<SandboxSession>;
|
|
52
58
|
getSession(sessionId: string): SandboxSession;
|
|
53
59
|
waitForSession(sessionId: string, opts?: WaitForSessionOptions): Promise<SandboxSession>;
|
package/dist/server/index.js
CHANGED
|
@@ -60,6 +60,50 @@ var init_types2 = __esm(() => {
|
|
|
60
60
|
};
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
// src/lib/archive.ts
|
|
64
|
+
import { existsSync as existsSync7, statSync } from "fs";
|
|
65
|
+
function shellQuote(value) {
|
|
66
|
+
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
67
|
+
}
|
|
68
|
+
async function tarDirectory(localDir, opts) {
|
|
69
|
+
if (!existsSync7(localDir) || !statSync(localDir).isDirectory()) {
|
|
70
|
+
throw new Error(`tarDirectory: not a directory: ${localDir}`);
|
|
71
|
+
}
|
|
72
|
+
const excludes = opts?.exclude ?? DEFAULT_UPLOAD_EXCLUDES;
|
|
73
|
+
const args = ["-czf", "-"];
|
|
74
|
+
for (const ex of excludes)
|
|
75
|
+
args.push(`--exclude=${ex}`);
|
|
76
|
+
args.push("-C", localDir, ".");
|
|
77
|
+
const proc = Bun.spawn(["tar", ...args], { stdout: "pipe", stderr: "pipe" });
|
|
78
|
+
const [buf, stderr, exitCode] = await Promise.all([
|
|
79
|
+
new Response(proc.stdout).arrayBuffer(),
|
|
80
|
+
new Response(proc.stderr).text(),
|
|
81
|
+
proc.exited
|
|
82
|
+
]);
|
|
83
|
+
if (exitCode !== 0) {
|
|
84
|
+
throw new Error(`tarDirectory: tar exited ${exitCode}: ${stderr.trim()}`);
|
|
85
|
+
}
|
|
86
|
+
return Buffer.from(buf);
|
|
87
|
+
}
|
|
88
|
+
function buildUntarCommand(remoteTarPath, remoteDir) {
|
|
89
|
+
const tar = shellQuote(remoteTarPath);
|
|
90
|
+
const dir = shellQuote(remoteDir);
|
|
91
|
+
return `mkdir -p ${dir} && tar -xzf ${tar} -C ${dir} && rm -f ${tar}`;
|
|
92
|
+
}
|
|
93
|
+
var DEFAULT_UPLOAD_EXCLUDES;
|
|
94
|
+
var init_archive = __esm(() => {
|
|
95
|
+
DEFAULT_UPLOAD_EXCLUDES = [
|
|
96
|
+
"node_modules",
|
|
97
|
+
".git",
|
|
98
|
+
"dist",
|
|
99
|
+
".next",
|
|
100
|
+
".turbo",
|
|
101
|
+
".cache",
|
|
102
|
+
".venv",
|
|
103
|
+
"__pycache__"
|
|
104
|
+
];
|
|
105
|
+
});
|
|
106
|
+
|
|
63
107
|
// src/providers/e2b.ts
|
|
64
108
|
var exports_e2b = {};
|
|
65
109
|
__export(exports_e2b, {
|
|
@@ -208,6 +252,22 @@ class E2BProvider {
|
|
|
208
252
|
throw new ProviderError("e2b", `Failed to list files at ${path}: ${err.message}`);
|
|
209
253
|
}
|
|
210
254
|
}
|
|
255
|
+
async uploadDir(sandboxId, localDir, remoteDir, opts) {
|
|
256
|
+
const sandbox = await this.getInstance(sandboxId);
|
|
257
|
+
try {
|
|
258
|
+
const archive = await tarDirectory(localDir, opts);
|
|
259
|
+
const remoteTar = `/tmp/sandboxes-upload-${Date.now()}.tar.gz`;
|
|
260
|
+
const data = archive.buffer.slice(archive.byteOffset, archive.byteOffset + archive.byteLength);
|
|
261
|
+
await sandbox.files.write(remoteTar, data);
|
|
262
|
+
const result = await sandbox.commands.run(buildUntarCommand(remoteTar, remoteDir));
|
|
263
|
+
if ((result.exitCode ?? 0) !== 0) {
|
|
264
|
+
throw new Error(result.stderr || `untar exited with code ${result.exitCode}`);
|
|
265
|
+
}
|
|
266
|
+
return { bytes: archive.length };
|
|
267
|
+
} catch (err) {
|
|
268
|
+
throw new ProviderError("e2b", `Failed to upload directory to ${remoteDir}: ${err.message}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
211
271
|
async stop(sandboxId) {
|
|
212
272
|
const sandbox = await this.getInstance(sandboxId);
|
|
213
273
|
try {
|
|
@@ -258,6 +318,7 @@ class E2BProvider {
|
|
|
258
318
|
var instanceCache;
|
|
259
319
|
var init_e2b = __esm(() => {
|
|
260
320
|
init_types2();
|
|
321
|
+
init_archive();
|
|
261
322
|
instanceCache = new Map;
|
|
262
323
|
});
|
|
263
324
|
|
|
@@ -388,6 +449,21 @@ class DaytonaProvider {
|
|
|
388
449
|
throw new ProviderError("daytona", `Failed to list files at ${path}: ${err.message}`);
|
|
389
450
|
}
|
|
390
451
|
}
|
|
452
|
+
async uploadDir(sandboxId, localDir, remoteDir, opts) {
|
|
453
|
+
const sandbox = await this.getInstance(sandboxId);
|
|
454
|
+
try {
|
|
455
|
+
const archive = await tarDirectory(localDir, opts);
|
|
456
|
+
const remoteTar = `/tmp/sandboxes-upload-${Date.now()}.tar.gz`;
|
|
457
|
+
await sandbox.fs.uploadFile(archive, remoteTar);
|
|
458
|
+
const result = await sandbox.process.executeCommand(buildUntarCommand(remoteTar, remoteDir));
|
|
459
|
+
if (result.exitCode !== 0) {
|
|
460
|
+
throw new Error(result.result || `untar exited with code ${result.exitCode}`);
|
|
461
|
+
}
|
|
462
|
+
return { bytes: archive.length };
|
|
463
|
+
} catch (err) {
|
|
464
|
+
throw new ProviderError("daytona", `Failed to upload directory to ${remoteDir}: ${err.message}`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
391
467
|
async stop(sandboxId) {
|
|
392
468
|
const sandbox = await this.getInstance(sandboxId);
|
|
393
469
|
try {
|
|
@@ -428,6 +504,7 @@ class DaytonaProvider {
|
|
|
428
504
|
var instanceCache2;
|
|
429
505
|
var init_daytona = __esm(() => {
|
|
430
506
|
init_types2();
|
|
507
|
+
init_archive();
|
|
431
508
|
instanceCache2 = new Map;
|
|
432
509
|
});
|
|
433
510
|
|
|
@@ -625,6 +702,32 @@ class ModalProvider {
|
|
|
625
702
|
throw new ProviderError("modal", `Failed to list files at ${path}: ${err.message}`);
|
|
626
703
|
}
|
|
627
704
|
}
|
|
705
|
+
async uploadDir(sandboxId, localDir, remoteDir, opts) {
|
|
706
|
+
try {
|
|
707
|
+
const archive = await tarDirectory(localDir, opts);
|
|
708
|
+
const b64 = archive.toString("base64");
|
|
709
|
+
const remoteTar = `/tmp/sandboxes-upload-${Date.now()}.tar.gz`;
|
|
710
|
+
const remoteB64 = `${remoteTar}.b64`;
|
|
711
|
+
const execChecked = async (cmd) => {
|
|
712
|
+
const result = await this.exec(sandboxId, `sh -c ${this.shellEscape(cmd)}`);
|
|
713
|
+
if (result.exit_code !== 0) {
|
|
714
|
+
throw new Error(result.stderr || `command exited with code ${result.exit_code}`);
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
await execChecked(`: > ${remoteB64}`);
|
|
718
|
+
const CHUNK = 60000;
|
|
719
|
+
for (let i = 0;i < b64.length; i += CHUNK) {
|
|
720
|
+
const chunk = b64.slice(i, i + CHUNK);
|
|
721
|
+
await execChecked(`printf '%s' ${this.shellEscape(chunk)} >> ${remoteB64}`);
|
|
722
|
+
}
|
|
723
|
+
await execChecked(`base64 -d ${remoteB64} > ${remoteTar} && ${buildUntarCommand(remoteTar, remoteDir)} && rm -f ${remoteB64}`);
|
|
724
|
+
return { bytes: archive.length };
|
|
725
|
+
} catch (err) {
|
|
726
|
+
if (err instanceof ProviderError)
|
|
727
|
+
throw err;
|
|
728
|
+
throw new ProviderError("modal", `Failed to upload directory to ${remoteDir}: ${err.message}`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
628
731
|
async stop(sandboxId) {
|
|
629
732
|
const sandbox = this.getSandbox(sandboxId);
|
|
630
733
|
try {
|
|
@@ -691,6 +794,7 @@ class ModalProvider {
|
|
|
691
794
|
var sandboxCache;
|
|
692
795
|
var init_modal = __esm(() => {
|
|
693
796
|
init_types2();
|
|
797
|
+
init_archive();
|
|
694
798
|
sandboxCache = new Map;
|
|
695
799
|
});
|
|
696
800
|
|
package/dist/types/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export declare const SANDBOX_STATUSES: readonly ["creating", "running", "paused"
|
|
|
4
4
|
export type SandboxStatus = (typeof SANDBOX_STATUSES)[number];
|
|
5
5
|
export declare const SESSION_STATUSES: readonly ["running", "completed", "failed", "killed"];
|
|
6
6
|
export type SessionStatus = (typeof SESSION_STATUSES)[number];
|
|
7
|
-
export declare const AGENT_TYPES: readonly ["codex", "gemini", "opencode", "pi", "takumi", "custom"];
|
|
7
|
+
export declare const AGENT_TYPES: readonly ["claude", "codex", "gemini", "opencode", "pi", "takumi", "custom"];
|
|
8
8
|
export type AgentType = (typeof AGENT_TYPES)[number];
|
|
9
9
|
export declare const EVENT_TYPES: readonly ["stdout", "stderr", "lifecycle", "agent"];
|
|
10
10
|
export type EventType = (typeof EVENT_TYPES)[number];
|
|
@@ -176,6 +176,14 @@ export interface FileInfo {
|
|
|
176
176
|
is_dir: boolean;
|
|
177
177
|
size: number;
|
|
178
178
|
}
|
|
179
|
+
export interface UploadDirOptions {
|
|
180
|
+
/** Patterns to exclude (passed to `tar --exclude`); defaults applied by the archiver. */
|
|
181
|
+
exclude?: string[];
|
|
182
|
+
}
|
|
183
|
+
export interface UploadDirResult {
|
|
184
|
+
/** Number of bytes uploaded (compressed archive size). */
|
|
185
|
+
bytes: number;
|
|
186
|
+
}
|
|
179
187
|
export interface SandboxesConfig {
|
|
180
188
|
default_provider?: SandboxProviderName;
|
|
181
189
|
default_image?: string;
|