# Robodev Agents: developer guide

Robodev Agents runs **Claude Code, Codex or Cursor** in an isolated sandbox per session, on each user's own subscription or API key, and returns **one normalized event log** whichever agent ran.

- Console: https://agents.robodev.povio.dev
- API base URL: https://agents.robodev.povio.dev/v1 (Bearer auth)
- API reference: https://agents.robodev.povio.dev/docs (OpenAPI: https://agents.robodev.povio.dev/openapi.json)
- MCP server: https://agents.robodev.povio.dev/mcp
- This guide as markdown: https://agents.robodev.povio.dev/docs/guide.md

Objects: an **Agent** (harness, model, instructions, connection), an **Environment** (sandbox template: image, setup script, network allowlist), a **Session** (one sandbox, many turns) and **Events** (the normalized log). Create agents and environments in the console or over MCP (`create_environment`, `create_agent`); runs reference them by id. Connections (subscriptions and provider API keys) are added in the console only.

## Use it from a coding agent (MCP)

The MCP server lets Claude Code, Cursor, Codex or any MCP client set up agents and environments, run them, and read what they did. It speaks Streamable HTTP (protocol 2026-07-28, with 2025-11-25 and earlier for older clients) and signs you in with OAuth 2.1: the client opens the console, you approve, and it gets a token for the MCP server only.

Claude Code:

```bash
claude mcp add --transport http robodev https://agents.robodev.povio.dev/mcp
# then run /mcp in Claude Code and choose "Authenticate"
```

Cursor:

```json
// ~/.cursor/mcp.json
{
  "mcpServers": {
    "robodev": { "url": "https://agents.robodev.povio.dev/mcp" }
  }
}
```

Codex:

```toml
# ~/.codex/config.toml
[mcp_servers.robodev]
url = "https://agents.robodev.povio.dev/mcp"
# then: codex mcp login robodev
```

Headless:

```bash
# No browser (CI, a sandboxed agent): send an API key instead of signing in.
claude mcp add --transport http robodev https://agents.robodev.povio.dev/mcp \
  --header "Authorization: Bearer $ROBODEV_API_KEY"
```

Tools:

- `list_agents` (agents:read): Your agents: id, name, harness, model.
- `list_environments` (agents:read): Your environments: id, name, network mode.
- `list_connections` (agents:read): Your subscriptions and API keys per harness (ids and labels, never secrets), for creating agents.
- `create_environment` (agents:write): New sandbox template: image, setup script, network allowlist, env vars, limits.
- `update_environment` (agents:write): Change an environment; only the fields you pass change. envVars replaces the whole set; list existing names in keepEnvVars to keep their values.
- `create_agent` (agents:write): New agent: harness, model, instructions, connection, MCP servers.
- `update_agent` (agents:write): Change an agent; only the fields you pass change. mcpServers replaces the list; give a server auth "bearer" or "headers" without new values to keep its stored secret.
- `run_agent` (agents:run): One-shot run in a fresh sandbox; waits for the answer (optionally checked against an outputSchema).
- `get_run` (agents:read): Result of a run or session that was still going when run_agent returned.
- `start_session` (agents:run): Keep a sandbox for several turns.
- `send_message` (agents:run): Next turn in a session; waits for the answer.
- `list_sessions` (agents:read): Recent sessions and their status.
- `get_session` (agents:read): One session with all its turns.
- `get_events` (agents:read): The normalized event log (commands, file changes, messages), paged by seq.
- `interrupt_session` (agents:run): Stop the running turn; the sandbox stays.
- `end_session` (agents:run): Delete the sandbox.

OAuth details for client authors: protected resource metadata at https://agents.robodev.povio.dev/.well-known/oauth-protected-resource/mcp, authorization server metadata at https://agents.robodev.povio.dev/.well-known/oauth-authorization-server. Clients identify themselves with a Client ID Metadata Document (an https `client_id`) or, for older clients, dynamic client registration. PKCE (S256) and the `resource` parameter are required. Scopes: `agents:read`, `agents:run`, `agents:write` (create and edit agents and environments). Approved clients can be revoked under **Keys** in the console.

## Quickstart (HTTP / TypeScript)

### 1. Install the client

```bash
bun add @robodev/agents-client   # or: npm install @robodev/agents-client
```

TypeScript, no runtime dependencies, Node 18+ and Bun. Not published to a registry yet: depend on `packages/client` from the robodev-managed-agents repo. Plain HTTP works too (see curl below).

### 2. Create an API key

In the console, open **Keys** and create a key named after your app. It's shown once. Each key acts as you; revoke it there and apps using it stop working immediately.

```bash
ROBODEV_URL=https://agents.robodev.povio.dev
ROBODEV_API_KEY=rma_…
ROBODEV_CONSOLE_URL=https://agents.robodev.povio.dev   # optional: errors link to the session log
```

### 3. Run the agent

```ts
import { RobodevAgents, RobodevRunError } from '@robodev/agents-client';

const robodev = new RobodevAgents(); // reads ROBODEV_URL and ROBODEV_API_KEY

type Triage = { kind: 'bug' | 'feature' | 'question'; summary: string };

try {
  const { output } = await robodev.runOutput<Triage>({
    agentId: '<agent-id>',
    environmentId: '<environment-id>',
    prompt: 'Triage this issue: "Login button does nothing on Safari 17"',
    outputSchema: {
      type: 'object',
      properties: { kind: { enum: ['bug', 'feature', 'question'] }, summary: { type: 'string' } },
      required: ['kind', 'summary'],
    },
  });
  console.log(output.kind, output.summary);
} catch (error) {
  if (error instanceof RobodevRunError) console.error(error.reason, error.message);
  else throw error;
}
```

`runOutput` waits for the run, checks the answer against `outputSchema`, and returns it typed. If the agent fails it throws a `RobodevRunError` with a reason and a link to the session log.

With Zod:

```ts
import { z } from 'zod';

const Triage = z.object({ kind: z.enum(['bug', 'feature', 'question']), summary: z.string() });

const { output } = await robodev.runOutput({
  agentId: '<agent-id>',
  environmentId: '<environment-id>',
  prompt,
  outputSchema: z.toJSONSchema(Triage),   // Zod 4
});
const triage = Triage.parse(output);       // fully typed
```

Plain HTTP:

```bash
curl -X POST https://agents.robodev.povio.dev/v1/runs \
  -H "Authorization: Bearer $ROBODEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agentId": "<agent-id>",
    "environmentId": "<environment-id>",
    "prompt": "Triage this issue: \"Login button does nothing on Safari 17\"",
    "wait": 300
  }'
```

`wait` (seconds, max 600) holds the request until the turn finishes. Without it, poll `GET /v1/runs/:sessionId?wait=60`, stream events, or pass `webhookUrl`.

## Multi-turn sessions

For work that takes several steps, keep one sandbox and send messages to it.

```ts
// Keep the sandbox and talk to it over several turns.
const session = await robodev.sessions.create({ agentId: '<agent-id>', environmentId: '<environment-id>' });

await robodev.sessions.send(session.id, { prompt: 'Clone the repo and run the tests', wait: 600 });
const next = await robodev.sessions.send(session.id, { prompt: 'Fix the first failing test', wait: 600 });

// Follow along live: same event shape for Claude Code, Codex and Cursor.
for await (const event of robodev.sessions.stream(session.id)) {
  if (event.item?.kind === 'command') console.log('$', event.item.command?.command);
}

await robodev.sessions.terminate(session.id);
```

## Webhooks

Instead of waiting, pass `webhookUrl` and get a signed POST when the turn finishes. Deliveries retry on errors. The signing secret is on the console's **Keys** page.

```ts
await robodev.run({
  agentId: '<agent-id>',
  environmentId: '<environment-id>',
  prompt,
  metadata: { ticketId: 'ENG-142' },        // comes back on the webhook
  externalId: 'triage:ENG-142',             // retries return the same run
  webhookUrl: 'https://your-app.com/webhooks/robodev',
});
```

```ts
import { verifyWebhook } from '@robodev/agents-client';

// POST /webhooks/robodev  (read the raw body; don't re-serialize JSON)
const event = verifyWebhook(rawBody, req.headers['x-robodev-signature'], process.env.ROBODEV_WEBHOOK_SECRET!);

if (event.type === 'turn.finished') handle(event.session.metadata, event.turn.output);
if (event.type === 'session.failed') alert(event.error);
```

## The event log

Every agent's output is normalized into the same shape. Stream it (`GET /v1/sessions/:id/stream`, SSE, resumable with `Last-Event-ID`), or page it (`GET /v1/sessions/:id/events?after=<seq>`).

```jsonc
{
  "v": 1, "seq": 42, "ts": "2026-10-02T09:12:03.120Z",
  "sessionId": "…", "turnId": "…", "harness": "codex",
  "type": "item.completed",
  "item": {
    "kind": "command",                 // message · reasoning · command · file_change · tool_call · mcp_call · web_search · plan · subagent
    "status": "completed",
    "command": { "command": "npm test", "output": "…", "exitCode": 0 }
  },
  "usage": { "inputTokens": 3799, "cacheReadTokens": 24064, "outputTokens": 43 }
}
```

`seq` is strictly increasing per session. Usage parts add up: total prompt = `inputTokens + cacheReadTokens + cacheWriteTokens`. The native lines are kept verbatim at `GET /v1/sessions/:id/raw`.
