Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 237 additions & 0 deletions packages/vercel-sandbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,243 @@ Sandbox runs sudo in the following configuration:
Both these images are based on Amazon Linux 2023. The full package list is
available [here](https://docs.aws.amazon.com/linux/al2023/release-notes/all-packages-AL2023.7.html).

## Multi-user

Sandboxes support creating isolated Linux users with their own home directories,
file permissions, and process ownership. This is useful for multi-agent workflows
where each agent needs its own workspace, or for simulating multi-user
environments.

### Creating users

```typescript
import { Sandbox } from "@vercel/sandbox";

const sandbox = await Sandbox.create();

// Creates /home/alice with isolated permissions
const alice = await sandbox.createUser("alice");

alice.username; // "alice"
alice.homeDir; // "/home/alice"
```

`createUser` sets up:

- A Linux user with `/bin/bash` as the default shell
- A home directory at `/home/<username>` group-owned by `vercel-sandbox` with `770` permissions

### Running commands as a user

All commands run as the user by default, with the working directory set to their
home:

```typescript
const alice = await sandbox.createUser("alice");

const whoami = await alice.runCommand("whoami");
await whoami.stdout(); // "alice\n"

const pwd = await alice.runCommand("pwd");
await pwd.stdout(); // "/home/alice\n"
```

You can pass environment variables, override the working directory, or use the
full `RunCommandParams` interface:

```typescript
// Environment variables
await alice.runCommand({
cmd: "node",
args: ["-e", "console.log(process.env.API_KEY)"],
env: { API_KEY: "secret" },
});

// Custom working directory
await alice.runCommand({ cmd: "ls", cwd: "/tmp" });

// Detached mode for long-running processes
const server = await alice.runCommand({
cmd: "node",
args: ["server.js"],
detached: true,
});
```

To escalate to root, pass `sudo: true`:

```typescript
await alice.runCommand({
cmd: "dnf",
args: ["install", "-y", "git"],
sudo: true,
});
```

### File operations

`writeFiles`, `readFile`, `readFileToBuffer`, and `mkDir` all resolve relative
paths against the user's home directory. Written files are owned by the user:

```typescript
const alice = await sandbox.createUser("alice");

// Writes to /home/alice/app.js, owned by alice:alice
await alice.writeFiles([
{ path: "app.js", content: Buffer.from('console.log("hi")') },
]);

// Read it back
const buf = await alice.readFileToBuffer({ path: "app.js" });
buf?.toString(); // 'console.log("hi")'

// Stream reads
const stream = await alice.readFile({ path: "app.js" });

// Create directories owned by the user
await alice.mkDir("projects/my-app");

// Absolute paths also work
await alice.writeFiles([
{ path: "/tmp/output.txt", content: Buffer.from("data") },
]);
```

### File isolation

Users cannot access each other's home directories:

```typescript
const alice = await sandbox.createUser("alice");
const bob = await sandbox.createUser("bob");

await alice.writeFiles([
{ path: "secret.txt", content: Buffer.from("alice only") },
]);

// Bob cannot read, list, or write to alice's home
const cat = await bob.runCommand({
cmd: "cat",
args: ["/home/alice/secret.txt"],
});
cat.exitCode; // non-zero — Permission denied
```

**The SDK can read all users' files** because home directories are group-owned
by `vercel-sandbox`. Both `SandboxUser` methods and direct `sandbox` methods
work:

```typescript
// Via SandboxUser (relative paths resolve to home dir)
const buf = await alice.readFileToBuffer({ path: "secret.txt" });
buf?.toString(); // "alice only"

// Via sandbox directly (absolute path required)
const buf2 = await sandbox.readFileToBuffer({ path: "/home/alice/secret.txt" });
buf2?.toString(); // "alice only"
```

### Groups and shared directories

Create groups to let users collaborate through a shared directory:

```typescript
const devs = await sandbox.createGroup("devs");
devs.sharedDir; // "/shared/devs"

await sandbox.addUserToGroup("alice", "devs");
await sandbox.addUserToGroup("bob", "devs");

// Alice writes to the shared directory
await alice.runCommand({
cmd: "bash",
args: ["-c", 'echo "spec v2" > /shared/devs/spec.txt'],
});

// Bob can read it — files inherit group ownership via setgid
const spec = await bob.runCommand({
cmd: "cat",
args: ["/shared/devs/spec.txt"],
});
await spec.stdout(); // "spec v2\n"

// Non-members are blocked
const charlie = await sandbox.createUser("charlie");
const ls = await charlie.runCommand({ cmd: "ls", args: ["/shared/devs"] });
ls.exitCode; // non-zero — Permission denied
```

Shared directories use setgid (`2770`), so files created inside them
automatically inherit the group. All group members get read/write access.

Convenience methods are available on `SandboxUser`:

```typescript
await alice.addToGroup("devs");
await alice.removeFromGroup("devs");
```

### Using `asUser` for existing users

If a user already exists (e.g., from a snapshot or manual creation), use
`asUser` to get a handle without re-creating:

```typescript
const existing = sandbox.asUser("bob");
await existing.runCommand("whoami"); // "bob"
```

### Username validation

Usernames and group names must match `/^[a-z_][a-z0-9_-]*$/` and be at most 32
characters. Invalid names throw an error immediately:

```typescript
sandbox.asUser("Alice"); // throws — uppercase
sandbox.asUser("user name"); // throws — space
sandbox.asUser("$(whoami)"); // throws — special characters
sandbox.asUser("a".repeat(33)); // throws — too long
```

### Multi-agent example

```typescript
const sandbox = await Sandbox.create();

// Each agent gets its own isolated workspace
const researcher = await sandbox.createUser("researcher");
const coder = await sandbox.createUser("coder");
const reviewer = await sandbox.createUser("reviewer");

// Shared workspace for collaboration
await sandbox.createGroup("project");
await sandbox.addUserToGroup("researcher", "project");
await sandbox.addUserToGroup("coder", "project");
await sandbox.addUserToGroup("reviewer", "project");

// Researcher writes findings to shared dir
await researcher.runCommand({
cmd: "bash",
args: ["-c", 'echo "API spec v2" > /shared/project/spec.txt'],
});

// Coder reads spec, writes code in their own home
const spec = await coder.runCommand({
cmd: "cat",
args: ["/shared/project/spec.txt"],
});
await coder.writeFiles([
{ path: "app.js", content: Buffer.from(`// ${await spec.stdout()}`) },
]);

// Reviewer can read the shared spec but not coder's private files
const blocked = await reviewer.runCommand({
cmd: "cat",
args: ["/home/coder/app.js"],
});
blocked.exitCode; // non-zero — isolation enforced
```

[create-token]: https://vercel.com/account/settings/tokens
[hive]: https://vercel.com/blog/a-deep-dive-into-hive-vercels-builds-infrastructure
[al-2023-packages]: https://docs.aws.amazon.com/linux/al2023/release-notes/all-packages-AL2023.7.html
Expand Down
1 change: 1 addition & 0 deletions packages/vercel-sandbox/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export {
type NetworkPolicyRule,
type NetworkTransformer,
} from "./sandbox.js";
export { SandboxUser } from "./sandbox-user.js";
export { Snapshot } from "./snapshot.js";
export { Command, CommandFinished } from "./command.js";
export { StreamError } from "./api-client/api-error.js";
Expand Down
Loading
Loading