chore: recover ottawa workspace state

This commit is contained in:
Nathan Flurry 2026-03-09 19:59:52 -07:00
parent 5d65013aa5
commit 25a31c8ab7
265 changed files with 1249 additions and 42839 deletions

View file

@ -0,0 +1,29 @@
# Sandbox Agent Runtime
You are running inside a Sandbox Agent runtime.
## Process Management — MANDATORY
IMPORTANT: You MUST use the Sandbox Agent process HTTP API instead of running
commands directly via the Bash tool for ALL of the following:
- Starting dev servers, build watchers, or any long-running background process
- Monitoring, stopping, or restarting processes
- Checking logs from background processes
Do NOT use the Bash tool with `run_in_background`, `&`, or `nohup` for these.
Do NOT run `npm run dev`, `python -m http.server`, or similar
commands directly in Bash. Instead, use curl to call the process HTTP API.
One-shot commands like `npm run build`, `npm test`, `make`, etc. that run to
completion should be run directly with the Bash tool as normal.
Use the `sandbox-agent-processes` skill (auto-loaded) for the full API reference.
Quick reference:
- **Start a managed process:** `curl -s -X POST $SANDBOX_AGENT_URL/v1/processes -H "Content-Type: application/json" -d '{"command":"npm","args":["run","dev"]}'`
- **Check logs:** `curl -s "$SANDBOX_AGENT_URL/v1/processes/proc_1/logs?tail=20"`
- **Stop a process:** `curl -s -X POST $SANDBOX_AGENT_URL/v1/processes/proc_1/stop`
The only exception is simple, quick inline commands (e.g., `ls`, `cat`, `echo`,
`git status`) that do not need lifecycle management.

View file

@ -0,0 +1,333 @@
---
name: sandbox-agent-processes
description: >-
Start, stop, and monitor long-lived processes (dev servers, builds, watchers)
and run one-shot commands inside the sandbox via the Sandbox Agent process HTTP API.
TRIGGER when: user asks to start a dev server, run a build watcher,
manage background processes, check process logs, or needs a long-running command
that outlives a single shell execution.
DO NOT TRIGGER when: user wants to run a simple inline shell command with the Bash tool.
user-invocable: false
---
# Sandbox Agent Process Management
You are running inside a Sandbox Agent runtime. Use the process HTTP API
documented below to manage processes. All endpoints are under `/v1/processes`.
The server URL is `$SANDBOX_AGENT_URL`. Use it as the base for all curl
commands (e.g., `curl -s $SANDBOX_AGENT_URL/v1/processes`).
All curl commands below should be run via the Bash tool.
---
## Run a one-shot command
Execute a command to completion and get stdout/stderr/exit code.
```bash
curl -s -X POST $SANDBOX_AGENT_URL/v1/processes/run \
-H "Content-Type: application/json" \
-d '{
"command": "make",
"args": ["build"],
"cwd": "/workspace",
"timeoutMs": 60000
}'
```
**Request fields:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `command` | string | yes | Program to execute |
| `args` | string[] | no | Command arguments |
| `cwd` | string | no | Working directory |
| `env` | object | no | Extra environment variables |
| `timeoutMs` | number | no | Timeout in ms (default: 30000) |
| `maxOutputBytes` | number | no | Cap on captured output (default: 1MB) |
**Response:**
```json
{
"exitCode": 0,
"timedOut": false,
"stdout": "...",
"stderr": "",
"stdoutTruncated": false,
"stderrTruncated": false,
"durationMs": 2345
}
```
---
## Create a managed process
Spawn a long-lived process (e.g., dev server) that persists until stopped.
```bash
curl -s -X POST $SANDBOX_AGENT_URL/v1/processes \
-H "Content-Type: application/json" \
-d '{
"command": "npm",
"args": ["run", "dev"],
"cwd": "/workspace"
}'
```
**Request fields:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `command` | string | yes | Program to execute |
| `args` | string[] | no | Command arguments |
| `cwd` | string | no | Working directory |
| `env` | object | no | Extra environment variables |
| `tty` | bool | no | Allocate a PTY (default: false) |
| `interactive` | bool | no | Mark as interactive (default: false) |
**Response** (`ProcessInfo`):
```json
{
"id": "proc_1",
"command": "npm",
"args": ["run", "dev"],
"cwd": "/workspace",
"tty": false,
"interactive": false,
"status": "running",
"pid": 12345,
"exitCode": null,
"createdAtMs": 1709866543221,
"exitedAtMs": null
}
```
---
## List processes
```bash
curl -s $SANDBOX_AGENT_URL/v1/processes
```
Returns `{ "processes": [ ...ProcessInfo ] }`.
---
## Get a single process
```bash
curl -s $SANDBOX_AGENT_URL/v1/processes/proc_1
```
Returns a `ProcessInfo` object. Check `status` (`"running"` or `"exited"`) and `exitCode`.
---
## Stop a process (SIGTERM)
```bash
curl -s -X POST $SANDBOX_AGENT_URL/v1/processes/proc_1/stop
```
Optionally wait for the process to exit:
```bash
curl -s -X POST "$SANDBOX_AGENT_URL/v1/processes/proc_1/stop?waitMs=5000"
```
Returns updated `ProcessInfo`.
---
## Kill a process (SIGKILL)
```bash
curl -s -X POST $SANDBOX_AGENT_URL/v1/processes/proc_1/kill
```
Same interface as stop. Use when SIGTERM is not sufficient.
---
## Delete a process
Remove a stopped process from the list. Fails with 409 if still running.
```bash
curl -s -X DELETE $SANDBOX_AGENT_URL/v1/processes/proc_1
```
Returns 204 on success.
---
## Fetch process logs
Get buffered log output from a managed process.
```bash
# All logs
curl -s $SANDBOX_AGENT_URL/v1/processes/proc_1/logs
# Last 50 entries
curl -s "$SANDBOX_AGENT_URL/v1/processes/proc_1/logs?tail=50"
# Only stdout
curl -s "$SANDBOX_AGENT_URL/v1/processes/proc_1/logs?stream=stdout"
```
**Query parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `stream` | string | `stdout`, `stderr`, `combined`, or `pty` |
| `tail` | number | Return only the last N entries |
| `since` | number | Only entries with sequence > this value |
| `follow` | bool | Stream live via SSE (see below) |
**Response:**
```json
{
"processId": "proc_1",
"stream": "combined",
"entries": [
{
"sequence": 1,
"stream": "stdout",
"timestampMs": 1709866543221,
"data": "aGVsbG8=",
"encoding": "base64"
}
]
}
```
Log entry `data` is base64-encoded. Decode it to read the output.
---
## Follow logs via SSE
Stream live log output using Server-Sent Events:
```bash
curl -s -N "$SANDBOX_AGENT_URL/v1/processes/proc_1/logs?follow=true"
```
Each SSE event has type `log` and contains a JSON `ProcessLogEntry` as data.
Use `since=<sequence>` to resume from a known position.
---
## Send input to a process
Write to a process's stdin (or PTY input for tty processes).
```bash
curl -s -X POST $SANDBOX_AGENT_URL/v1/processes/proc_1/input \
-H "Content-Type: application/json" \
-d '{"data": "echo hello\n", "encoding": "utf8"}'
```
**Request fields:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `data` | string | yes | Input data |
| `encoding` | string | no | `utf8`, `text`, or `base64` (default: `utf8`) |
Returns `{ "bytesWritten": 12 }`.
---
## Resize terminal (PTY only)
For processes started with `"tty": true`:
```bash
curl -s -X POST $SANDBOX_AGENT_URL/v1/processes/proc_1/terminal/resize \
-H "Content-Type: application/json" \
-d '{"cols": 120, "rows": 40}'
```
---
## Get/set runtime configuration
```bash
# Get current config
curl -s $SANDBOX_AGENT_URL/v1/processes/config
# Update config
curl -s -X POST $SANDBOX_AGENT_URL/v1/processes/config \
-H "Content-Type: application/json" \
-d '{
"maxConcurrentProcesses": 32,
"defaultRunTimeoutMs": 60000,
"maxRunTimeoutMs": 300000,
"maxOutputBytes": 2097152,
"maxLogBytesPerProcess": 10485760,
"maxInputBytesPerRequest": 65536
}'
```
---
## Common patterns
### Start a dev server and check it's running
```bash
curl -s -X POST $SANDBOX_AGENT_URL/v1/processes \
-H "Content-Type: application/json" \
-d '{"command":"npm","args":["run","dev"],"cwd":"/workspace"}'
# Wait a moment, then check logs for startup confirmation
curl -s "$SANDBOX_AGENT_URL/v1/processes/proc_1/logs?tail=20"
```
### Run a build and check for errors
```bash
curl -s -X POST $SANDBOX_AGENT_URL/v1/processes/run \
-H "Content-Type: application/json" \
-d '{"command":"npm","args":["run","build"],"cwd":"/workspace","timeoutMs":120000}'
```
Check `exitCode` in the response. Non-zero means failure — read `stderr` for details.
### Stop and clean up a process
```bash
curl -s -X POST "$SANDBOX_AGENT_URL/v1/processes/proc_1/stop?waitMs=5000"
curl -s -X DELETE $SANDBOX_AGENT_URL/v1/processes/proc_1
```
---
## Error responses
Errors use `application/problem+json`:
```json
{
"type": "about:blank",
"status": 409,
"title": "Conflict",
"detail": "max concurrent process limit reached (64)"
}
```
| Status | Meaning |
|--------|---------|
| 400 | Bad request (invalid encoding, malformed body) |
| 404 | Process not found |
| 409 | Conflict (process still running, or limit reached) |
| 413 | Input payload too large |
| 501 | Process API not supported on this platform |

View file

@ -26,6 +26,7 @@ struct AcpProxyRuntimeInner {
agent_manager: Arc<AgentManager>,
require_preinstall: bool,
request_timeout: Duration,
server_url: Option<String>,
instances: RwLock<HashMap<String, Arc<ProxyInstance>>>,
instance_locks: Mutex<HashMap<String, Arc<Mutex<()>>>>,
install_locks: Mutex<HashMap<AgentId, Arc<Mutex<()>>>>,
@ -56,7 +57,7 @@ pub type PinBoxSseStream =
std::pin::Pin<Box<dyn Stream<Item = Result<Event, std::convert::Infallible>> + Send>>;
impl AcpProxyRuntime {
pub fn new(agent_manager: Arc<AgentManager>) -> Self {
pub fn new(agent_manager: Arc<AgentManager>, server_url: Option<String>) -> Self {
let require_preinstall = std::env::var("SANDBOX_AGENT_REQUIRE_PREINSTALL")
.ok()
.is_some_and(|value| {
@ -76,6 +77,7 @@ impl AcpProxyRuntime {
agent_manager,
require_preinstall,
request_timeout,
server_url,
instances: RwLock::new(HashMap::new()),
instance_locks: Mutex::new(HashMap::new()),
install_locks: Mutex::new(HashMap::new()),
@ -313,12 +315,17 @@ impl AcpProxyRuntime {
"create_instance: launch spec resolved, spawning"
);
let mut env = launch.env;
if let Some(url) = &self.inner.server_url {
env.insert("SANDBOX_AGENT_URL".to_string(), url.clone());
}
let spawn_started = std::time::Instant::now();
let runtime = AdapterRuntime::start(
LaunchSpec {
program: launch.program,
args: launch.args,
env: launch.env,
env,
},
self.inner.request_timeout,
)

View file

@ -0,0 +1,162 @@
//! Built-in skills and CLAUDE.md instructions that are automatically written
//! to the agent's config directory when an agent instance is created.
//!
//! Each skill is embedded at compile time from `builtin-skills/` and written to
//! disk so the agent process can discover it. A global CLAUDE.md with behavioral
//! directives is also injected into `~/.claude/CLAUDE.md`.
//!
//! Set `SANDBOX_AGENT_SKIP_BUILTIN_SKILLS=1` to disable auto-injection.
use std::fs;
use std::path::PathBuf;
/// Embedded content of the sandbox-agent-processes skill.
const SANDBOX_AGENT_PROCESSES_SKILL: &str =
include_str!("../builtin-skills/sandbox-agent-processes/SKILL.md");
/// Embedded CLAUDE.md with behavioral directives for the agent.
const BUILTIN_CLAUDE_MD: &str = include_str!("../builtin-skills/CLAUDE.md");
/// Marker used to identify the sandbox-agent-managed section in CLAUDE.md.
const CLAUDE_MD_MARKER_START: &str = "<!-- BEGIN SANDBOX-AGENT-MANAGED -->";
const CLAUDE_MD_MARKER_END: &str = "<!-- END SANDBOX-AGENT-MANAGED -->";
/// Returns `true` if built-in skill injection is disabled via environment variable.
fn is_disabled() -> bool {
std::env::var("SANDBOX_AGENT_SKIP_BUILTIN_SKILLS")
.ok()
.is_some_and(|v| {
let t = v.trim();
t == "1" || t.eq_ignore_ascii_case("true") || t.eq_ignore_ascii_case("yes")
})
}
/// Writes all built-in skills and the global CLAUDE.md to the agent's personal
/// config directory (`~/.claude/`). No-op if `SANDBOX_AGENT_SKIP_BUILTIN_SKILLS=1`.
///
/// The `server_url` is substituted into the content so the agent knows which
/// endpoint to call.
pub fn write_builtin_skills(server_url: Option<&str>) {
if is_disabled() {
tracing::debug!("builtin skills: injection disabled via SANDBOX_AGENT_SKIP_BUILTIN_SKILLS");
return;
}
let Some(home) = dirs::home_dir() else {
tracing::warn!("builtin skills: cannot determine home directory, skipping");
return;
};
let claude_dir = home.join(".claude");
// Write skills
let skills_dir = claude_dir.join("skills");
write_skill(
&skills_dir,
"sandbox-agent-processes",
SANDBOX_AGENT_PROCESSES_SKILL,
server_url,
);
// Write/update CLAUDE.md with behavioral directives
write_claude_md(&claude_dir, server_url);
}
/// Writes a single skill's SKILL.md into `{skills_dir}/{name}/SKILL.md`,
/// creating directories as needed.
fn write_skill(skills_dir: &PathBuf, name: &str, content: &str, server_url: Option<&str>) {
let dir = skills_dir.join(name);
if let Err(err) = fs::create_dir_all(&dir) {
tracing::warn!(
skill = name,
error = %err,
"builtin skills: failed to create skill directory"
);
return;
}
let final_content = substitute_url(content, server_url);
let path = dir.join("SKILL.md");
match fs::write(&path, final_content) {
Ok(()) => {
tracing::info!(skill = name, path = %path.display(), "builtin skills: written");
}
Err(err) => {
tracing::warn!(
skill = name,
error = %err,
"builtin skills: failed to write SKILL.md"
);
}
}
}
/// Writes or updates the sandbox-agent-managed section of `~/.claude/CLAUDE.md`.
///
/// If the file already exists, only the section between the marker comments is
/// replaced. If no markers exist, the section is appended. If the file does not
/// exist, it is created.
fn write_claude_md(claude_dir: &PathBuf, server_url: Option<&str>) {
let path = claude_dir.join("CLAUDE.md");
let managed_block = format!(
"{}\n{}\n{}",
CLAUDE_MD_MARKER_START,
substitute_url(BUILTIN_CLAUDE_MD, server_url).trim(),
CLAUDE_MD_MARKER_END,
);
let new_content = match fs::read_to_string(&path) {
Ok(existing) => {
if let (Some(start), Some(end)) = (
existing.find(CLAUDE_MD_MARKER_START),
existing.find(CLAUDE_MD_MARKER_END),
) {
// Replace existing managed section
let before = existing[..start].trim_end();
let after = existing[end + CLAUDE_MD_MARKER_END.len()..].trim_start();
if before.is_empty() && after.is_empty() {
format!("{}\n", managed_block)
} else if before.is_empty() {
format!("{}\n\n{}", managed_block, after)
} else if after.is_empty() {
format!("{}\n\n{}\n", before, managed_block)
} else {
format!("{}\n\n{}\n\n{}", before, managed_block, after)
}
} else {
// Append managed section
if existing.trim().is_empty() {
managed_block
} else {
format!("{}\n\n{}\n", existing.trim_end(), managed_block)
}
}
}
Err(_) => {
// File doesn't exist, create it
format!("{}\n", managed_block)
}
};
match fs::write(&path, new_content) {
Ok(()) => {
tracing::info!(path = %path.display(), "builtin skills: CLAUDE.md written");
}
Err(err) => {
tracing::warn!(
error = %err,
"builtin skills: failed to write CLAUDE.md"
);
}
}
}
/// Replace `$SANDBOX_AGENT_URL` placeholder with the actual server URL.
fn substitute_url(content: &str, server_url: Option<&str>) -> String {
if let Some(url) = server_url {
content.replace("$SANDBOX_AGENT_URL", url)
} else {
content.to_string()
}
}

View file

@ -421,7 +421,9 @@ fn run_server(cli: &CliConfig, server: &ServerArgs) -> Result<(), CliError> {
let agent_manager = AgentManager::new(default_install_dir())
.map_err(|err| CliError::Server(err.to_string()))?;
let state = Arc::new(AppState::with_branding(auth, agent_manager, branding));
let server_url = format!("http://{}:{}", server.host, server.port);
crate::builtin_skills::write_builtin_skills(Some(&server_url));
let state = Arc::new(AppState::with_branding(auth, agent_manager, branding, Some(server_url)));
let (mut router, state) = build_router_with_state(state);
let cors = build_cors_layer(server)?;

View file

@ -1,6 +1,7 @@
//! Sandbox agent core utilities.
mod acp_proxy_runtime;
pub mod builtin_skills;
pub mod cli;
pub mod daemon;
mod process_runtime;

View file

@ -93,16 +93,17 @@ pub struct AppState {
impl AppState {
pub fn new(auth: AuthConfig, agent_manager: AgentManager) -> Self {
Self::with_branding(auth, agent_manager, BrandingMode::SandboxAgent)
Self::with_branding(auth, agent_manager, BrandingMode::SandboxAgent, None)
}
pub fn with_branding(
auth: AuthConfig,
agent_manager: AgentManager,
branding: BrandingMode,
server_url: Option<String>,
) -> Self {
let agent_manager = Arc::new(agent_manager);
let acp_proxy = Arc::new(AcpProxyRuntime::new(agent_manager.clone()));
let acp_proxy = Arc::new(AcpProxyRuntime::new(agent_manager.clone(), server_url));
let opencode_server_manager = Arc::new(OpenCodeServerManager::new(
agent_manager.clone(),
OpenCodeServerManagerConfig {