@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 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
- const env = mergeEnv(sandbox.env_vars, opts.callEnvVars);
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: params.env_vars,
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>;
@@ -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 { AgentType, CreateSandboxInput, ExecResult, FileInfo, Sandbox, SandboxEvent, SandboxProviderName, SandboxSession } from "./types/index.js";
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>;
@@ -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
 
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/sandboxes",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "author": "Andrei Hasna <andrei@hasna.com>",
5
5
  "repository": {
6
6
  "type": "git",