Skip to content
Merged
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
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- **Shell Emulation in Agent Exec API**: built-in shell commands for familiar terminal patterns inside sandbox sessions
- `POST /api/v1/sessions/:id/exec` accepts a new `command` field with a shell-style command line
- Built-ins: `echo`, `cat`, `ls`, `pwd`, `cd`, `mkdir` (`-p`), `rm` (`-r`/`-rf`), `cp`, `mv`, `env`, `export`
- Pipes (`|`), redirection (`>`, `>>`, `<`), and sequencing (`&&` short-circuit, `;`)
- Single and double quoted strings
- `command` takes precedence over `files`/`source`/`wasm_path` when present
- `export KEY=value` writes through to the session's `WasiEnv`, so exported variables persist across `command`/`exec` calls and are visible to subsequent WASM executions
- CWD scoped to a single shell invocation (each `command` request starts at `/`); `cd` mutates the in-invocation CWD so chains like `cd sub && pwd` work
- All shell file operations resolved through the session work_dir with path-traversal prevention
- All in-process built-ins — no subprocess execution, no access to the host shell or native binaries
- `execute_code` tool schema documents the `command` field for LLM agents
- **Multi-file JavaScript Project Execution**: agents can upload and run an entire JS project in a single exec request
- `POST /api/v1/sessions/:id/exec` accepts `files` (map of filename → content) and `entry` (entry filename)
- All files written to the session work_dir (with intermediate directories created) before execution
- Filename validation rejects empty names, absolute paths, and `..` traversal
- Dispatch order in `handle_exec`: `command` → `files` → `source` → `wasm_path`; language/entry validation runs synchronously so callers get HTTP 400 immediately
- Sibling files visible to the runtime via the preopened WASI directory (`require()` of siblings depends on runtime-side support — the wasmhub `nodejs` runtime v0.2.0 does not yet implement CommonJS `require()`)
- `execute_code` tool schema documents `files`/`entry` for LLM agents
- **JavaScript Source Execution in Agent Exec API**: run JS without precompiling to WASM
- `POST /api/v1/sessions/:id/exec` accepts `source` + `language` as an alternative to `wasm_path`
- Supported language aliases: `javascript`, `js`, `nodejs` — all map to the wasmhub nodejs runtime
- Unsupported languages (e.g. `python`) return HTTP 400 with a clear message, before any thread spawn or network I/O
- Runtime fetched from wasmhub via `runtime_cache` and cached after first download
- Source written to `_run_.js` in the session work_dir and executed with the runtime
- `source` without `language` defaults to JavaScript

### Changed
- Exec threads (both source and WASM execution paths) now run with a 64 MB stack via `std::thread::Builder::stack_size` — language runtimes like QuickJS generate deep call chains that overflow the default 8 MB stack
- wasmhub runtime renamed `quickjs` → `nodejs`; manifest URL pinned to v0.2.0
- Removed six WASI debug `eprintln!` calls that leaked to host stderr

## [0.19.0](https://github.com/anistark/wasmrun/releases/tag/v0.19.0) - 2026-05-20

### Added
- **Agent Tool Schemas for LLM Agents**: Function-calling definitions for AI agent integration
- `GET /api/v1/tools` — returns tool definitions for LLM function calling
Expand Down
20 changes: 16 additions & 4 deletions docs/docs/exec/agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ The agent API manages **sessions** — each session is an isolated exec mode san
- **Output buffers** — stdout/stderr captured per execution
- **Timeout** — auto-cleanup after idle expiry

When you call the exec endpoint, the server loads the WASM file from the session's filesystem, runs it through the same interpreter used by `wasmrun exec`, and returns the captured output as JSON.
The exec endpoint accepts four input modes — a shell command line, a JavaScript source snippet, a multi-file JS project, or a pre-compiled `.wasm` file — and returns captured stdout/stderr/exit code as JSON. JavaScript runs through a wasmhub-hosted language runtime; WASM modules run through the same interpreter used by `wasmrun exec`. Shell commands are handled by an in-process built-in shell with no subprocess or host shell access.

```
┌─ wasmrun agent ─────────────────────────────────────────┐
Expand Down Expand Up @@ -66,12 +66,22 @@ wasmrun agent --port 8430
curl -X POST http://localhost:8430/api/v1/sessions
# → {"session_id": "a1b2c3...", "created_at": "..."}

# Upload a WASM file
# Run a shell command in the session
curl -X POST http://localhost:8430/api/v1/sessions/a1b2c3.../exec \
-H "Content-Type: application/json" \
-d '{"command": "echo hello > out.txt && cat out.txt"}'
# → {"stdout": "hello\n", "stderr": "", "exit_code": 0, ...}

# Or run JavaScript inline
curl -X POST http://localhost:8430/api/v1/sessions/a1b2c3.../exec \
-H "Content-Type: application/json" \
-d '{"source": "console.log(1+1)", "language": "javascript"}'
# → {"stdout": "2\n", "exit_code": 0, ...}

# Or run a pre-compiled WASM file
curl -X POST http://localhost:8430/api/v1/sessions/a1b2c3.../files \
-H "Content-Type: application/json" \
-d '{"path": "hello.wasm", "content": "..."}'

# Execute it
curl -X POST http://localhost:8430/api/v1/sessions/a1b2c3.../exec \
-H "Content-Type: application/json" \
-d '{"wasm_path": "hello.wasm"}'
Expand All @@ -81,6 +91,8 @@ curl -X POST http://localhost:8430/api/v1/sessions/a1b2c3.../exec \
curl -X DELETE http://localhost:8430/api/v1/sessions/a1b2c3...
```

See the [Agent Execution](./usage/agent-exec.md) reference for all four input modes (shell `command`, JS `source`, multi-file `files`+`entry`, `wasm_path`).

## Tool Schemas for LLM Agents

The server exposes tool definitions that can be passed directly to OpenAI or Anthropic APIs for function calling:
Expand Down
173 changes: 133 additions & 40 deletions docs/docs/exec/usage/agent-exec.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,34 @@ sidebar_position: 6
title: Agent Execution
---

# Agent WASM Execution
# Agent Execution

Execute a `.wasm` file within a session's sandbox.
Run code inside a session's sandbox. The exec endpoint accepts four mutually exclusive input modes:

## Execute WASM
| Mode | Request field(s) | Use when |
|------|------------------|----------|
| Shell command | `command` | You want a familiar terminal-style one-liner over the session FS |
| JavaScript source | `source` + `language` | You have a single JS snippet to evaluate |
| Multi-file JS project | `files` + `entry` (+ `language`) | You have several source files that need to live on disk together |
| Pre-compiled WASM | `wasm_path` (+ `function`, `args`) | You already have a `.wasm` file in the session FS |

If more than one is provided, dispatch follows that priority order (`command` → `files` → `source` → `wasm_path`).

```
POST /api/v1/sessions/:id/exec
```

**Request body:**
```json
{
"wasm_path": "hello.wasm",
"function": "_start",
"args": ["arg1", "arg2"],
"timeout": 30,
"env": {
"MY_VAR": "value"
}
}
```
## Common Fields

These apply to every mode:

| Field | Required | Default | Description |
|-------|----------|---------|-------------|
| `wasm_path` | yes | — | Path to `.wasm` file relative to session root |
| `function` | no | auto-detect | Exported function to call (defaults to `_start`, `main`, or start section) |
| `args` | no | `[]` | Arguments passed to the WASM program |
| `timeout` | no | `30` | Execution timeout in seconds |
| `env` | no | `{}` | Environment variables to set before execution |

**Response** (200):
## Common Response

```json
{
"stdout": "Hello, World!\n",
Expand All @@ -56,6 +52,117 @@ If execution fails (parse error, trap, etc.), the response still returns 200 wit
}
```

Output buffers are cleared between calls — each response contains only the output of that invocation.

---

## Shell Command

Run a built-in shell command line against the session filesystem. No language runtime, no WASM module, no subprocess.

```json
{
"command": "echo hello > out.txt && cat out.txt"
}
```

**Supported built-ins:** `echo`, `cat`, `ls`, `pwd`, `cd`, `mkdir` (`-p`), `rm` (`-r`/`-rf`), `cp`, `mv`, `env`, `export`.

**Operators:** pipes (`|`), redirection (`>`, `>>`, `<`), sequencing (`&&` short-circuit on failure, `;` always continue), single and double quoted strings.

**Scope:**
- CWD starts at `/` for every request. `cd` mutates the in-invocation CWD, so chains like `cd sub && pwd && ls` work, but the CWD is not carried across requests.
- `export KEY=value` writes through to the session's environment, so the variable is visible to subsequent `command`/`source`/`files`/`wasm_path` executions in the same session.
- Path traversal that escapes the session root is rejected.

Unknown commands return exit code `127` with `command not found` on stderr — there is no fallback to the host shell.

**Example:**
```sh
curl -X POST .../exec -H "Content-Type: application/json" -d '{
"command": "mkdir -p logs && echo started > logs/run.log && ls logs"
}'
# → {"stdout": "run.log\n", "stderr": "", "exit_code": 0, ...}
```

---

## JavaScript Source

Evaluate a single source string with the wasmhub JavaScript runtime. The runtime is fetched once and cached.

```json
{
"source": "console.log(1 + 1)",
"language": "javascript"
}
```

| Field | Required | Default | Description |
|-------|----------|---------|-------------|
| `source` | yes | — | Source code to execute |
| `language` | no | `javascript` | One of `javascript`, `js`, `nodejs` |

Unsupported languages return HTTP `400` with a clear message before any thread is spawned.

**Example:**
```sh
curl -X POST .../exec -H "Content-Type: application/json" -d '{
"source": "console.log(1+1)",
"language": "javascript"
}'
# → {"stdout": "2\n", "exit_code": 0, ...}
```

---

## Multi-file JavaScript Project

Upload an entire project in one request. All files are written to the session root (creating intermediate directories) and the entry file is run.

```json
{
"files": {
"main.js": "console.log('hi');",
"lib/util.js": "exports.greet = n => 'hi ' + n;"
},
"entry": "main.js",
"language": "javascript"
}
```

| Field | Required | Default | Description |
|-------|----------|---------|-------------|
| `files` | yes | — | Map of filename → file content. Filenames must be relative and free of `..` |
| `entry` | yes | — | Entry filename; must be a key in `files` |
| `language` | no | `javascript` | One of `javascript`, `js`, `nodejs` |

Validation (missing entry, unknown language, absolute/traversal paths) runs synchronously and returns HTTP `400` immediately.

Sibling files are visible to the runtime via the preopened WASI directory, but loading them from JS requires runtime-side support. The wasmhub `nodejs` runtime (v0.2.0) does not yet implement CommonJS `require()` — files can be uploaded today and will become loadable once the runtime ships that.

---

## Pre-compiled WASM

Execute a `.wasm` file already present in the session filesystem.

```json
{
"wasm_path": "hello.wasm",
"function": "_start",
"args": ["arg1", "arg2"]
}
```

| Field | Required | Default | Description |
|-------|----------|---------|-------------|
| `wasm_path` | yes | — | Path to `.wasm` file relative to session root |
| `function` | no | auto-detect | Exported function to call (defaults to `_start`, `main`, or start section) |
| `args` | no | `[]` | Arguments passed to the WASM program |

---

## Timeout

If execution exceeds the timeout, the response includes:
Expand All @@ -70,27 +177,13 @@ If execution exceeds the timeout, the response includes:
}
```

## Multiple Executions

A session supports multiple sequential executions. Output buffers are cleared between each call — you always get only the output from the current execution.

```sh
# First exec
curl -X POST .../exec -d '{"wasm_path": "a.wasm"}'
# → {"stdout": "output from a", ...}

# Second exec (does NOT include output from a)
curl -X POST .../exec -d '{"wasm_path": "b.wasm"}'
# → {"stdout": "output from b", ...}
```

## Workflow

A typical agent workflow:
A typical agent loop:

1. Create session
2. Write `.wasm` file via file upload endpoint
3. Execute it via `/exec`
4. Read the structured response
5. Optionally run more executions
6. Destroy session when done
1. Create a session.
2. Either upload files explicitly (file endpoints) or pass them inline via `files`/`source`/`command`.
3. Execute via `/exec`.
4. Read the structured response.
5. Optionally run more executions in the same session — the filesystem and exported env vars persist.
6. Destroy the session when done.
5 changes: 5 additions & 0 deletions src/agent/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ pub struct ExecRequest {
pub entry: Option<String>,
/// Language for source execution: "javascript", "js", or "nodejs".
pub language: Option<String>,
/// Shell command line to execute via the built-in shell emulator.
/// Supports pipes (`|`), redirection (`>`, `>>`, `<`), and sequencing
/// (`&&`, `;`) with built-ins for common file/env operations.
/// Takes precedence over `files`/`source`/`wasm_path` when present.
pub command: Option<String>,
pub function: Option<String>,
#[serde(default)]
pub args: Vec<String>,
Expand Down
1 change: 1 addition & 0 deletions src/agent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ pub mod api;
pub mod executor;
pub mod server;
pub mod session;
pub mod shell;
pub mod tools;
Loading
Loading