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
56 changes: 56 additions & 0 deletions .github/workflows/test-rust-sdk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Test Rust SDK

on:
push:
branches: [main]
paths:
- 'sdks/rust/**'
- 'examples/rust/**'
- '.github/workflows/test-rust-sdk.yml'
pull_request:
paths:
- 'sdks/rust/**'
- 'examples/rust/**'
- '.github/workflows/test-rust-sdk.yml'
workflow_dispatch:

jobs:
build-and-test:
name: Build & Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy

- uses: Swatinem/rust-cache@v2
with:
workspaces: |
sdks/rust
examples/rust

- name: Format check (SDK)
working-directory: sdks/rust
run: cargo fmt --all -- --check

- name: Format check (examples)
working-directory: examples/rust
run: cargo fmt --all -- --check

- name: Clippy
working-directory: sdks/rust
run: cargo clippy --all-targets -- -D warnings

- name: Build SDK (lib + examples + tests)
working-directory: sdks/rust
run: cargo build --all-targets

- name: Build examples/rust
working-directory: examples/rust
run: cargo build

- name: Offline tests
working-directory: sdks/rust
run: cargo test --tests
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ truth they come from.
- `proto/` — inter-tier contracts
- `docs/mint.json` — docs navigation
- `cmd/oc/` — CLI entrypoint
- `sdks/typescript/` and `sdks/python/` — published SDKs
- `sdks/typescript/`, `sdks/python/`, and `sdks/rust/` — published SDKs

Managed-agent product behavior is mostly **not** implemented here:

Expand Down Expand Up @@ -132,7 +132,7 @@ you are editing:

- `proto/` — contracts between tiers
- public HTTP API routes in `internal/api/`
- `sdks/` — published TypeScript and Python SDKs
- `sdks/` — published TypeScript, Python, and Rust SDKs
- `cmd/oc/` — CLI behavior users script against
- `docs/` — user-facing product and API documentation

Expand Down
34 changes: 31 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ oc config set api-key YOUR_API_KEY
Install the SDK:

```bash
npm install @opencomputer/sdk
# or
pip install opencomputer-sdk
npm install @opencomputer/sdk # TypeScript
pip install opencomputer-sdk # Python
cargo add opencomputer tokio --features tokio/full # Rust
```

```typescript
Expand All @@ -62,6 +62,34 @@ console.log(output.stdout); // hello
await sandbox.kill();
```

```rust
use opencomputer::{RunOpts, Sandbox, SandboxOpts};

#[tokio::main]
async fn main() -> opencomputer::Result<()> {
let sandbox = Sandbox::create(SandboxOpts::new().template("default")).await?;

let result = sandbox
.commands()
.run("node --version", RunOpts::new())
.await?;
println!("{}", result.stdout);

sandbox
.files()
.write("/app/index.js", "console.log(\"hello\")")
.await?;
let output = sandbox
.commands()
.run("node /app/index.js", RunOpts::new())
.await?;
println!("{}", output.stdout); // hello

sandbox.kill().await?;
Ok(())
}
```

### Agent SDK

Run a full Claude agent session inside the VM with real-time event streaming:
Expand Down
9 changes: 9 additions & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,15 @@
"reference/python-sdk/secrets"
]
},
{
"group": "Rust SDK",
"pages": [
"reference/rust-sdk/overview",
"reference/rust-sdk/sandbox",
"reference/rust-sdk/exec",
"reference/rust-sdk/filesystem"
]
},
{
"group": "CLI Reference",
"pages": [
Expand Down
22 changes: 22 additions & 0 deletions docs/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ npm install @opencomputer/sdk
pip install opencomputer-sdk
```

```toml cargo
# In Cargo.toml
opencomputer = "0.1"
tokio = { version = "1", features = ["full"] }
```

```bash CLI
# Installs to ~/.local/bin (no sudo). See cli/overview for manual install.
curl -fsSL https://raw.githubusercontent.com/diggerhq/opencomputer/main/scripts/install.sh | bash
Expand Down Expand Up @@ -71,6 +77,22 @@ async def main():
asyncio.run(main())
```

```rust Rust
use opencomputer::{RunOpts, Sandbox, SandboxOpts};

#[tokio::main]
async fn main() -> opencomputer::Result<()> {
let sandbox = Sandbox::create(SandboxOpts::new()).await?;
let result = sandbox
.commands()
.run("echo 'Hello World from OpenSandbox!'", RunOpts::new())
.await?;
println!("{}", result.stdout);
sandbox.kill().await?;
Ok(())
}
```

</CodeGroup>

## Next Steps
Expand Down
19 changes: 19 additions & 0 deletions docs/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,25 @@ async def main():
asyncio.run(main())
```

```rust Rust
use opencomputer::{RunOpts, Sandbox, SandboxOpts};

#[tokio::main]
async fn main() -> opencomputer::Result<()> {
let sandbox = Sandbox::create(SandboxOpts::new()).await?;

// Run a command inside the sandbox
let result = sandbox
.commands()
.run("echo 'Hello World from OpenSandbox!'", RunOpts::new())
.await?;
println!("{}", result.stdout);

sandbox.kill().await?;
Ok(())
}
```

</CodeGroup>

Running this will print:
Expand Down
147 changes: 147 additions & 0 deletions docs/reference/rust-sdk/exec.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
---
title: "Exec"
description: "Run commands and stream output"
---

`sandbox.commands()` and `sandbox.exec()` return the same `Exec` handle —
the names are kept for parity with the TypeScript and Python SDKs.

## `exec.run(command, opts: RunOpts) -> Result<ProcessResult>`

Run a shell command and wait for completion. Executed via `sh -c`, so pipes,
redirects, and shell expansion work.

```rust
use opencomputer::RunOpts;

let result = sandbox
.commands()
.run(
"npm run build",
RunOpts::new()
.cwd("/app")
.env("NODE_ENV", "production")
.timeout(120),
)
.await?;

if result.exit_code != 0 {
eprintln!("Build failed: {}", result.stderr);
}
```

### `RunOpts`

| Builder method | Type | Default | Description |
| --------------------- | ------- | ------- | -------------------------------------- |
| `.timeout(secs)` | `u64` | `60` | Timeout in seconds |
| `.env(key, value)` | `&str`, `&str` | — | Inject one env var; chainable |
| `.envs(map)` | `HashMap<String,String>` | — | Replace the env-var map |
| `.cwd(path)` | `&str` / `String` | — | Working directory |

### `ProcessResult`

| Field | Type | Description |
| ------------- | -------- | --------------- |
| `exit_code` | `i32` | Exit code |
| `stdout` | `String` | Captured stdout |
| `stderr` | `String` | Captured stderr |

## `exec.start(command, opts: ExecStartOpts)`

Start a long-running command and attach for streaming I/O. Returns
`(ExecSession, mpsc::UnboundedReceiver<StreamEvent>)`. Drain the receiver
to consume stdout / stderr / exit / scrollback events.

`exec.background(...)` is an alias.

```rust
use opencomputer::{ExecStartOpts, StreamEvent};

let (session, mut events) = sandbox
.exec()
.start(
"node",
ExecStartOpts::new()
.args(vec!["server.js".into()])
.cwd("/app")
.env("PORT", "3000"),
)
.await?;

tokio::spawn(async move {
while let Some(ev) = events.recv().await {
match ev {
StreamEvent::Stdout(b) => print!("{}", String::from_utf8_lossy(&b)),
StreamEvent::Stderr(b) => eprint!("{}", String::from_utf8_lossy(&b)),
StreamEvent::ScrollbackEnd => eprintln!("--- live output ---"),
StreamEvent::Exit(code) => {
eprintln!("exited: {code}");
break;
}
}
}
});

session.send_stdin("hello\n");
let code = session.done().await;
```

### `ExecStartOpts`

| Builder method | Type | Description |
| ------------------------------------ | --------- | ------------------------------------------- |
| `.args(vec)` | `Vec<String>` | Command arguments |
| `.env(key, value)` / `.envs(map)` | — | Environment variables |
| `.cwd(path)` | `&str` / `String` | Working directory |
| `.timeout(secs)` | `u64` | Timeout in seconds |
| `max_run_after_disconnect` | `u64` | Seconds to keep running after disconnect |

### `ExecSession`

| Member | Description |
| ---------------------------------- | -------------------------------------------------------- |
| `.session_id` | `String` — exec session ID |
| `.sandbox_id` | `String` |
| `.done().await` | Wait for the process to exit; returns the exit code |
| `.send_stdin(data)` | Write to the process stdin |
| `.kill(signal: Option<i32>).await` | Kill the process. Default signal is `SIGKILL` (9) |
| `.close().await` | Detach from the WebSocket without killing the process |

### `StreamEvent`

| Variant | Payload | Description |
| ---------------------- | ----------- | ---------------------------------------------- |
| `Stdout(Vec<u8>)` | bytes | Stdout chunk |
| `Stderr(Vec<u8>)` | bytes | Stderr chunk |
| `ScrollbackEnd` | — | End of historical replay; live output begins |
| `Exit(i32)` | exit code | Always the last event |

## Managing sessions

```rust
let sessions = sandbox.exec().list().await?;
for s in sessions {
let status = if s.running {
"running".to_string()
} else {
format!("exited ({:?})", s.exit_code)
};
println!("{} {} clients={}", s.session_id, status, s.attached_clients);
}

sandbox.exec().kill(&session_id, Some(15)).await?; // SIGTERM
```

## `exec.attach(session_id)`

Re-attach to a running exec session. Same return type as `start()`. Server
replays scrollback first, sends a `StreamEvent::ScrollbackEnd`, then streams
live output.

<Tip>
CLI equivalent: [`oc exec`](/cli/exec).
Other SDKs: [TypeScript](/reference/typescript-sdk/exec) ·
[Python](/reference/python-sdk/exec) ·
[HTTP API](/api-reference/exec/run).
</Tip>
Loading
Loading