diff --git a/README.md b/README.md
index 1f90841..22b67be 100644
--- a/README.md
+++ b/README.md
@@ -63,10 +63,14 @@ Choose the installation method that works best for your use case.
Install skill with:
-```
+```bash
npx skills add rivet-dev/skills -s sandbox-agent
```
+```bash
+bunx skills add rivet-dev/skills -s sandbox-agent
+```
+
### TypeScript SDK
Import the SDK directly into your Node or browser application. Full type safety and streaming support.
@@ -77,6 +81,12 @@ Import the SDK directly into your Node or browser application. Full type safety
npm install sandbox-agent
```
+```bash
+bun add sandbox-agent
+# Optional: allow Bun to run postinstall scripts for native binaries (required for SandboxAgent.start()).
+bun pm trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64
+```
+
**Setup**
Local (embedded mode):
@@ -154,6 +164,12 @@ Install the CLI wrapper (optional but convenient):
npm install -g @sandbox-agent/cli
```
+```bash
+# Allow Bun to run postinstall scripts for native binaries.
+bun add -g @sandbox-agent/cli
+bun pm -g trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64
+```
+
Create a session and send a message:
```bash
@@ -168,6 +184,10 @@ You can also use npx like:
npx sandbox-agent --help
```
+```bash
+bunx sandbox-agent --help
+```
+
[CLI documentation](https://sandboxagent.dev/docs/cli)
### Inspector
diff --git a/docs/ai/skill.mdx b/docs/ai/skill.mdx
index bb48c2c..b06e657 100644
--- a/docs/ai/skill.mdx
+++ b/docs/ai/skill.mdx
@@ -13,9 +13,18 @@ https://rivet.dev/docs/skill.md
To add it to an agent using the Skills CLI:
-```
-npx skills add rivet-dev/skills -s sandbox-agent
-```
+
+
+ ```bash
+ npx skills add rivet-dev/skills -s sandbox-agent
+ ```
+
+
+ ```bash
+ bunx skills add rivet-dev/skills -s sandbox-agent
+ ```
+
+
If you run a reverse proxy in front of the docs, make sure `/skill.md` and `/.well-known/skills/*`
are forwarded to Mintlify.
diff --git a/docs/deploy/local.mdx b/docs/deploy/local.mdx
index e70a14f..5fafb50 100644
--- a/docs/deploy/local.mdx
+++ b/docs/deploy/local.mdx
@@ -15,11 +15,20 @@ curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh
sandbox-agent server --no-token --host 127.0.0.1 --port 2468
```
-Or with npm:
+Or with npm or Bun:
-```bash
-npx sandbox-agent server --no-token --host 127.0.0.1 --port 2468
-```
+
+
+ ```bash
+ npx sandbox-agent server --no-token --host 127.0.0.1 --port 2468
+ ```
+
+
+ ```bash
+ bunx sandbox-agent server --no-token --host 127.0.0.1 --port 2468
+ ```
+
+
## With the TypeScript SDK
diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx
index 4902344..f2a1f1b 100644
--- a/docs/quickstart.mdx
+++ b/docs/quickstart.mdx
@@ -6,9 +6,18 @@ icon: "rocket"
- ```bash
- npx skills add rivet-dev/skills -s sandbox-agent
- ```
+
+
+ ```bash
+ npx skills add rivet-dev/skills -s sandbox-agent
+ ```
+
+
+ ```bash
+ bunx skills add rivet-dev/skills -s sandbox-agent
+ ```
+
+
@@ -88,6 +97,14 @@ icon: "rocket"
```
+
+ Run without installing globally.
+
+ ```bash
+ bunx @sandbox-agent/cli server --no-token --host 0.0.0.0 --port 2468
+ ```
+
+
Install globally, then run.
@@ -97,17 +114,41 @@ icon: "rocket"
```
-
- If you're running from source instead of the installed CLI.
+
+ Install globally, then run.
```bash
- cargo run -p sandbox-agent -- server --no-token --host 0.0.0.0 --port 2468
+ bun add -g @sandbox-agent/cli
+ # Allow Bun to run postinstall scripts for native binaries (required for SandboxAgent.start()).
+ bun pm -g trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64
+ sandbox-agent server --no-token --host 0.0.0.0 --port 2468
+ ```
+
+
+
+
+ For local development, use `SandboxAgent.start()` to automatically spawn and manage the server as a subprocess.
+
+ ```bash
+ npm install sandbox-agent
+ ```
+
+ ```typescript
+ import { SandboxAgent } from "sandbox-agent";
+
+ const client = await SandboxAgent.start();
```
-
+
For local development, use `SandboxAgent.start()` to automatically spawn and manage the server as a subprocess.
+ ```bash
+ bun add sandbox-agent
+ # Allow Bun to run postinstall scripts for native binaries (required for SandboxAgent.start()).
+ bun pm trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64
+ ```
+
```typescript
import { SandboxAgent } from "sandbox-agent";
@@ -116,6 +157,14 @@ icon: "rocket"
This installs the binary and starts the server for you. No manual setup required.
+
+
+ If you're running from source instead of the installed CLI.
+
+ ```bash
+ cargo run -p sandbox-agent -- server --no-token --host 0.0.0.0 --port 2468
+ ```
+
Binding to `0.0.0.0` allows the server to accept connections from any network interface, which is required when running inside a sandbox where clients connect remotely.
diff --git a/docs/sdks/typescript.mdx b/docs/sdks/typescript.mdx
index 7ebc165..7cd2ef1 100644
--- a/docs/sdks/typescript.mdx
+++ b/docs/sdks/typescript.mdx
@@ -9,9 +9,20 @@ client for sessions, events, and agent operations.
## Install
-```bash
-npm install sandbox-agent
-```
+
+
+ ```bash
+ npm install sandbox-agent
+ ```
+
+
+ ```bash
+ bun add sandbox-agent
+ # Allow Bun to run postinstall scripts for native binaries (required for SandboxAgent.start()).
+ bun pm trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64
+ ```
+
+
## Create a client
diff --git a/justfile b/justfile
index d426bb4..2b3dd6b 100644
--- a/justfile
+++ b/justfile
@@ -46,3 +46,7 @@ check:
[group('dev')]
fmt:
cargo fmt --all
+
+[group('dev')]
+dev-docs:
+ cd docs && pnpm dlx mintlify dev
diff --git a/package.json b/package.json
index 66b5e57..a8566b6 100644
--- a/package.json
+++ b/package.json
@@ -7,8 +7,7 @@
"build": "turbo run build",
"dev": "turbo run dev --parallel",
"generate": "turbo run generate",
- "typecheck": "turbo run typecheck",
- "dev:docs": "cd docs/ && pnpm dlx mintlify dev"
+ "typecheck": "turbo run typecheck"
},
"devDependencies": {
"turbo": "^2.4.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f384d73..321e588 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -288,6 +288,10 @@ importers:
version: 5.9.3
sdks/cli:
+ dependencies:
+ '@sandbox-agent/cli-shared':
+ specifier: workspace:*
+ version: link:../cli-shared
optionalDependencies:
'@sandbox-agent/cli-darwin-arm64':
specifier: workspace:*
@@ -306,6 +310,18 @@ importers:
specifier: ^3.0.0
version: 3.2.4(@types/debug@4.1.12)(@types/node@25.1.0)
+ sdks/cli-shared:
+ devDependencies:
+ '@types/node':
+ specifier: ^22.0.0
+ version: 22.19.7
+ tsup:
+ specifier: ^8.0.0
+ version: 8.5.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
+ typescript:
+ specifier: ^5.7.0
+ version: 5.9.3
+
sdks/cli/platforms/darwin-arm64: {}
sdks/cli/platforms/darwin-x64: {}
@@ -315,6 +331,10 @@ importers:
sdks/cli/platforms/win32-x64: {}
sdks/typescript:
+ dependencies:
+ '@sandbox-agent/cli-shared':
+ specifier: workspace:*
+ version: link:../cli-shared
devDependencies:
'@types/node':
specifier: ^22.0.0
diff --git a/research/detect-sandbox.md b/research/detect-sandbox.md
index 3c9961f..d081f1f 100644
--- a/research/detect-sandbox.md
+++ b/research/detect-sandbox.md
@@ -567,7 +567,7 @@ console.log(`Running on: ${provider.provider} (${provider.confidence} confidence
Several open-source projects implement cloud detection patterns:
- **cloud-detect** (Python, `pip install cloud-detect`): Detects AWS, GCP, Azure, Alibaba, DigitalOcean, Oracle via filesystem + metadata
-- **cloud-detect-js** (Node, `npm install cloud-detect-js`): JavaScript port with similar capabilities
+- **cloud-detect-js** (Node, `npm install cloud-detect-js` or `bun add cloud-detect-js`): JavaScript port with similar capabilities
- **banzaicloud/satellite** (Go): Uses two-tier detection with sysfs first, then metadata fallback
- **OpenTelemetry Resource Detectors**: Production-grade detectors across Node.js, Python, Go — use `@opentelemetry/resource-detector-aws`, `@opentelemetry/resource-detector-gcp`, etc.
diff --git a/sdks/cli-shared/package.json b/sdks/cli-shared/package.json
new file mode 100644
index 0000000..896c536
--- /dev/null
+++ b/sdks/cli-shared/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@sandbox-agent/cli-shared",
+ "version": "0.1.4-rc.7",
+ "description": "Shared helpers for sandbox-agent CLI and SDK",
+ "license": "Apache-2.0",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/rivet-dev/sandbox-agent"
+ },
+ "type": "module",
+ "scripts": {
+ "build": "tsup",
+ "typecheck": "tsc --noEmit"
+ },
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.mjs",
+ "require": "./dist/index.cjs"
+ }
+ },
+ "files": [
+ "dist"
+ ],
+ "devDependencies": {
+ "@types/node": "^22.0.0",
+ "tsup": "^8.0.0",
+ "typescript": "^5.7.0"
+ }
+}
diff --git a/sdks/cli-shared/src/index.ts b/sdks/cli-shared/src/index.ts
new file mode 100644
index 0000000..3696364
--- /dev/null
+++ b/sdks/cli-shared/src/index.ts
@@ -0,0 +1,63 @@
+export type InstallCommandBlock = {
+ label: string;
+ commands: string[];
+};
+
+export type NonExecutableBinaryMessageOptions = {
+ binPath: string;
+ trustPackages: string;
+ bunInstallBlocks: InstallCommandBlock[];
+ genericInstallCommands?: string[];
+};
+
+export function isBunRuntime(): boolean {
+ if (typeof process?.versions?.bun === "string") return true;
+ const userAgent = process?.env?.npm_config_user_agent || "";
+ return userAgent.includes("bun/");
+}
+
+const PERMISSION_ERRORS = new Set(["EACCES", "EPERM", "ENOEXEC"]);
+
+export function isPermissionError(error: unknown): boolean {
+ if (!error || typeof error !== "object") return false;
+ const code = (error as { code?: unknown }).code;
+ return typeof code === "string" && PERMISSION_ERRORS.has(code);
+}
+
+export function formatNonExecutableBinaryMessage(
+ options: NonExecutableBinaryMessageOptions,
+): string {
+ const { binPath, trustPackages, bunInstallBlocks, genericInstallCommands } =
+ options;
+
+ const lines = [`sandbox-agent binary is not executable: ${binPath}`];
+
+ if (isBunRuntime()) {
+ lines.push(
+ "Allow Bun to run postinstall scripts for native binaries and reinstall:",
+ );
+ for (const block of bunInstallBlocks) {
+ lines.push(`${block.label}:`);
+ for (const command of block.commands) {
+ lines.push(` ${command}`);
+ }
+ }
+ lines.push(`Or run: chmod +x "${binPath}"`);
+ return lines.join("\n");
+ }
+
+ lines.push(
+ "Postinstall scripts for native packages did not run, so the binary was left non-executable.",
+ );
+ if (genericInstallCommands && genericInstallCommands.length > 0) {
+ lines.push("Reinstall with scripts enabled:");
+ for (const command of genericInstallCommands) {
+ lines.push(` ${command}`);
+ }
+ } else {
+ lines.push("Reinstall with scripts enabled for:");
+ lines.push(` ${trustPackages}`);
+ }
+ lines.push(`Or run: chmod +x "${binPath}"`);
+ return lines.join("\n");
+}
diff --git a/sdks/cli-shared/tsconfig.json b/sdks/cli-shared/tsconfig.json
new file mode 100644
index 0000000..a274d38
--- /dev/null
+++ b/sdks/cli-shared/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2022"],
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "noEmit": true,
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "types": ["node"]
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/sdks/cli-shared/tsup.config.ts b/sdks/cli-shared/tsup.config.ts
new file mode 100644
index 0000000..644948a
--- /dev/null
+++ b/sdks/cli-shared/tsup.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from "tsup";
+
+export default defineConfig({
+ entry: ["src/index.ts"],
+ format: ["esm", "cjs"],
+ dts: true,
+ clean: true,
+ sourcemap: true,
+});
diff --git a/sdks/cli/bin/sandbox-agent b/sdks/cli/bin/sandbox-agent
index 9374244..93cac38 100755
--- a/sdks/cli/bin/sandbox-agent
+++ b/sdks/cli/bin/sandbox-agent
@@ -1,7 +1,40 @@
#!/usr/bin/env node
const { execFileSync } = require("child_process");
+const {
+ formatNonExecutableBinaryMessage,
+ isPermissionError,
+} = require("@sandbox-agent/cli-shared");
+const fs = require("fs");
const path = require("path");
+const TRUST_PACKAGES =
+ "@sandbox-agent/cli-linux-x64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64";
+
+function printExecutableHint(binPath) {
+ console.error(
+ formatNonExecutableBinaryMessage({
+ binPath,
+ trustPackages: TRUST_PACKAGES,
+ bunInstallBlocks: [
+ {
+ label: "Project install",
+ commands: [
+ `bun pm trust ${TRUST_PACKAGES}`,
+ "bun add @sandbox-agent/cli",
+ ],
+ },
+ {
+ label: "Global install",
+ commands: [
+ `bun pm -g trust ${TRUST_PACKAGES}`,
+ "bun add -g @sandbox-agent/cli",
+ ],
+ },
+ ],
+ }),
+ );
+}
+
const PLATFORMS = {
"darwin-arm64": "@sandbox-agent/cli-darwin-arm64",
"darwin-x64": "@sandbox-agent/cli-darwin-x64",
@@ -19,7 +52,30 @@ if (!pkg) {
try {
const pkgPath = require.resolve(`${pkg}/package.json`);
const bin = process.platform === "win32" ? "sandbox-agent.exe" : "sandbox-agent";
- execFileSync(path.join(path.dirname(pkgPath), "bin", bin), process.argv.slice(2), { stdio: "inherit" });
+ const binPath = path.join(path.dirname(pkgPath), "bin", bin);
+ if (process.platform !== "win32") {
+ try {
+ fs.accessSync(binPath, fs.constants.X_OK);
+ } catch (error) {
+ try {
+ fs.chmodSync(binPath, 0o755);
+ } catch (chmodError) {
+ if (isPermissionError(chmodError)) {
+ printExecutableHint(binPath);
+ }
+ console.error(`Failed to make ${binPath} executable.`);
+ throw chmodError;
+ }
+ }
+ }
+ try {
+ execFileSync(binPath, process.argv.slice(2), { stdio: "inherit" });
+ } catch (execError) {
+ if (isPermissionError(execError)) {
+ printExecutableHint(binPath);
+ }
+ throw execError;
+ }
} catch (e) {
if (e.status !== undefined) process.exit(e.status);
throw e;
diff --git a/sdks/cli/package.json b/sdks/cli/package.json
index 6001f31..79faa85 100644
--- a/sdks/cli/package.json
+++ b/sdks/cli/package.json
@@ -13,6 +13,9 @@
"scripts": {
"test": "vitest run"
},
+ "dependencies": {
+ "@sandbox-agent/cli-shared": "workspace:*"
+ },
"devDependencies": {
"vitest": "^3.0.0"
},
diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json
index b6cde13..2076c42 100644
--- a/sdks/typescript/package.json
+++ b/sdks/typescript/package.json
@@ -16,6 +16,9 @@
"import": "./dist/index.js"
}
},
+ "dependencies": {
+ "@sandbox-agent/cli-shared": "workspace:*"
+ },
"files": [
"dist"
],
diff --git a/sdks/typescript/src/spawn.ts b/sdks/typescript/src/spawn.ts
index 5a48437..c879786 100644
--- a/sdks/typescript/src/spawn.ts
+++ b/sdks/typescript/src/spawn.ts
@@ -1,5 +1,9 @@
import type { ChildProcess } from "node:child_process";
import type { AddressInfo } from "node:net";
+import {
+ formatNonExecutableBinaryMessage,
+ isPermissionError,
+} from "@sandbox-agent/cli-shared";
export type SandboxAgentSpawnLogMode = "inherit" | "pipe" | "silent";
@@ -28,6 +32,9 @@ const PLATFORM_PACKAGES: Record = {
"win32-x64": "@sandbox-agent/cli-win32-x64",
};
+const TRUST_PACKAGES =
+ "@sandbox-agent/cli-linux-x64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64";
+
export function isNodeRuntime(): boolean {
return typeof process !== "undefined" && !!process.versions?.node;
}
@@ -66,6 +73,31 @@ export async function spawnSandboxAgent(
throw new Error("sandbox-agent binary not found. Install @sandbox-agent/cli or set SANDBOX_AGENT_BIN.");
}
+ if (process.platform !== "win32") {
+ try {
+ fs.accessSync(binaryPath, fs.constants.X_OK);
+ } catch (error) {
+ if (isPermissionError(error)) {
+ throw new Error(
+ formatNonExecutableBinaryMessage({
+ binPath: binaryPath,
+ trustPackages: TRUST_PACKAGES,
+ bunInstallBlocks: [
+ {
+ label: "Project install",
+ commands: [
+ `bun pm trust ${TRUST_PACKAGES}`,
+ "bun add sandbox-agent",
+ ],
+ },
+ ],
+ }),
+ );
+ }
+ throw error;
+ }
+ }
+
const stdio = logMode === "inherit" ? "inherit" : logMode === "silent" ? "ignore" : "pipe";
const args = ["server", "--host", bindHost, "--port", String(port), "--token", token];
const child = spawn(binaryPath, args, {