Back to Microsandbox

Commands

docs/sandboxes/commands.mdx

0.5.18.2 KB
Original Source

Command execution doesn't use SSH or the network. There's a dedicated channel between the host and a guest agent running inside the VM, completely separate from the sandbox's network stack. The agent spawns processes, streams output back, and reports exit codes.

One nice consequence: exec works even when a sandbox has networking fully disabled.

Execute a command

Run a command and wait for it to complete. You get back the exit code, stdout, and stderr.

<CodeGroup> ```rust Rust let output = sb.exec("python", ["-c", "print('hello')"]).await?; println!("{}", output.stdout()?); // "hello\n" println!("{}", output.status().code); // 0 ```
typescript
const output = await sb.exec("python", ["-c", "print('hello')"]);
console.log(output.stdout()); // "hello\n"
console.log(output.success);  // true
console.log(output.code);     // 0
python
output = await sb.exec("python", ["-c", "print('hello')"])
print(output.stdout_text)     # "hello\n"
print(output.success)         # True
print(output.exit_code)       # 0
go
output, err := sb.Exec(ctx, "python", []string{"-c", "print('hello')"})
fmt.Print(output.Stdout())     // "hello\n"
fmt.Println(output.Success())  // true
fmt.Println(output.ExitCode()) // 0
bash
msb exec worker -- python -c "print('hello')"
</CodeGroup>

Execution options

These options apply to a single execution and don't change the sandbox's defaults.

<CodeGroup> ```rust Rust let output = sb.exec_with("python", |e| e .args(["compute.py"]) .cwd("/app") .env("PYTHONPATH", "/app/lib") .timeout(Duration::from_secs(30)) .rlimit(RlimitResource::Nofile, 1024) ).await?; ```
typescript
const output = await sb.execWith("python", (e) =>
    e.args(["compute.py"])
        .cwd("/app")
        .env("PYTHONPATH", "/app/lib")
        .timeout(30_000)
        .rlimit("nofile", 1024),
);
python
from microsandbox import Rlimit

output = await sb.exec(
    "python",
    ["compute.py"],
    cwd="/app",
    env={"PYTHONPATH": "/app/lib"},
    timeout=30.0,
    rlimits=[Rlimit.nofile(1024)],
)
go
output, err := sb.Exec(ctx, "python", []string{"compute.py"},
    m.WithExecCwd("/app"),
    m.WithExecEnv(map[string]string{"PYTHONPATH": "/app/lib"}),
    m.WithExecTimeout(30*time.Second),
)
bash
msb exec worker \
  -w /app \
  -e PYTHONPATH=/app/lib \
  --timeout 30s \
  --rlimit nofile=1024 \
  -- python compute.py
</CodeGroup>

Shell commands

Run a command through the sandbox's configured shell (defaults to /bin/sh). Useful for pipelines, redirects, and other shell syntax that exec doesn't interpret.

<CodeGroup> ```rust Rust let output = sb.shell("ls -la /app && echo done").await?; ```
typescript
const output = await sb.shell("ls -la /app && echo done")
python
output = await sb.shell("ls -la /app && echo done")
go
output, err := sb.Shell(ctx, "ls -la /app && echo done")
bash
msb exec worker -- sh -c "ls -la /app && echo done"
</CodeGroup>

Stream output

For long-running processes or large output, streaming gives you stdout, stderr, and exit events as they happen instead of buffering everything until the command finishes.

<CodeGroup> ```rust Rust let mut handle = sb.exec_stream("tail", ["-f", "/var/log/app.log"]).await?;

while let Some(event) = handle.recv().await { match event { ExecEvent::Stdout(data) => print!("{}", String::from_utf8_lossy(&data)), ExecEvent::Stderr(data) => eprint!("{}", String::from_utf8_lossy(&data)), ExecEvent::Exited { code } => break, _ => {} } }


```typescript TypeScript
const handle = await sb.execStream("tail", ["-f", "/var/log/app.log"]);

for await (const event of handle) {
    switch (event.kind) {
        case "stdout": process.stdout.write(event.data); break;
        case "stderr": process.stderr.write(event.data); break;
        case "exited": console.log(`Exited: ${event.code}`); break;
    }
}
python
handle = await sb.exec_stream("tail", ["-f", "/var/log/app.log"])

async for event in handle:
    match event.event_type:
        case "stdout": print(event.data.decode(), end="")
        case "stderr": print(event.data.decode(), end="", file=sys.stderr)
        case "exited": print(f"Exited: {event.code}")
go
handle, err := sb.ExecStream(ctx, "tail", []string{"-f", "/var/log/app.log"})
for {
    event, err := handle.Recv(ctx)
    if err != nil {
        return err
    }
    switch event.Kind {
    case m.ExecEventStdout:
        os.Stdout.Write(event.Data)
    case m.ExecEventStderr:
        os.Stderr.Write(event.Data)
    case m.ExecEventExited:
        fmt.Printf("Exited: %d\n", event.ExitCode)
    case m.ExecEventDone:
        return nil
    }
}
</CodeGroup>

Interactive attach

Bridges your terminal directly to a process inside the sandbox for a fully interactive PTY session. Useful for debugging, running REPLs, or anything that expects a real terminal.

<CodeGroup> ```rust Rust let exit_code = sb.attach_shell().await?;

let exit_code = sb.attach_with("python", |a| a .env("DEBUG", "1") .cwd("/app") .detach_keys("ctrl-q") ).await?;


```typescript TypeScript
// Attach to the default shell
const exitCode = await sb.attachShell();

// Attach to a specific command with custom detach keys
await sb.attachWith("bash", (a) =>
    a.env("DEBUG", "1").cwd("/app").detachKeys("ctrl-q"),
);
python
# Attach to the default shell
exit_code = await sb.attach_shell()

# Attach to a specific command with custom detach keys
exit_code = await sb.attach(
    "python",
    env={"DEBUG": "1"},
    cwd="/app",
    detach_keys="ctrl-q",
)
go
// Attach to the default shell.
exitCode, err := sb.AttachShell(ctx)
if err != nil {
    return err
}
fmt.Println("shell exited with", exitCode)

// Attach to a specific command.
exitCode, err = sb.Attach(ctx, "python")
if err != nil {
    return err
}
fmt.Println("python exited with", exitCode)
bash
# Attach to the default shell
msb exec -t worker

# Attach to a specific command
msb exec -t worker -e DEBUG=1 -w /app -- python
</CodeGroup> <Tip> Press `Ctrl+]` (or your configured detach keys) to detach from the session without stopping the process. The process keeps running inside the VM and you can reattach later via its session ID. </Tip>

Write stdin

Streaming handles can also accept stdin. Enable stdin_pipe and write bytes to it. Combined with tty: true, this lets you drive interactive processes programmatically.

<CodeGroup> ```rust Rust let mut handle = sb.exec_stream_with("python", |e| e.stdin_pipe().tty(true)).await?; let stdin = handle.take_stdin().unwrap(); stdin.write(b"print('hello')\n").await?; stdin.write(b"exit()\n").await?; handle.wait().await?; ```
typescript
const handle = await sb.execStreamWith("python", (e) => e.stdinPipe().tty(true));
const stdin = await handle.takeStdin();
await stdin!.write("print('hello')\n");
await stdin!.write("exit()\n");
await stdin!.close();
await handle.wait();
python
from microsandbox import Stdin

handle = await sb.exec_stream("python", stdin=Stdin.pipe(), tty=True)
stdin = handle.take_stdin()
await stdin.write(b"print('hello')\n")
await stdin.write(b"exit()\n")
await stdin.close()
await handle.wait()
go
handle, err := sb.ExecStream(ctx, "python", nil, m.WithExecStdinPipe())
stdin := handle.TakeStdin()
_, _ = stdin.Write([]byte("print('hello')\n"))
_, _ = stdin.Write([]byte("exit()\n"))
_ = stdin.Close()
_, err = handle.Wait(ctx)
</CodeGroup>

Session IDs

Each streaming exec creates a session ID that you can use to correlate stream events and persisted log entries. Session listing and reattach are coming soon.

<CodeGroup> ```rust Rust let handle = sb.exec_stream("python", ["server.py"]).await?; let session_id = handle.id().to_string(); ```
typescript
const handle = await sb.execStream("python", ["server.py"])
const sessionId = handle.id
python
handle = await sb.exec_stream("python", ["server.py"])
session_id = handle.id
go
handle, err := sb.ExecStream(ctx, "python", []string{"server.py"})
if err != nil {
    return err
}
sessionID, err := handle.ID()
</CodeGroup>