mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 07:04:48 +00:00
docs: add mcp and skill session config (#106)
This commit is contained in:
parent
d236edf35c
commit
4c8d93e077
95 changed files with 10014 additions and 1342 deletions
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -40,5 +40,13 @@ npm-debug.log*
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# Agent runtime directories
|
||||||
|
.agents/
|
||||||
|
.claude/
|
||||||
|
.opencode/
|
||||||
|
|
||||||
|
# Example temp files
|
||||||
|
.tmp-upload/
|
||||||
|
|
||||||
# CLI binaries (downloaded during npm publish)
|
# CLI binaries (downloaded during npm publish)
|
||||||
sdks/cli/platforms/*/bin/
|
sdks/cli/platforms/*/bin/
|
||||||
|
|
|
||||||
10
.mcp.json
Normal file
10
.mcp.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"everything": {
|
||||||
|
"args": [
|
||||||
|
"@modelcontextprotocol/server-everything"
|
||||||
|
],
|
||||||
|
"command": "npx"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
CLAUDE.md
18
CLAUDE.md
|
|
@ -47,6 +47,16 @@ Universal schema guidance:
|
||||||
- On parse failures, emit an `agent.unparsed` event (source=daemon, synthetic=true) and treat it as a test failure. Preserve raw payloads when `include_raw=true`.
|
- On parse failures, emit an `agent.unparsed` event (source=daemon, synthetic=true) and treat it as a test failure. Preserve raw payloads when `include_raw=true`.
|
||||||
- Track subagent support in `docs/conversion.md`. For now, normalize subagent activity into normal message/tool flow, but revisit explicit subagent modeling later.
|
- Track subagent support in `docs/conversion.md`. For now, normalize subagent activity into normal message/tool flow, but revisit explicit subagent modeling later.
|
||||||
- Keep the FAQ in `README.md` and `frontend/packages/website/src/components/FAQ.tsx` in sync. When adding or modifying FAQ entries, update both files.
|
- Keep the FAQ in `README.md` and `frontend/packages/website/src/components/FAQ.tsx` in sync. When adding or modifying FAQ entries, update both files.
|
||||||
|
- Update `research/wip-agent-support.md` as agent support changes are implemented.
|
||||||
|
|
||||||
|
### OpenAPI / utoipa requirements
|
||||||
|
|
||||||
|
Every `#[utoipa::path(...)]` handler function must have a doc comment where:
|
||||||
|
- The **first line** becomes the OpenAPI `summary` (short human-readable title, e.g. `"List Agents"`). This is used as the sidebar label and page heading in the docs site.
|
||||||
|
- The **remaining lines** become the OpenAPI `description` (one-sentence explanation of what the endpoint does).
|
||||||
|
- Every `responses(...)` entry must have a `description` (no empty descriptions).
|
||||||
|
|
||||||
|
When adding or modifying endpoints, regenerate `docs/openapi.json` and verify titles render correctly in the docs site.
|
||||||
|
|
||||||
### CLI ⇄ HTTP endpoint map (keep in sync)
|
### CLI ⇄ HTTP endpoint map (keep in sync)
|
||||||
|
|
||||||
|
|
@ -64,6 +74,14 @@ Universal schema guidance:
|
||||||
- `sandbox-agent api sessions reply-question` ↔ `POST /v1/sessions/{sessionId}/questions/{questionId}/reply`
|
- `sandbox-agent api sessions reply-question` ↔ `POST /v1/sessions/{sessionId}/questions/{questionId}/reply`
|
||||||
- `sandbox-agent api sessions reject-question` ↔ `POST /v1/sessions/{sessionId}/questions/{questionId}/reject`
|
- `sandbox-agent api sessions reject-question` ↔ `POST /v1/sessions/{sessionId}/questions/{questionId}/reject`
|
||||||
- `sandbox-agent api sessions reply-permission` ↔ `POST /v1/sessions/{sessionId}/permissions/{permissionId}/reply`
|
- `sandbox-agent api sessions reply-permission` ↔ `POST /v1/sessions/{sessionId}/permissions/{permissionId}/reply`
|
||||||
|
- `sandbox-agent api fs entries` ↔ `GET /v1/fs/entries`
|
||||||
|
- `sandbox-agent api fs read` ↔ `GET /v1/fs/file`
|
||||||
|
- `sandbox-agent api fs write` ↔ `PUT /v1/fs/file`
|
||||||
|
- `sandbox-agent api fs delete` ↔ `DELETE /v1/fs/entry`
|
||||||
|
- `sandbox-agent api fs mkdir` ↔ `POST /v1/fs/mkdir`
|
||||||
|
- `sandbox-agent api fs move` ↔ `POST /v1/fs/move`
|
||||||
|
- `sandbox-agent api fs stat` ↔ `GET /v1/fs/stat`
|
||||||
|
- `sandbox-agent api fs upload-batch` ↔ `POST /v1/fs/upload-batch`
|
||||||
|
|
||||||
## OpenCode Compatibility Layer
|
## OpenCode Compatibility Layer
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ edition = "2021"
|
||||||
authors = [ "Rivet Gaming, LLC <developer@rivet.gg>" ]
|
authors = [ "Rivet Gaming, LLC <developer@rivet.gg>" ]
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
repository = "https://github.com/rivet-dev/sandbox-agent"
|
repository = "https://github.com/rivet-dev/sandbox-agent"
|
||||||
description = "Universal API for automatic coding agents in sandboxes. Supprots Claude Code, Codex, OpenCode, and Amp."
|
description = "Universal API for automatic coding agents in sandboxes. Supports Claude Code, Codex, OpenCode, and Amp."
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
# Internal crates
|
# Internal crates
|
||||||
|
|
@ -69,6 +69,7 @@ url = "2.5"
|
||||||
regress = "0.10"
|
regress = "0.10"
|
||||||
include_dir = "0.7"
|
include_dir = "0.7"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
|
toml_edit = "0.22"
|
||||||
|
|
||||||
# Code generation (build deps)
|
# Code generation (build deps)
|
||||||
typify = "0.4"
|
typify = "0.4"
|
||||||
|
|
|
||||||
278
docs/agent-sessions.mdx
Normal file
278
docs/agent-sessions.mdx
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
---
|
||||||
|
title: "Agent Sessions"
|
||||||
|
description: "Create sessions and send messages to agents."
|
||||||
|
sidebarTitle: "Sessions"
|
||||||
|
icon: "comments"
|
||||||
|
---
|
||||||
|
|
||||||
|
Sessions are the unit of interaction with an agent. You create one session per task, then send messages and stream events.
|
||||||
|
|
||||||
|
## Session Options
|
||||||
|
|
||||||
|
`POST /v1/sessions/{sessionId}` accepts the following fields:
|
||||||
|
|
||||||
|
- `agent` (required): `claude`, `codex`, `opencode`, `amp`, or `mock`
|
||||||
|
- `agentMode`: agent mode string (for example, `build`, `plan`)
|
||||||
|
- `permissionMode`: permission mode string (`default`, `plan`, `bypass`, etc.)
|
||||||
|
- `model`: model override (agent-specific)
|
||||||
|
- `variant`: model variant (agent-specific)
|
||||||
|
- `agentVersion`: agent version override
|
||||||
|
- `mcp`: MCP server config map (see `MCP`)
|
||||||
|
- `skills`: skill path config (see `Skills`)
|
||||||
|
|
||||||
|
## Create A Session
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts TypeScript
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
token: process.env.SANDBOX_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.createSession("build-session", {
|
||||||
|
agent: "codex",
|
||||||
|
agentMode: "build",
|
||||||
|
permissionMode: "default",
|
||||||
|
model: "gpt-4.1",
|
||||||
|
variant: "reasoning",
|
||||||
|
agentVersion: "latest",
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash cURL
|
||||||
|
curl -X POST "http://127.0.0.1:2468/v1/sessions/build-session" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"agent": "codex",
|
||||||
|
"agentMode": "build",
|
||||||
|
"permissionMode": "default",
|
||||||
|
"model": "gpt-4.1",
|
||||||
|
"variant": "reasoning",
|
||||||
|
"agentVersion": "latest"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
## Send A Message
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts TypeScript
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
token: process.env.SANDBOX_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.postMessage("build-session", {
|
||||||
|
message: "Summarize the repository structure.",
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash cURL
|
||||||
|
curl -X POST "http://127.0.0.1:2468/v1/sessions/build-session/messages" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"message":"Summarize the repository structure."}'
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
## Stream A Turn
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts TypeScript
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
token: process.env.SANDBOX_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await client.postMessageStream("build-session", {
|
||||||
|
message: "Explain the main entrypoints.",
|
||||||
|
});
|
||||||
|
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
if (reader) {
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
console.log(decoder.decode(value, { stream: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash cURL
|
||||||
|
curl -N -X POST "http://127.0.0.1:2468/v1/sessions/build-session/messages/stream" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"message":"Explain the main entrypoints."}'
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
## Fetch Events
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts TypeScript
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
token: process.env.SANDBOX_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
const events = await client.getEvents("build-session", {
|
||||||
|
offset: 0,
|
||||||
|
limit: 50,
|
||||||
|
includeRaw: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(events.events);
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash cURL
|
||||||
|
curl -X GET "http://127.0.0.1:2468/v1/sessions/build-session/events?offset=0&limit=50" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN"
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
`GET /v1/sessions/{sessionId}/get-messages` is an alias for `events`.
|
||||||
|
|
||||||
|
## Stream Events (SSE)
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts TypeScript
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
token: process.env.SANDBOX_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const event of client.streamEvents("build-session", { offset: 0 })) {
|
||||||
|
console.log(event.type, event.data);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash cURL
|
||||||
|
curl -N -X GET "http://127.0.0.1:2468/v1/sessions/build-session/events/sse?offset=0" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN"
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
## List Sessions
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts TypeScript
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
token: process.env.SANDBOX_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sessions = await client.listSessions();
|
||||||
|
console.log(sessions.sessions);
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash cURL
|
||||||
|
curl -X GET "http://127.0.0.1:2468/v1/sessions" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN"
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
## Reply To A Question
|
||||||
|
|
||||||
|
When the agent asks a question, reply with an array of answers. Each inner array is one multi-select response.
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts TypeScript
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
token: process.env.SANDBOX_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.replyQuestion("build-session", "question-1", {
|
||||||
|
answers: [["yes"]],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash cURL
|
||||||
|
curl -X POST "http://127.0.0.1:2468/v1/sessions/build-session/questions/question-1/reply" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"answers":[["yes"]]}'
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
## Reject A Question
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts TypeScript
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
token: process.env.SANDBOX_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.rejectQuestion("build-session", "question-1");
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash cURL
|
||||||
|
curl -X POST "http://127.0.0.1:2468/v1/sessions/build-session/questions/question-1/reject" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN"
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
## Reply To A Permission Request
|
||||||
|
|
||||||
|
Use `once`, `always`, or `reject`.
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts TypeScript
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
token: process.env.SANDBOX_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.replyPermission("build-session", "permission-1", {
|
||||||
|
reply: "once",
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash cURL
|
||||||
|
curl -X POST "http://127.0.0.1:2468/v1/sessions/build-session/permissions/permission-1/reply" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"reply":"once"}'
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
## Terminate A Session
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts TypeScript
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
token: process.env.SANDBOX_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.terminateSession("build-session");
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash cURL
|
||||||
|
curl -X POST "http://127.0.0.1:2468/v1/sessions/build-session/terminate" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN"
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
87
docs/attachments.mdx
Normal file
87
docs/attachments.mdx
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
---
|
||||||
|
title: "Attachments"
|
||||||
|
description: "Upload files into the sandbox and attach them to prompts."
|
||||||
|
sidebarTitle: "Attachments"
|
||||||
|
icon: "paperclip"
|
||||||
|
---
|
||||||
|
|
||||||
|
Use the filesystem API to upload files, then reference them as attachments when sending prompts.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Upload a file">
|
||||||
|
<CodeGroup>
|
||||||
|
```ts TypeScript
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
import fs from "node:fs";
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
token: process.env.SANDBOX_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
const buffer = await fs.promises.readFile("./data.csv");
|
||||||
|
|
||||||
|
const upload = await client.writeFsFile(
|
||||||
|
{ path: "./uploads/data.csv", sessionId: "my-session" },
|
||||||
|
buffer,
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(upload.path);
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash cURL
|
||||||
|
curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=./uploads/data.csv&sessionId=my-session" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||||
|
--data-binary @./data.csv
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
The response returns the absolute path that you should use for attachments.
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Attach the file in a prompt">
|
||||||
|
<CodeGroup>
|
||||||
|
```ts TypeScript
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
token: process.env.SANDBOX_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.postMessage("my-session", {
|
||||||
|
message: "Please analyze the attached CSV.",
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
path: "/home/sandbox/uploads/data.csv",
|
||||||
|
mime: "text/csv",
|
||||||
|
filename: "data.csv",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash cURL
|
||||||
|
curl -X POST "http://127.0.0.1:2468/v1/sessions/my-session/messages" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"message": "Please analyze the attached CSV.",
|
||||||
|
"attachments": [
|
||||||
|
{
|
||||||
|
"path": "/home/sandbox/uploads/data.csv",
|
||||||
|
"mime": "text/csv",
|
||||||
|
"filename": "data.csv"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Use absolute paths from the upload response to avoid ambiguity.
|
||||||
|
- If `mime` is omitted, the server defaults to `application/octet-stream`.
|
||||||
|
- OpenCode receives file parts directly; other agents will see the attachment paths appended to the prompt.
|
||||||
137
docs/cli.mdx
137
docs/cli.mdx
|
|
@ -2,7 +2,6 @@
|
||||||
title: "CLI Reference"
|
title: "CLI Reference"
|
||||||
description: "Complete CLI reference for sandbox-agent."
|
description: "Complete CLI reference for sandbox-agent."
|
||||||
sidebarTitle: "CLI"
|
sidebarTitle: "CLI"
|
||||||
icon: "terminal"
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Server
|
## Server
|
||||||
|
|
@ -250,6 +249,8 @@ sandbox-agent api sessions create <SESSION_ID> [OPTIONS]
|
||||||
| `-m, --model <MODEL>` | Model override |
|
| `-m, --model <MODEL>` | Model override |
|
||||||
| `-v, --variant <VARIANT>` | Model variant |
|
| `-v, --variant <VARIANT>` | Model variant |
|
||||||
| `-A, --agent-version <VERSION>` | Agent version |
|
| `-A, --agent-version <VERSION>` | Agent version |
|
||||||
|
| `--mcp-config <PATH>` | JSON file with MCP server config (see `mcp` docs) |
|
||||||
|
| `--skill <PATH>` | Skill directory or `SKILL.md` path (repeatable) |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sandbox-agent api sessions create my-session \
|
sandbox-agent api sessions create my-session \
|
||||||
|
|
@ -381,6 +382,132 @@ sandbox-agent api sessions reply-permission my-session perm1 --reply once
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Filesystem
|
||||||
|
|
||||||
|
#### List Entries
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sandbox-agent api fs entries [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `--path <PATH>` | Directory path (default: `.`) |
|
||||||
|
| `--session-id <SESSION_ID>` | Resolve relative paths from the session working directory |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sandbox-agent api fs entries --path ./workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Read File
|
||||||
|
|
||||||
|
`api fs read` writes raw bytes to stdout.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sandbox-agent api fs read <PATH> [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `--session-id <SESSION_ID>` | Resolve relative paths from the session working directory |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sandbox-agent api fs read ./notes.txt > ./notes.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Write File
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sandbox-agent api fs write <PATH> [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `--content <TEXT>` | Write UTF-8 content |
|
||||||
|
| `--from-file <PATH>` | Read content from a local file |
|
||||||
|
| `--session-id <SESSION_ID>` | Resolve relative paths from the session working directory |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sandbox-agent api fs write ./hello.txt --content "hello"
|
||||||
|
sandbox-agent api fs write ./image.bin --from-file ./image.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Delete Entry
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sandbox-agent api fs delete <PATH> [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `--recursive` | Delete directories recursively |
|
||||||
|
| `--session-id <SESSION_ID>` | Resolve relative paths from the session working directory |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sandbox-agent api fs delete ./old.log
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Create Directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sandbox-agent api fs mkdir <PATH> [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `--session-id <SESSION_ID>` | Resolve relative paths from the session working directory |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sandbox-agent api fs mkdir ./cache
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Move/Rename
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sandbox-agent api fs move <FROM> <TO> [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `--overwrite` | Overwrite destination if it exists |
|
||||||
|
| `--session-id <SESSION_ID>` | Resolve relative paths from the session working directory |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sandbox-agent api fs move ./a.txt ./b.txt --overwrite
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Stat
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sandbox-agent api fs stat <PATH> [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `--session-id <SESSION_ID>` | Resolve relative paths from the session working directory |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sandbox-agent api fs stat ./notes.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Upload Batch (tar)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sandbox-agent api fs upload-batch --tar <PATH> [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `--tar <PATH>` | Tar archive to extract |
|
||||||
|
| `--path <PATH>` | Destination directory |
|
||||||
|
| `--session-id <SESSION_ID>` | Resolve relative paths from the session working directory |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sandbox-agent api fs upload-batch --tar ./skills.tar --path ./skills
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## CLI to HTTP Mapping
|
## CLI to HTTP Mapping
|
||||||
|
|
||||||
| CLI Command | HTTP Endpoint |
|
| CLI Command | HTTP Endpoint |
|
||||||
|
|
@ -399,3 +526,11 @@ sandbox-agent api sessions reply-permission my-session perm1 --reply once
|
||||||
| `api sessions reply-question` | `POST /v1/sessions/{sessionId}/questions/{questionId}/reply` |
|
| `api sessions reply-question` | `POST /v1/sessions/{sessionId}/questions/{questionId}/reply` |
|
||||||
| `api sessions reject-question` | `POST /v1/sessions/{sessionId}/questions/{questionId}/reject` |
|
| `api sessions reject-question` | `POST /v1/sessions/{sessionId}/questions/{questionId}/reject` |
|
||||||
| `api sessions reply-permission` | `POST /v1/sessions/{sessionId}/permissions/{permissionId}/reply` |
|
| `api sessions reply-permission` | `POST /v1/sessions/{sessionId}/permissions/{permissionId}/reply` |
|
||||||
|
| `api fs entries` | `GET /v1/fs/entries` |
|
||||||
|
| `api fs read` | `GET /v1/fs/file` |
|
||||||
|
| `api fs write` | `PUT /v1/fs/file` |
|
||||||
|
| `api fs delete` | `DELETE /v1/fs/entry` |
|
||||||
|
| `api fs mkdir` | `POST /v1/fs/mkdir` |
|
||||||
|
| `api fs move` | `POST /v1/fs/move` |
|
||||||
|
| `api fs stat` | `GET /v1/fs/stat` |
|
||||||
|
| `api fs upload-batch` | `POST /v1/fs/upload-batch` |
|
||||||
|
|
|
||||||
245
docs/custom-tools.mdx
Normal file
245
docs/custom-tools.mdx
Normal file
|
|
@ -0,0 +1,245 @@
|
||||||
|
---
|
||||||
|
title: "Custom Tools"
|
||||||
|
description: "Give agents custom tools inside the sandbox using MCP servers or skills."
|
||||||
|
sidebarTitle: "Custom Tools"
|
||||||
|
icon: "wrench"
|
||||||
|
---
|
||||||
|
|
||||||
|
There are two ways to give agents custom tools that run inside the sandbox:
|
||||||
|
|
||||||
|
| | MCP Server | Skill |
|
||||||
|
|---|---|---|
|
||||||
|
| **How it works** | Sandbox Agent spawns your MCP server process and routes tool calls to it via stdio | A markdown file that instructs the agent to run your script with `node` (or any command) |
|
||||||
|
| **Tool discovery** | Agent sees tools automatically via MCP protocol | Agent reads instructions from the skill file |
|
||||||
|
| **Best for** | Structured tools with typed inputs/outputs | Lightweight scripts with natural-language instructions |
|
||||||
|
| **Requires** | `@modelcontextprotocol/sdk` dependency | Just a markdown file and a script |
|
||||||
|
|
||||||
|
Both approaches execute code inside the sandbox, so your tools have full access to the sandbox filesystem, network, and installed system tools.
|
||||||
|
|
||||||
|
## Option A: Tools via MCP
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Write your MCP server">
|
||||||
|
Create an MCP server that exposes tools using `@modelcontextprotocol/sdk` with `StdioServerTransport`. This server will run inside the sandbox.
|
||||||
|
|
||||||
|
```ts src/mcp-server.ts
|
||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const server = new McpServer({
|
||||||
|
name: "rand",
|
||||||
|
version: "1.0.0",
|
||||||
|
});
|
||||||
|
|
||||||
|
server.tool(
|
||||||
|
"random_number",
|
||||||
|
"Generate a random integer between min and max (inclusive)",
|
||||||
|
{
|
||||||
|
min: z.number().describe("Minimum value"),
|
||||||
|
max: z.number().describe("Maximum value"),
|
||||||
|
},
|
||||||
|
async ({ min, max }) => ({
|
||||||
|
content: [{ type: "text", text: String(Math.floor(Math.random() * (max - min + 1)) + min) }],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a simple example. Your MCP server runs inside the sandbox, so you can execute any code you'd like: query databases, call internal APIs, run shell commands, or interact with any service available in the container.
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Package the MCP server">
|
||||||
|
Bundle into a single JS file so it can be uploaded and executed without a `node_modules` folder.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx esbuild src/mcp-server.ts --bundle --format=cjs --platform=node --target=node18 --minify --outfile=dist/mcp-server.cjs
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates `dist/mcp-server.cjs` ready to upload.
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Create sandbox and upload MCP server">
|
||||||
|
Start your sandbox, then write the bundled file into it.
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts TypeScript
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
import fs from "node:fs";
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
token: process.env.SANDBOX_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = await fs.promises.readFile("./dist/mcp-server.cjs");
|
||||||
|
await client.writeFsFile(
|
||||||
|
{ path: "/opt/mcp/custom-tools/mcp-server.cjs" },
|
||||||
|
content,
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash cURL
|
||||||
|
curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=/opt/mcp/custom-tools/mcp-server.cjs" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||||
|
--data-binary @./dist/mcp-server.cjs
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Create a session">
|
||||||
|
Point an MCP server config at the bundled JS file. When the session starts, Sandbox Agent spawns the MCP server process and routes tool calls to it.
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts TypeScript
|
||||||
|
await client.createSession("custom-tools", {
|
||||||
|
agent: "claude",
|
||||||
|
mcp: {
|
||||||
|
customTools: {
|
||||||
|
type: "local",
|
||||||
|
command: ["node", "/opt/mcp/custom-tools/mcp-server.cjs"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash cURL
|
||||||
|
curl -X POST "http://127.0.0.1:2468/v1/sessions/custom-tools" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"agent": "claude",
|
||||||
|
"mcp": {
|
||||||
|
"customTools": {
|
||||||
|
"type": "local",
|
||||||
|
"command": ["node", "/opt/mcp/custom-tools/mcp-server.cjs"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Option B: Tools via Skills
|
||||||
|
|
||||||
|
Skills are markdown files that instruct the agent how to use a script. Upload the script and a skill file, then point the session at the skill directory.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Write your script">
|
||||||
|
Write a script that the agent will execute. This runs inside the sandbox just like an MCP server, but the agent invokes it directly via its shell tool.
|
||||||
|
|
||||||
|
```ts src/random-number.ts
|
||||||
|
const min = Number(process.argv[2]);
|
||||||
|
const max = Number(process.argv[3]);
|
||||||
|
|
||||||
|
if (Number.isNaN(min) || Number.isNaN(max)) {
|
||||||
|
console.error("Usage: random-number <min> <max>");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(Math.floor(Math.random() * (max - min + 1)) + min);
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Write a skill file">
|
||||||
|
Create a `SKILL.md` that tells the agent what the script does and how to run it. The frontmatter `name` and `description` fields are required. See [Skill authoring best practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices) for tips on writing effective skills.
|
||||||
|
|
||||||
|
```md SKILL.md
|
||||||
|
---
|
||||||
|
name: random-number
|
||||||
|
description: Generate a random integer between min and max (inclusive). Use when the user asks for a random number.
|
||||||
|
---
|
||||||
|
|
||||||
|
To generate a random number, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node /opt/skills/random-number/random-number.cjs <min> <max>
|
||||||
|
```
|
||||||
|
|
||||||
|
This prints a single random integer between min and max (inclusive).
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Package the script">
|
||||||
|
Bundle the script just like an MCP server so it has no dependencies at runtime.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx esbuild src/random-number.ts --bundle --format=cjs --platform=node --target=node18 --minify --outfile=dist/random-number.cjs
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Create sandbox and upload files">
|
||||||
|
Upload both the bundled script and the skill file.
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts TypeScript
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
import fs from "node:fs";
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
token: process.env.SANDBOX_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
const script = await fs.promises.readFile("./dist/random-number.cjs");
|
||||||
|
await client.writeFsFile(
|
||||||
|
{ path: "/opt/skills/random-number/random-number.cjs" },
|
||||||
|
script,
|
||||||
|
);
|
||||||
|
|
||||||
|
const skill = await fs.promises.readFile("./SKILL.md");
|
||||||
|
await client.writeFsFile(
|
||||||
|
{ path: "/opt/skills/random-number/SKILL.md" },
|
||||||
|
skill,
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash cURL
|
||||||
|
curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=/opt/skills/random-number/random-number.cjs" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||||
|
--data-binary @./dist/random-number.cjs
|
||||||
|
|
||||||
|
curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=/opt/skills/random-number/SKILL.md" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||||
|
--data-binary @./SKILL.md
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Create a session">
|
||||||
|
Point the session at the skill directory. The agent reads `SKILL.md` and learns how to use your script.
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts TypeScript
|
||||||
|
await client.createSession("custom-tools", {
|
||||||
|
agent: "claude",
|
||||||
|
skills: {
|
||||||
|
sources: [
|
||||||
|
{ type: "local", source: "/opt/skills/random-number" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash cURL
|
||||||
|
curl -X POST "http://127.0.0.1:2468/v1/sessions/custom-tools" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"agent": "claude",
|
||||||
|
"skills": {
|
||||||
|
"sources": [
|
||||||
|
{ "type": "local", "source": "/opt/skills/random-number" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The sandbox image must include a Node.js runtime that can execute the bundled files.
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
---
|
|
||||||
title: "Deploy"
|
|
||||||
sidebarTitle: "Overview"
|
|
||||||
description: "Choose where to run the sandbox-agent server."
|
|
||||||
icon: "server"
|
|
||||||
---
|
|
||||||
|
|
||||||
<CardGroup cols={2}>
|
|
||||||
<Card title="Local" icon="laptop" href="/deploy/local">
|
|
||||||
Run locally for development. The SDK can auto-spawn the server.
|
|
||||||
</Card>
|
|
||||||
<Card title="E2B" icon="cube" href="/deploy/e2b">
|
|
||||||
Deploy inside an E2B sandbox with network access.
|
|
||||||
</Card>
|
|
||||||
<Card title="Vercel" icon="triangle" href="/deploy/vercel">
|
|
||||||
Deploy inside a Vercel Sandbox with port forwarding.
|
|
||||||
</Card>
|
|
||||||
<Card title="Cloudflare" icon="cloud" href="/deploy/cloudflare">
|
|
||||||
Deploy inside a Cloudflare Sandbox with port exposure.
|
|
||||||
</Card>
|
|
||||||
<Card title="Daytona" icon="cloud" href="/deploy/daytona">
|
|
||||||
Run in a Daytona workspace with port forwarding.
|
|
||||||
</Card>
|
|
||||||
<Card title="Docker" icon="docker" href="/deploy/docker">
|
|
||||||
Build and run in a container (development only).
|
|
||||||
</Card>
|
|
||||||
</CardGroup>
|
|
||||||
111
docs/docs.json
111
docs/docs.json
|
|
@ -25,66 +25,97 @@
|
||||||
},
|
},
|
||||||
"navbar": {
|
"navbar": {
|
||||||
"links": [
|
"links": [
|
||||||
|
{
|
||||||
|
"label": "Gigacode",
|
||||||
|
"icon": "terminal",
|
||||||
|
"href": "https://github.com/rivet-dev/sandbox-agent/tree/main/gigacode"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "Discord",
|
"label": "Discord",
|
||||||
"icon": "discord",
|
"icon": "discord",
|
||||||
"href": "https://discord.gg/auCecybynK"
|
"href": "https://discord.gg/auCecybynK"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "GitHub",
|
"type": "github",
|
||||||
"icon": "github",
|
|
||||||
"href": "https://github.com/rivet-dev/sandbox-agent"
|
"href": "https://github.com/rivet-dev/sandbox-agent"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"pages": [
|
"tabs": [
|
||||||
{
|
{
|
||||||
"group": "Getting started",
|
"tab": "Documentation",
|
||||||
"pages": [
|
"pages": [
|
||||||
"quickstart",
|
|
||||||
"building-chat-ui",
|
|
||||||
"manage-sessions",
|
|
||||||
"opencode-compatibility"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"group": "Deploy",
|
|
||||||
"pages": [
|
|
||||||
"deploy/index",
|
|
||||||
"deploy/local",
|
|
||||||
"deploy/e2b",
|
|
||||||
"deploy/daytona",
|
|
||||||
"deploy/vercel",
|
|
||||||
"deploy/cloudflare",
|
|
||||||
"deploy/docker"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"group": "SDKs",
|
|
||||||
"pages": ["sdks/typescript", "sdks/python"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"group": "Reference",
|
|
||||||
"pages": [
|
|
||||||
"cli",
|
|
||||||
"inspector",
|
|
||||||
"session-transcript-schema",
|
|
||||||
"credentials",
|
|
||||||
"gigacode",
|
|
||||||
{
|
{
|
||||||
"group": "AI",
|
"group": "Getting started",
|
||||||
"pages": ["ai/skill", "ai/llms-txt"]
|
"pages": [
|
||||||
|
"quickstart",
|
||||||
|
"building-chat-ui",
|
||||||
|
"manage-sessions",
|
||||||
|
{
|
||||||
|
"group": "Deploy",
|
||||||
|
"icon": "server",
|
||||||
|
"pages": [
|
||||||
|
"deploy/local",
|
||||||
|
"deploy/e2b",
|
||||||
|
"deploy/daytona",
|
||||||
|
"deploy/vercel",
|
||||||
|
"deploy/cloudflare",
|
||||||
|
"deploy/docker"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Advanced",
|
"group": "SDKs",
|
||||||
"pages": ["daemon", "cors", "telemetry"]
|
"pages": ["sdks/typescript", "sdks/python"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Agent Features",
|
||||||
|
"pages": [
|
||||||
|
"agent-sessions",
|
||||||
|
"attachments",
|
||||||
|
"skills",
|
||||||
|
"mcp",
|
||||||
|
"custom-tools"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Features",
|
||||||
|
"pages": ["file-system"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Reference",
|
||||||
|
"pages": [
|
||||||
|
"cli",
|
||||||
|
"inspector",
|
||||||
|
"session-transcript-schema",
|
||||||
|
"opencode-compatibility",
|
||||||
|
{
|
||||||
|
"group": "More",
|
||||||
|
"pages": [
|
||||||
|
"credentials",
|
||||||
|
"daemon",
|
||||||
|
"cors",
|
||||||
|
"telemetry",
|
||||||
|
{
|
||||||
|
"group": "AI",
|
||||||
|
"pages": ["ai/skill", "ai/llms-txt"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "HTTP API Reference",
|
"tab": "HTTP API",
|
||||||
"openapi": "openapi.json"
|
"pages": [
|
||||||
|
{
|
||||||
|
"group": "HTTP Reference",
|
||||||
|
"openapi": "openapi.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
184
docs/file-system.mdx
Normal file
184
docs/file-system.mdx
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
---
|
||||||
|
title: "File System"
|
||||||
|
description: "Read, write, and manage files inside the sandbox."
|
||||||
|
sidebarTitle: "File System"
|
||||||
|
icon: "folder"
|
||||||
|
---
|
||||||
|
|
||||||
|
The filesystem API lets you list, read, write, move, and delete files inside the sandbox, plus upload batches of files via tar archives.
|
||||||
|
|
||||||
|
## Path Resolution
|
||||||
|
|
||||||
|
- Absolute paths are used as-is.
|
||||||
|
- Relative paths use the session working directory when `sessionId` is provided.
|
||||||
|
- Without `sessionId`, relative paths resolve against the server home directory.
|
||||||
|
- Relative paths cannot contain `..` or absolute prefixes; requests that attempt to escape the root are rejected.
|
||||||
|
|
||||||
|
The session working directory is the server process current working directory at the moment the session is created.
|
||||||
|
|
||||||
|
## List Entries
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts TypeScript
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
token: process.env.SANDBOX_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
const entries = await client.listFsEntries({
|
||||||
|
path: "./workspace",
|
||||||
|
sessionId: "my-session",
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(entries);
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash cURL
|
||||||
|
curl -X GET "http://127.0.0.1:2468/v1/fs/entries?path=./workspace&sessionId=my-session" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN"
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
## Read And Write Files
|
||||||
|
|
||||||
|
`PUT /v1/fs/file` writes raw bytes. `GET /v1/fs/file` returns raw bytes.
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts TypeScript
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
token: process.env.SANDBOX_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.writeFsFile({ path: "./notes.txt", sessionId: "my-session" }, "hello");
|
||||||
|
|
||||||
|
const bytes = await client.readFsFile({
|
||||||
|
path: "./notes.txt",
|
||||||
|
sessionId: "my-session",
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = new TextDecoder().decode(bytes);
|
||||||
|
console.log(text);
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash cURL
|
||||||
|
curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=./notes.txt&sessionId=my-session" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||||
|
--data-binary "hello"
|
||||||
|
|
||||||
|
curl -X GET "http://127.0.0.1:2468/v1/fs/file?path=./notes.txt&sessionId=my-session" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||||
|
--output ./notes.txt
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
## Create Directories
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts TypeScript
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
token: process.env.SANDBOX_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.mkdirFs({
|
||||||
|
path: "./data",
|
||||||
|
sessionId: "my-session",
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash cURL
|
||||||
|
curl -X POST "http://127.0.0.1:2468/v1/fs/mkdir?path=./data&sessionId=my-session" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN"
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
## Move, Delete, And Stat
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts TypeScript
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
token: process.env.SANDBOX_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.moveFs(
|
||||||
|
{ from: "./notes.txt", to: "./notes-old.txt", overwrite: true },
|
||||||
|
{ sessionId: "my-session" },
|
||||||
|
);
|
||||||
|
|
||||||
|
const stat = await client.statFs({
|
||||||
|
path: "./notes-old.txt",
|
||||||
|
sessionId: "my-session",
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.deleteFsEntry({
|
||||||
|
path: "./notes-old.txt",
|
||||||
|
sessionId: "my-session",
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(stat);
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash cURL
|
||||||
|
curl -X POST "http://127.0.0.1:2468/v1/fs/move?sessionId=my-session" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"from":"./notes.txt","to":"./notes-old.txt","overwrite":true}'
|
||||||
|
|
||||||
|
curl -X GET "http://127.0.0.1:2468/v1/fs/stat?path=./notes-old.txt&sessionId=my-session" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN"
|
||||||
|
|
||||||
|
curl -X DELETE "http://127.0.0.1:2468/v1/fs/entry?path=./notes-old.txt&sessionId=my-session" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN"
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
## Batch Upload (Tar)
|
||||||
|
|
||||||
|
Batch upload accepts `application/x-tar` only and extracts into the destination directory. The response returns absolute paths for extracted files, capped at 1024 entries.
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts TypeScript
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import tar from "tar";
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
token: process.env.SANDBOX_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
const archivePath = path.join(process.cwd(), "skills.tar");
|
||||||
|
await tar.c({
|
||||||
|
cwd: "./skills",
|
||||||
|
file: archivePath,
|
||||||
|
}, ["."]);
|
||||||
|
|
||||||
|
const tarBuffer = await fs.promises.readFile(archivePath);
|
||||||
|
const result = await client.uploadFsBatch(tarBuffer, {
|
||||||
|
path: "./skills",
|
||||||
|
sessionId: "my-session",
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(result);
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash cURL
|
||||||
|
tar -cf skills.tar -C ./skills .
|
||||||
|
|
||||||
|
curl -X POST "http://127.0.0.1:2468/v1/fs/upload-batch?path=./skills&sessionId=my-session" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||||
|
-H "Content-Type: application/x-tar" \
|
||||||
|
--data-binary @skills.tar
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
---
|
---
|
||||||
title: "Inspector"
|
title: "Inspector"
|
||||||
description: "Debug and inspect agent sessions with the Inspector UI."
|
description: "Debug and inspect agent sessions with the Inspector UI."
|
||||||
icon: "magnifying-glass"
|
|
||||||
---
|
---
|
||||||
|
|
||||||
The Inspector is a web-based GUI for debugging and inspecting Sandbox Agent sessions. Use it to view events, send messages, and troubleshoot agent behavior in real-time.
|
The Inspector is a web-based GUI for debugging and inspecting Sandbox Agent sessions. Use it to view events, send messages, and troubleshoot agent behavior in real-time.
|
||||||
|
|
|
||||||
122
docs/mcp.mdx
Normal file
122
docs/mcp.mdx
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
---
|
||||||
|
title: "MCP"
|
||||||
|
description: "Configure MCP servers for agent sessions."
|
||||||
|
sidebarTitle: "MCP"
|
||||||
|
icon: "plug"
|
||||||
|
---
|
||||||
|
|
||||||
|
MCP (Model Context Protocol) servers extend agents with tools. Sandbox Agent can auto-load MCP servers when a session starts by passing an `mcp` map in the create-session request.
|
||||||
|
|
||||||
|
## Session Config
|
||||||
|
|
||||||
|
The `mcp` field is a map of server name to config. Use `type: "local"` for stdio servers and `type: "remote"` for HTTP/SSE servers:
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
|
||||||
|
```ts TypeScript
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
token: process.env.SANDBOX_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.createSession("claude-mcp", {
|
||||||
|
agent: "claude",
|
||||||
|
mcp: {
|
||||||
|
filesystem: {
|
||||||
|
type: "local",
|
||||||
|
command: "my-mcp-server",
|
||||||
|
args: ["--root", "."],
|
||||||
|
},
|
||||||
|
github: {
|
||||||
|
type: "remote",
|
||||||
|
url: "https://example.com/mcp",
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer ${GITHUB_TOKEN}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash cURL
|
||||||
|
curl -X POST "http://127.0.0.1:2468/v1/sessions/claude-mcp" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"agent": "claude",
|
||||||
|
"mcp": {
|
||||||
|
"filesystem": {
|
||||||
|
"type": "local",
|
||||||
|
"command": "my-mcp-server",
|
||||||
|
"args": ["--root", "."]
|
||||||
|
},
|
||||||
|
"github": {
|
||||||
|
"type": "remote",
|
||||||
|
"url": "https://example.com/mcp",
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Bearer ${GITHUB_TOKEN}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
## Config Fields
|
||||||
|
|
||||||
|
### Local Server
|
||||||
|
|
||||||
|
Stdio servers that run inside the sandbox.
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|---|---|
|
||||||
|
| `type` | `local` |
|
||||||
|
| `command` | string or array (`["node", "server.js"]`) |
|
||||||
|
| `args` | array of string arguments |
|
||||||
|
| `env` | environment variables map |
|
||||||
|
| `enabled` | enable or disable the server |
|
||||||
|
| `timeoutMs` | tool timeout override |
|
||||||
|
| `cwd` | working directory for the MCP process |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"command": ["node", "./mcp/server.js"],
|
||||||
|
"args": ["--root", "."],
|
||||||
|
"env": { "LOG_LEVEL": "debug" },
|
||||||
|
"cwd": "/workspace"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remote Server
|
||||||
|
|
||||||
|
HTTP/SSE servers accessed over the network.
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|---|---|
|
||||||
|
| `type` | `remote` |
|
||||||
|
| `url` | MCP server URL |
|
||||||
|
| `headers` | static headers map |
|
||||||
|
| `bearerTokenEnvVar` | env var name to inject into `Authorization: Bearer ...` |
|
||||||
|
| `envHeaders` | map of header name to env var name |
|
||||||
|
| `oauth` | object with `clientId`, `clientSecret`, `scope`, or `false` to disable |
|
||||||
|
| `enabled` | enable or disable the server |
|
||||||
|
| `timeoutMs` | tool timeout override |
|
||||||
|
| `transport` | `http` or `sse` |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "remote",
|
||||||
|
"url": "https://example.com/mcp",
|
||||||
|
"headers": { "x-client": "sandbox-agent" },
|
||||||
|
"bearerTokenEnvVar": "MCP_TOKEN",
|
||||||
|
"transport": "sse"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom MCP Servers
|
||||||
|
|
||||||
|
To bundle and upload your own MCP server into the sandbox, see [Custom Tools](/custom-tools).
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,6 @@
|
||||||
---
|
---
|
||||||
title: "OpenCode SDK & UI Support"
|
title: "OpenCode Compatibility"
|
||||||
description: "Connect OpenCode clients, SDKs, and web UI to Sandbox Agent."
|
description: "Connect OpenCode clients, SDKs, and web UI to Sandbox Agent."
|
||||||
icon: "rectangle-terminal"
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Warning>
|
<Warning>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
---
|
---
|
||||||
title: "Session Transcript Schema"
|
title: "Session Transcript Schema"
|
||||||
description: "Universal event schema for session transcripts across all agents."
|
description: "Universal event schema for session transcripts across all agents."
|
||||||
icon: "brackets-curly"
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Each coding agent outputs events in its own native format. The sandbox-agent converts these into a universal event schema, giving you a consistent session transcript regardless of which agent you use.
|
Each coding agent outputs events in its own native format. The sandbox-agent converts these into a universal event schema, giving you a consistent session transcript regardless of which agent you use.
|
||||||
|
|
@ -27,7 +26,7 @@ This table shows which agent feature coverage appears in the universal event str
|
||||||
| Reasoning/Thinking | - | ✓ | - | - |
|
| Reasoning/Thinking | - | ✓ | - | - |
|
||||||
| Command Execution | - | ✓ | - | - |
|
| Command Execution | - | ✓ | - | - |
|
||||||
| File Changes | - | ✓ | - | - |
|
| File Changes | - | ✓ | - | - |
|
||||||
| MCP Tools | - | ✓ | - | - |
|
| MCP Tools | ✓ | ✓ | ✓ | ✓ |
|
||||||
| Streaming Deltas | ✓ | ✓ | ✓ | - |
|
| Streaming Deltas | ✓ | ✓ | ✓ | - |
|
||||||
| Variants | | ✓ | ✓ | ✓ |
|
| Variants | | ✓ | ✓ | ✓ |
|
||||||
|
|
||||||
|
|
|
||||||
87
docs/skills.mdx
Normal file
87
docs/skills.mdx
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
---
|
||||||
|
title: "Skills"
|
||||||
|
description: "Auto-load skills into agent sessions."
|
||||||
|
sidebarTitle: "Skills"
|
||||||
|
icon: "sparkles"
|
||||||
|
---
|
||||||
|
|
||||||
|
Skills are local instruction bundles stored in `SKILL.md` files. Sandbox Agent can fetch, discover, and link skill directories into agent-specific skill paths at session start using the `skills.sources` field. The format is fully compatible with [skills.sh](https://skills.sh).
|
||||||
|
|
||||||
|
## Session Config
|
||||||
|
|
||||||
|
Pass `skills.sources` when creating a session to load skills from GitHub repos, local paths, or git URLs.
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
|
||||||
|
```ts TypeScript
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
token: process.env.SANDBOX_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.createSession("claude-skills", {
|
||||||
|
agent: "claude",
|
||||||
|
skills: {
|
||||||
|
sources: [
|
||||||
|
{ type: "github", source: "rivet-dev/skills", skills: ["sandbox-agent"] },
|
||||||
|
{ type: "local", source: "/workspace/my-custom-skill" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash cURL
|
||||||
|
curl -X POST "http://127.0.0.1:2468/v1/sessions/claude-skills" \
|
||||||
|
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"agent": "claude",
|
||||||
|
"skills": {
|
||||||
|
"sources": [
|
||||||
|
{ "type": "github", "source": "rivet-dev/skills", "skills": ["sandbox-agent"] },
|
||||||
|
{ "type": "local", "source": "/workspace/my-custom-skill" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
Each skill directory must contain `SKILL.md`. See [Skill authoring best practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices) for tips on writing effective skills.
|
||||||
|
|
||||||
|
## Skill Sources
|
||||||
|
|
||||||
|
Each entry in `skills.sources` describes where to find skills. Three source types are supported:
|
||||||
|
|
||||||
|
| Type | `source` value | Example |
|
||||||
|
|------|---------------|---------|
|
||||||
|
| `github` | `owner/repo` | `"rivet-dev/skills"` |
|
||||||
|
| `local` | Filesystem path | `"/workspace/my-skill"` |
|
||||||
|
| `git` | Git clone URL | `"https://git.example.com/skills.git"` |
|
||||||
|
|
||||||
|
### Optional fields
|
||||||
|
|
||||||
|
- **`skills`** — Array of skill directory names to include. When omitted, all discovered skills are installed.
|
||||||
|
- **`ref`** — Branch, tag, or commit to check out (default: HEAD). Applies to `github` and `git` types.
|
||||||
|
- **`subpath`** — Subdirectory within the repo to search for skills.
|
||||||
|
|
||||||
|
## Custom Skills
|
||||||
|
|
||||||
|
To write, upload, and configure your own skills inside the sandbox, see [Custom Tools](/custom-tools).
|
||||||
|
|
||||||
|
## Advanced
|
||||||
|
|
||||||
|
### Discovery logic
|
||||||
|
|
||||||
|
After resolving a source to a local directory (cloning if needed), Sandbox Agent discovers skills by:
|
||||||
|
1. Checking if the directory itself contains `SKILL.md`.
|
||||||
|
2. Scanning `skills/` subdirectory for child directories containing `SKILL.md`.
|
||||||
|
3. Scanning immediate children of the directory for `SKILL.md`.
|
||||||
|
|
||||||
|
Discovered skills are symlinked into project-local skill roots (`.claude/skills/<name>`, `.agents/skills/<name>`, `.opencode/skill/<name>`).
|
||||||
|
|
||||||
|
### Caching
|
||||||
|
|
||||||
|
GitHub sources are downloaded as zip archives and git sources are cloned to `~/.sandbox-agent/skills-cache/` and updated on subsequent session creations. GitHub sources do not require `git` to be installed.
|
||||||
17
examples/CLAUDE.md
Normal file
17
examples/CLAUDE.md
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Examples Instructions
|
||||||
|
|
||||||
|
## Docker Isolation
|
||||||
|
|
||||||
|
- Docker examples must behave like standalone sandboxes.
|
||||||
|
- Do not bind mount host files or host directories into Docker example containers.
|
||||||
|
- If an example needs tools, skills, or MCP servers, install them inside the container during setup.
|
||||||
|
|
||||||
|
## Testing Examples
|
||||||
|
|
||||||
|
Examples can be tested by starting them in the background and communicating directly with the sandbox-agent API:
|
||||||
|
|
||||||
|
1. Start the example: `SANDBOX_AGENT_DEV=1 pnpm start &`
|
||||||
|
2. Note the base URL and session ID from the output.
|
||||||
|
3. Send messages: `curl -X POST http://127.0.0.1:<port>/v1/sessions/<sessionId>/messages -H "Content-Type: application/json" -d '{"message":"..."}'`
|
||||||
|
4. Poll events: `curl http://127.0.0.1:<port>/v1/sessions/<sessionId>/events`
|
||||||
|
5. Approve permissions: `curl -X POST http://127.0.0.1:<port>/v1/sessions/<sessionId>/permissions/<permissionId>/reply -H "Content-Type: application/json" -d '{"reply":"once"}'`
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "node_modules/wrangler/config-schema.json",
|
"$schema": "node_modules/wrangler/config-schema.json",
|
||||||
"name": "sandbox-agent-cloudflare",
|
"name": "sandbox-agent-cloudflare",
|
||||||
"main": "src/cloudflare.ts",
|
"main": "src/index.ts",
|
||||||
"compatibility_date": "2025-01-01",
|
"compatibility_date": "2025-01-01",
|
||||||
"compatibility_flags": ["nodejs_compat"],
|
"compatibility_flags": ["nodejs_compat"],
|
||||||
"assets": {
|
"assets": {
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,14 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "tsx src/daytona.ts",
|
"start": "tsx src/index.ts",
|
||||||
"start:snapshot": "tsx src/daytona-with-snapshot.ts",
|
"start:snapshot": "tsx src/daytona-with-snapshot.ts",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@daytonaio/sdk": "latest",
|
"@daytonaio/sdk": "latest",
|
||||||
"@sandbox-agent/example-shared": "workspace:*"
|
"@sandbox-agent/example-shared": "workspace:*",
|
||||||
|
"sandbox-agent": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "latest",
|
"@types/node": "latest",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Daytona, Image } from "@daytonaio/sdk";
|
import { Daytona, Image } from "@daytonaio/sdk";
|
||||||
import { runPrompt } from "@sandbox-agent/example-shared";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
import { detectAgent, buildInspectorUrl, generateSessionId, waitForHealth } from "@sandbox-agent/example-shared";
|
||||||
|
|
||||||
const daytona = new Daytona();
|
const daytona = new Daytona();
|
||||||
|
|
||||||
|
|
@ -24,12 +25,21 @@ await sandbox.process.executeCommand(
|
||||||
|
|
||||||
const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;
|
const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;
|
||||||
|
|
||||||
|
console.log("Waiting for server...");
|
||||||
|
await waitForHealth({ baseUrl });
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({ baseUrl });
|
||||||
|
const sessionId = generateSessionId();
|
||||||
|
await client.createSession(sessionId, { agent: detectAgent() });
|
||||||
|
|
||||||
|
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
|
||||||
|
console.log(" Press Ctrl+C to stop.");
|
||||||
|
|
||||||
|
const keepAlive = setInterval(() => {}, 60_000);
|
||||||
const cleanup = async () => {
|
const cleanup = async () => {
|
||||||
|
clearInterval(keepAlive);
|
||||||
await sandbox.delete(60);
|
await sandbox.delete(60);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
process.once("SIGINT", cleanup);
|
process.once("SIGINT", cleanup);
|
||||||
process.once("SIGTERM", cleanup);
|
process.once("SIGTERM", cleanup);
|
||||||
|
|
||||||
await runPrompt(baseUrl);
|
|
||||||
await cleanup();
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Daytona } from "@daytonaio/sdk";
|
import { Daytona } from "@daytonaio/sdk";
|
||||||
import { runPrompt } from "@sandbox-agent/example-shared";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
import { detectAgent, buildInspectorUrl, generateSessionId, waitForHealth } from "@sandbox-agent/example-shared";
|
||||||
|
|
||||||
const daytona = new Daytona();
|
const daytona = new Daytona();
|
||||||
|
|
||||||
|
|
@ -25,12 +26,21 @@ await sandbox.process.executeCommand(
|
||||||
|
|
||||||
const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;
|
const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;
|
||||||
|
|
||||||
|
console.log("Waiting for server...");
|
||||||
|
await waitForHealth({ baseUrl });
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({ baseUrl });
|
||||||
|
const sessionId = generateSessionId();
|
||||||
|
await client.createSession(sessionId, { agent: detectAgent() });
|
||||||
|
|
||||||
|
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
|
||||||
|
console.log(" Press Ctrl+C to stop.");
|
||||||
|
|
||||||
|
const keepAlive = setInterval(() => {}, 60_000);
|
||||||
const cleanup = async () => {
|
const cleanup = async () => {
|
||||||
|
clearInterval(keepAlive);
|
||||||
await sandbox.delete(60);
|
await sandbox.delete(60);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
process.once("SIGINT", cleanup);
|
process.once("SIGINT", cleanup);
|
||||||
process.once("SIGTERM", cleanup);
|
process.once("SIGTERM", cleanup);
|
||||||
|
|
||||||
await runPrompt(baseUrl);
|
|
||||||
await cleanup();
|
|
||||||
|
|
@ -3,12 +3,13 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "tsx src/docker.ts",
|
"start": "tsx src/index.ts",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sandbox-agent/example-shared": "workspace:*",
|
"@sandbox-agent/example-shared": "workspace:*",
|
||||||
"dockerode": "latest"
|
"dockerode": "latest",
|
||||||
|
"sandbox-agent": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/dockerode": "latest",
|
"@types/dockerode": "latest",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import Docker from "dockerode";
|
import Docker from "dockerode";
|
||||||
import { runPrompt, waitForHealth } from "@sandbox-agent/example-shared";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
import { detectAgent, buildInspectorUrl, generateSessionId, waitForHealth } from "@sandbox-agent/example-shared";
|
||||||
|
|
||||||
const IMAGE = "alpine:latest";
|
const IMAGE = "alpine:latest";
|
||||||
const PORT = 3000;
|
const PORT = 3000;
|
||||||
|
|
@ -44,13 +45,19 @@ await container.start();
|
||||||
const baseUrl = `http://127.0.0.1:${PORT}`;
|
const baseUrl = `http://127.0.0.1:${PORT}`;
|
||||||
await waitForHealth({ baseUrl });
|
await waitForHealth({ baseUrl });
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({ baseUrl });
|
||||||
|
const sessionId = generateSessionId();
|
||||||
|
await client.createSession(sessionId, { agent: detectAgent() });
|
||||||
|
|
||||||
|
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
|
||||||
|
console.log(" Press Ctrl+C to stop.");
|
||||||
|
|
||||||
|
const keepAlive = setInterval(() => {}, 60_000);
|
||||||
const cleanup = async () => {
|
const cleanup = async () => {
|
||||||
|
clearInterval(keepAlive);
|
||||||
try { await container.stop({ t: 5 }); } catch {}
|
try { await container.stop({ t: 5 }); } catch {}
|
||||||
try { await container.remove({ force: true }); } catch {}
|
try { await container.remove({ force: true }); } catch {}
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
process.once("SIGINT", cleanup);
|
process.once("SIGINT", cleanup);
|
||||||
process.once("SIGTERM", cleanup);
|
process.once("SIGTERM", cleanup);
|
||||||
|
|
||||||
await runPrompt(baseUrl);
|
|
||||||
await cleanup();
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "tsx src/e2b.ts",
|
"start": "tsx src/index.ts",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Sandbox } from "@e2b/code-interpreter";
|
import { Sandbox } from "@e2b/code-interpreter";
|
||||||
import { runPrompt, waitForHealth } from "@sandbox-agent/example-shared";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
import { detectAgent, buildInspectorUrl, generateSessionId, waitForHealth } from "@sandbox-agent/example-shared";
|
||||||
|
|
||||||
const envs: Record<string, string> = {};
|
const envs: Record<string, string> = {};
|
||||||
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||||
|
|
@ -29,12 +30,18 @@ const baseUrl = `https://${sandbox.getHost(3000)}`;
|
||||||
console.log("Waiting for server...");
|
console.log("Waiting for server...");
|
||||||
await waitForHealth({ baseUrl });
|
await waitForHealth({ baseUrl });
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({ baseUrl });
|
||||||
|
const sessionId = generateSessionId();
|
||||||
|
await client.createSession(sessionId, { agent: detectAgent() });
|
||||||
|
|
||||||
|
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
|
||||||
|
console.log(" Press Ctrl+C to stop.");
|
||||||
|
|
||||||
|
const keepAlive = setInterval(() => {}, 60_000);
|
||||||
const cleanup = async () => {
|
const cleanup = async () => {
|
||||||
|
clearInterval(keepAlive);
|
||||||
await sandbox.kill();
|
await sandbox.kill();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
process.once("SIGINT", cleanup);
|
process.once("SIGINT", cleanup);
|
||||||
process.once("SIGTERM", cleanup);
|
process.once("SIGTERM", cleanup);
|
||||||
|
|
||||||
await runPrompt(baseUrl);
|
|
||||||
await cleanup();
|
|
||||||
19
examples/file-system/package.json
Normal file
19
examples/file-system/package.json
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "@sandbox-agent/example-file-system",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "tsx src/index.ts",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sandbox-agent/example-shared": "workspace:*",
|
||||||
|
"sandbox-agent": "workspace:*",
|
||||||
|
"tar": "^7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "latest",
|
||||||
|
"tsx": "latest",
|
||||||
|
"typescript": "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
57
examples/file-system/src/index.ts
Normal file
57
examples/file-system/src/index.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
import { detectAgent, buildInspectorUrl, generateSessionId } from "@sandbox-agent/example-shared";
|
||||||
|
import { startDockerSandbox } from "@sandbox-agent/example-shared/docker";
|
||||||
|
import * as tar from "tar";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
console.log("Starting sandbox...");
|
||||||
|
const { baseUrl, cleanup } = await startDockerSandbox({ port: 3003 });
|
||||||
|
|
||||||
|
console.log("Creating sample files...");
|
||||||
|
const tmpDir = path.resolve(__dirname, "../.tmp-upload");
|
||||||
|
const projectDir = path.join(tmpDir, "my-project");
|
||||||
|
fs.mkdirSync(path.join(projectDir, "src"), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(projectDir, "README.md"), "# My Project\n\nUploaded via batch tar.\n");
|
||||||
|
fs.writeFileSync(path.join(projectDir, "src", "index.ts"), 'console.log("hello from uploaded project");\n');
|
||||||
|
fs.writeFileSync(path.join(projectDir, "package.json"), JSON.stringify({ name: "my-project", version: "1.0.0" }, null, 2) + "\n");
|
||||||
|
console.log(" Created 3 files in my-project/");
|
||||||
|
|
||||||
|
console.log("Uploading files via batch tar...");
|
||||||
|
const client = await SandboxAgent.connect({ baseUrl });
|
||||||
|
|
||||||
|
const tarPath = path.join(tmpDir, "upload.tar");
|
||||||
|
await tar.create(
|
||||||
|
{ file: tarPath, cwd: tmpDir },
|
||||||
|
["my-project"],
|
||||||
|
);
|
||||||
|
const tarBuffer = await fs.promises.readFile(tarPath);
|
||||||
|
const uploadResult = await client.uploadFsBatch(tarBuffer, { path: "/opt" });
|
||||||
|
console.log(` Uploaded ${uploadResult.paths.length} files: ${uploadResult.paths.join(", ")}`);
|
||||||
|
|
||||||
|
// Cleanup temp files
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
|
||||||
|
console.log("Verifying uploaded files...");
|
||||||
|
const entries = await client.listFsEntries({ path: "/opt/my-project" });
|
||||||
|
console.log(` Found ${entries.length} entries in /opt/my-project`);
|
||||||
|
for (const entry of entries) {
|
||||||
|
console.log(` ${entry.entryType === "directory" ? "d" : "-"} ${entry.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const readmeBytes = await client.readFsFile({ path: "/opt/my-project/README.md" });
|
||||||
|
const readmeText = new TextDecoder().decode(readmeBytes);
|
||||||
|
console.log(` README.md content: ${readmeText.trim()}`);
|
||||||
|
|
||||||
|
console.log("Creating session...");
|
||||||
|
const sessionId = generateSessionId();
|
||||||
|
await client.createSession(sessionId, { agent: detectAgent() });
|
||||||
|
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
|
||||||
|
console.log(' Try: "read the README in /opt/my-project"');
|
||||||
|
console.log(" Press Ctrl+C to stop.");
|
||||||
|
|
||||||
|
const keepAlive = setInterval(() => {}, 60_000);
|
||||||
|
process.on("SIGINT", () => { clearInterval(keepAlive); cleanup().then(() => process.exit(0)); });
|
||||||
16
examples/file-system/tsconfig.json
Normal file
16
examples/file-system/tsconfig.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022", "DOM"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "**/*.test.ts"]
|
||||||
|
}
|
||||||
22
examples/mcp-custom-tool/package.json
Normal file
22
examples/mcp-custom-tool/package.json
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"name": "@sandbox-agent/example-mcp-custom-tool",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build:mcp": "esbuild src/mcp-server.ts --bundle --format=cjs --platform=node --target=node18 --minify --outfile=dist/mcp-server.cjs",
|
||||||
|
"start": "pnpm build:mcp && tsx src/index.ts",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "latest",
|
||||||
|
"@sandbox-agent/example-shared": "workspace:*",
|
||||||
|
"sandbox-agent": "workspace:*",
|
||||||
|
"zod": "latest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "latest",
|
||||||
|
"esbuild": "latest",
|
||||||
|
"tsx": "latest",
|
||||||
|
"typescript": "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
49
examples/mcp-custom-tool/src/index.ts
Normal file
49
examples/mcp-custom-tool/src/index.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
import { detectAgent, buildInspectorUrl, generateSessionId } from "@sandbox-agent/example-shared";
|
||||||
|
import { startDockerSandbox } from "@sandbox-agent/example-shared/docker";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
// Verify the bundled MCP server exists (built by `pnpm build:mcp`).
|
||||||
|
const serverFile = path.resolve(__dirname, "../dist/mcp-server.cjs");
|
||||||
|
if (!fs.existsSync(serverFile)) {
|
||||||
|
console.error("Error: dist/mcp-server.cjs not found. Run `pnpm build:mcp` first.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a Docker container running sandbox-agent.
|
||||||
|
console.log("Starting sandbox...");
|
||||||
|
const { baseUrl, cleanup } = await startDockerSandbox({ port: 3004 });
|
||||||
|
|
||||||
|
// Upload the bundled MCP server into the sandbox filesystem.
|
||||||
|
console.log("Uploading MCP server bundle...");
|
||||||
|
const client = await SandboxAgent.connect({ baseUrl });
|
||||||
|
|
||||||
|
const bundle = await fs.promises.readFile(serverFile);
|
||||||
|
const written = await client.writeFsFile(
|
||||||
|
{ path: "/opt/mcp/custom-tools/mcp-server.cjs" },
|
||||||
|
bundle,
|
||||||
|
);
|
||||||
|
console.log(` Written: ${written.path} (${written.bytesWritten} bytes)`);
|
||||||
|
|
||||||
|
// Create a session with the uploaded MCP server as a local command.
|
||||||
|
console.log("Creating session with custom MCP tool...");
|
||||||
|
const sessionId = generateSessionId();
|
||||||
|
await client.createSession(sessionId, {
|
||||||
|
agent: detectAgent(),
|
||||||
|
mcp: {
|
||||||
|
customTools: {
|
||||||
|
type: "local",
|
||||||
|
command: ["node", "/opt/mcp/custom-tools/mcp-server.cjs"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
|
||||||
|
console.log(' Try: "generate a random number between 1 and 100"');
|
||||||
|
console.log(" Press Ctrl+C to stop.");
|
||||||
|
|
||||||
|
const keepAlive = setInterval(() => {}, 60_000);
|
||||||
|
process.on("SIGINT", () => { clearInterval(keepAlive); cleanup().then(() => process.exit(0)); });
|
||||||
24
examples/mcp-custom-tool/src/mcp-server.ts
Normal file
24
examples/mcp-custom-tool/src/mcp-server.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const server = new McpServer({ name: "rand", version: "1.0.0" });
|
||||||
|
|
||||||
|
server.tool(
|
||||||
|
"random_number",
|
||||||
|
"Generate a random integer between min and max (inclusive)",
|
||||||
|
{
|
||||||
|
min: z.number().describe("Minimum value"),
|
||||||
|
max: z.number().describe("Maximum value"),
|
||||||
|
},
|
||||||
|
async ({ min, max }) => ({
|
||||||
|
content: [{ type: "text", text: String(Math.floor(Math.random() * (max - min + 1)) + min) }],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
16
examples/mcp-custom-tool/tsconfig.json
Normal file
16
examples/mcp-custom-tool/tsconfig.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022", "DOM"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "**/*.test.ts"]
|
||||||
|
}
|
||||||
18
examples/mcp/package.json
Normal file
18
examples/mcp/package.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"name": "@sandbox-agent/example-mcp",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "tsx src/index.ts",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sandbox-agent/example-shared": "workspace:*",
|
||||||
|
"sandbox-agent": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "latest",
|
||||||
|
"tsx": "latest",
|
||||||
|
"typescript": "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
31
examples/mcp/src/index.ts
Normal file
31
examples/mcp/src/index.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
import { detectAgent, buildInspectorUrl, generateSessionId } from "@sandbox-agent/example-shared";
|
||||||
|
import { startDockerSandbox } from "@sandbox-agent/example-shared/docker";
|
||||||
|
|
||||||
|
console.log("Starting sandbox...");
|
||||||
|
const { baseUrl, cleanup } = await startDockerSandbox({
|
||||||
|
port: 3002,
|
||||||
|
setupCommands: [
|
||||||
|
"npm install -g --silent @modelcontextprotocol/server-everything@2026.1.26",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Creating session with everything MCP server...");
|
||||||
|
const client = await SandboxAgent.connect({ baseUrl });
|
||||||
|
const sessionId = generateSessionId();
|
||||||
|
await client.createSession(sessionId, {
|
||||||
|
agent: detectAgent(),
|
||||||
|
mcp: {
|
||||||
|
everything: {
|
||||||
|
type: "local",
|
||||||
|
command: ["mcp-server-everything"],
|
||||||
|
timeoutMs: 10000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
|
||||||
|
console.log(' Try: "generate a random number between 1 and 100"');
|
||||||
|
console.log(" Press Ctrl+C to stop.");
|
||||||
|
|
||||||
|
const keepAlive = setInterval(() => {}, 60_000);
|
||||||
|
process.on("SIGINT", () => { clearInterval(keepAlive); cleanup().then(() => process.exit(0)); });
|
||||||
16
examples/mcp/tsconfig.json
Normal file
16
examples/mcp/tsconfig.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022", "DOM"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "**/*.test.ts"]
|
||||||
|
}
|
||||||
5
examples/shared/Dockerfile
Normal file
5
examples/shared/Dockerfile
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
FROM node:22-bookworm-slim
|
||||||
|
RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends ca-certificates > /dev/null 2>&1 && \
|
||||||
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
npm install -g --silent @sandbox-agent/cli@latest && \
|
||||||
|
sandbox-agent install-agent claude
|
||||||
58
examples/shared/Dockerfile.dev
Normal file
58
examples/shared/Dockerfile.dev
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
FROM node:22-bookworm-slim AS frontend
|
||||||
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Copy workspace root config
|
||||||
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
|
|
||||||
|
# Copy packages needed for the inspector build chain:
|
||||||
|
# inspector -> sandbox-agent SDK -> cli-shared
|
||||||
|
COPY sdks/typescript/ sdks/typescript/
|
||||||
|
COPY sdks/cli-shared/ sdks/cli-shared/
|
||||||
|
COPY frontend/packages/inspector/ frontend/packages/inspector/
|
||||||
|
COPY docs/openapi.json docs/
|
||||||
|
|
||||||
|
# Create stub package.json for workspace packages referenced in pnpm-workspace.yaml
|
||||||
|
# but not needed for the inspector build (avoids install errors).
|
||||||
|
RUN set -e; for dir in \
|
||||||
|
sdks/cli sdks/gigacode \
|
||||||
|
resources/agent-schemas resources/vercel-ai-sdk-schemas \
|
||||||
|
scripts/release scripts/sandbox-testing \
|
||||||
|
examples/shared examples/docker examples/e2b examples/vercel \
|
||||||
|
examples/daytona examples/cloudflare examples/file-system \
|
||||||
|
examples/mcp examples/mcp-custom-tool \
|
||||||
|
examples/skills examples/skills-custom-tool \
|
||||||
|
frontend/packages/website; do \
|
||||||
|
mkdir -p "$dir"; \
|
||||||
|
printf '{"name":"@stub/%s","private":true,"version":"0.0.0"}\n' "$(basename "$dir")" > "$dir/package.json"; \
|
||||||
|
done; \
|
||||||
|
for parent in sdks/cli/platforms sdks/gigacode/platforms; do \
|
||||||
|
for plat in darwin-arm64 darwin-x64 linux-arm64 linux-x64 win32-x64; do \
|
||||||
|
mkdir -p "$parent/$plat"; \
|
||||||
|
printf '{"name":"@stub/%s-%s","private":true,"version":"0.0.0"}\n' "$(basename "$parent")" "$plat" > "$parent/$plat/package.json"; \
|
||||||
|
done; \
|
||||||
|
done
|
||||||
|
|
||||||
|
RUN pnpm install --no-frozen-lockfile
|
||||||
|
ENV SKIP_OPENAPI_GEN=1
|
||||||
|
RUN pnpm --filter sandbox-agent build && \
|
||||||
|
pnpm --filter @sandbox-agent/inspector build
|
||||||
|
|
||||||
|
FROM rust:1.88.0-bookworm AS builder
|
||||||
|
WORKDIR /build
|
||||||
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
COPY server/ ./server/
|
||||||
|
COPY gigacode/ ./gigacode/
|
||||||
|
COPY resources/agent-schemas/artifacts/ ./resources/agent-schemas/artifacts/
|
||||||
|
COPY --from=frontend /build/frontend/packages/inspector/dist/ ./frontend/packages/inspector/dist/
|
||||||
|
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||||
|
--mount=type=cache,target=/usr/local/cargo/git \
|
||||||
|
--mount=type=cache,target=/build/target \
|
||||||
|
cargo build -p sandbox-agent --release && \
|
||||||
|
cp target/release/sandbox-agent /sandbox-agent
|
||||||
|
|
||||||
|
FROM node:22-bookworm-slim
|
||||||
|
RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends ca-certificates > /dev/null 2>&1 && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
COPY --from=builder /sandbox-agent /usr/local/bin/sandbox-agent
|
||||||
|
RUN sandbox-agent install-agent claude
|
||||||
|
|
@ -3,15 +3,18 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/sandbox-agent-client.ts"
|
".": "./src/sandbox-agent-client.ts",
|
||||||
|
"./docker": "./src/docker.ts"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"dockerode": "latest",
|
||||||
"sandbox-agent": "workspace:*"
|
"sandbox-agent": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/dockerode": "latest",
|
||||||
"@types/node": "latest",
|
"@types/node": "latest",
|
||||||
"typescript": "latest"
|
"typescript": "latest"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
301
examples/shared/src/docker.ts
Normal file
301
examples/shared/src/docker.ts
Normal file
|
|
@ -0,0 +1,301 @@
|
||||||
|
import Docker from "dockerode";
|
||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { PassThrough } from "node:stream";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { waitForHealth } from "./sandbox-agent-client.ts";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const EXAMPLE_IMAGE = "sandbox-agent-examples:latest";
|
||||||
|
const EXAMPLE_IMAGE_DEV = "sandbox-agent-examples-dev:latest";
|
||||||
|
const DOCKERFILE_DIR = path.resolve(__dirname, "..");
|
||||||
|
const REPO_ROOT = path.resolve(DOCKERFILE_DIR, "../..");
|
||||||
|
|
||||||
|
export interface DockerSandboxOptions {
|
||||||
|
/** Container port used by sandbox-agent inside Docker. */
|
||||||
|
port: number;
|
||||||
|
/** Optional fixed host port mapping. If omitted, Docker assigns a free host port automatically. */
|
||||||
|
hostPort?: number;
|
||||||
|
/** Additional shell commands to run before starting sandbox-agent. */
|
||||||
|
setupCommands?: string[];
|
||||||
|
/** Docker image to use. Defaults to the pre-built sandbox-agent-examples image. */
|
||||||
|
image?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DockerSandbox {
|
||||||
|
baseUrl: string;
|
||||||
|
cleanup: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DIRECT_CREDENTIAL_KEYS = [
|
||||||
|
"ANTHROPIC_API_KEY",
|
||||||
|
"CLAUDE_API_KEY",
|
||||||
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
||||||
|
"ANTHROPIC_AUTH_TOKEN",
|
||||||
|
"OPENAI_API_KEY",
|
||||||
|
"CODEX_API_KEY",
|
||||||
|
"CEREBRAS_API_KEY",
|
||||||
|
"OPENCODE_API_KEY",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function stripShellQuotes(value: string): string {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (trimmed.length >= 2 && trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
|
||||||
|
return trimmed.slice(1, -1);
|
||||||
|
}
|
||||||
|
if (trimmed.length >= 2 && trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
||||||
|
return trimmed.slice(1, -1);
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseExtractedCredentials(output: string): Record<string, string> {
|
||||||
|
const parsed: Record<string, string> = {};
|
||||||
|
for (const rawLine of output.split("\n")) {
|
||||||
|
const line = rawLine.trim();
|
||||||
|
if (!line) continue;
|
||||||
|
const cleanLine = line.startsWith("export ") ? line.slice(7) : line;
|
||||||
|
const match = cleanLine.match(/^([A-Z0-9_]+)=(.*)$/);
|
||||||
|
if (!match) continue;
|
||||||
|
const [, key, rawValue] = match;
|
||||||
|
const value = stripShellQuotes(rawValue);
|
||||||
|
if (!value) continue;
|
||||||
|
parsed[key] = value;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClaudeCredentialFile {
|
||||||
|
hostPath: string;
|
||||||
|
containerPath: string;
|
||||||
|
base64Content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readClaudeCredentialFiles(): ClaudeCredentialFile[] {
|
||||||
|
const homeDir = process.env.HOME || "";
|
||||||
|
if (!homeDir) return [];
|
||||||
|
|
||||||
|
const candidates: Array<{ hostPath: string; containerPath: string }> = [
|
||||||
|
{
|
||||||
|
hostPath: path.join(homeDir, ".claude", ".credentials.json"),
|
||||||
|
containerPath: "/root/.claude/.credentials.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostPath: path.join(homeDir, ".claude-oauth-credentials.json"),
|
||||||
|
containerPath: "/root/.claude-oauth-credentials.json",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const files: ClaudeCredentialFile[] = [];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (!fs.existsSync(candidate.hostPath)) continue;
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(candidate.hostPath, "utf8");
|
||||||
|
files.push({
|
||||||
|
hostPath: candidate.hostPath,
|
||||||
|
containerPath: candidate.containerPath,
|
||||||
|
base64Content: Buffer.from(raw, "utf8").toString("base64"),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Ignore unreadable credential file candidates.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectCredentialEnv(): Record<string, string> {
|
||||||
|
const merged: Record<string, string> = {};
|
||||||
|
let extracted: Record<string, string> = {};
|
||||||
|
try {
|
||||||
|
const output = execFileSync(
|
||||||
|
"sandbox-agent",
|
||||||
|
["credentials", "extract-env"],
|
||||||
|
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
|
||||||
|
);
|
||||||
|
extracted = parseExtractedCredentials(output);
|
||||||
|
} catch {
|
||||||
|
// Fall back to direct env vars if extraction is unavailable.
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(extracted)) {
|
||||||
|
if (value) merged[key] = value;
|
||||||
|
}
|
||||||
|
for (const key of DIRECT_CREDENTIAL_KEYS) {
|
||||||
|
const direct = process.env[key];
|
||||||
|
if (direct) merged[key] = direct;
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shellSingleQuotedLiteral(value: string): string {
|
||||||
|
return `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripAnsi(value: string): string {
|
||||||
|
return value.replace(
|
||||||
|
/[\u001B\u009B][[\]()#;?]*(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007|(?:\d{1,4}(?:;\d{0,4})*)?[0-9A-ORZcf-nqry=><])/g,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureExampleImage(_docker: Docker): Promise<string> {
|
||||||
|
const dev = !!process.env.SANDBOX_AGENT_DEV;
|
||||||
|
const imageName = dev ? EXAMPLE_IMAGE_DEV : EXAMPLE_IMAGE;
|
||||||
|
|
||||||
|
if (dev) {
|
||||||
|
console.log(" Building sandbox image from source (may take a while, only runs once)...");
|
||||||
|
try {
|
||||||
|
execFileSync("docker", [
|
||||||
|
"build", "-t", imageName,
|
||||||
|
"-f", path.join(DOCKERFILE_DIR, "Dockerfile.dev"),
|
||||||
|
REPO_ROOT,
|
||||||
|
], {
|
||||||
|
stdio: ["ignore", "ignore", "pipe"],
|
||||||
|
});
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const stderr = err instanceof Error && "stderr" in err ? String((err as { stderr: unknown }).stderr) : "";
|
||||||
|
throw new Error(`Failed to build sandbox image: ${stderr}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(" Building sandbox image (may take a while, only runs once)...");
|
||||||
|
try {
|
||||||
|
execFileSync("docker", ["build", "-t", imageName, DOCKERFILE_DIR], {
|
||||||
|
stdio: ["ignore", "ignore", "pipe"],
|
||||||
|
});
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const stderr = err instanceof Error && "stderr" in err ? String((err as { stderr: unknown }).stderr) : "";
|
||||||
|
throw new Error(`Failed to build sandbox image: ${stderr}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return imageName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a Docker container running sandbox-agent and wait for it to be healthy.
|
||||||
|
* Registers SIGINT/SIGTERM handlers for cleanup.
|
||||||
|
*/
|
||||||
|
export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<DockerSandbox> {
|
||||||
|
const { port, hostPort } = opts;
|
||||||
|
const useCustomImage = !!opts.image;
|
||||||
|
let image = opts.image ?? EXAMPLE_IMAGE;
|
||||||
|
// TODO: Replace setupCommands shell bootstrapping with native sandbox-agent exec API once available.
|
||||||
|
const setupCommands = [...(opts.setupCommands ?? [])];
|
||||||
|
const credentialEnv = collectCredentialEnv();
|
||||||
|
const claudeCredentialFiles = readClaudeCredentialFiles();
|
||||||
|
const bootstrapEnv: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (claudeCredentialFiles.length > 0) {
|
||||||
|
delete credentialEnv.ANTHROPIC_API_KEY;
|
||||||
|
delete credentialEnv.CLAUDE_API_KEY;
|
||||||
|
delete credentialEnv.CLAUDE_CODE_OAUTH_TOKEN;
|
||||||
|
delete credentialEnv.ANTHROPIC_AUTH_TOKEN;
|
||||||
|
|
||||||
|
const credentialBootstrapCommands = claudeCredentialFiles.flatMap((file, index) => {
|
||||||
|
const envKey = `SANDBOX_AGENT_CLAUDE_CREDENTIAL_${index}_B64`;
|
||||||
|
bootstrapEnv[envKey] = file.base64Content;
|
||||||
|
return [
|
||||||
|
`mkdir -p ${shellSingleQuotedLiteral(path.posix.dirname(file.containerPath))}`,
|
||||||
|
`printf %s "$${envKey}" | base64 -d > ${shellSingleQuotedLiteral(file.containerPath)}`,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
setupCommands.unshift(...credentialBootstrapCommands);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(credentialEnv)) {
|
||||||
|
if (!process.env[key]) process.env[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const docker = new Docker({ socketPath: "/var/run/docker.sock" });
|
||||||
|
|
||||||
|
if (useCustomImage) {
|
||||||
|
try {
|
||||||
|
await docker.getImage(image).inspect();
|
||||||
|
} catch {
|
||||||
|
console.log(` Pulling ${image}...`);
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
docker.pull(image, (err: Error | null, stream: NodeJS.ReadableStream) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
docker.modem.followProgress(stream, (err: Error | null) => (err ? reject(err) : resolve()));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
image = await ensureExampleImage(docker);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bootCommands = [
|
||||||
|
...setupCommands,
|
||||||
|
`sandbox-agent server --no-token --host 0.0.0.0 --port ${port}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
const container = await docker.createContainer({
|
||||||
|
Image: image,
|
||||||
|
WorkingDir: "/root",
|
||||||
|
Cmd: ["sh", "-c", bootCommands.join(" && ")],
|
||||||
|
Env: [
|
||||||
|
...Object.entries(credentialEnv).map(([key, value]) => `${key}=${value}`),
|
||||||
|
...Object.entries(bootstrapEnv).map(([key, value]) => `${key}=${value}`),
|
||||||
|
],
|
||||||
|
ExposedPorts: { [`${port}/tcp`]: {} },
|
||||||
|
HostConfig: {
|
||||||
|
AutoRemove: true,
|
||||||
|
PortBindings: { [`${port}/tcp`]: [{ HostPort: hostPort ? `${hostPort}` : "0" }] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await container.start();
|
||||||
|
|
||||||
|
const logChunks: string[] = [];
|
||||||
|
const startupLogs = await container.logs({
|
||||||
|
follow: true,
|
||||||
|
stdout: true,
|
||||||
|
stderr: true,
|
||||||
|
since: 0,
|
||||||
|
}) as NodeJS.ReadableStream;
|
||||||
|
const stdoutStream = new PassThrough();
|
||||||
|
const stderrStream = new PassThrough();
|
||||||
|
stdoutStream.on("data", (chunk) => {
|
||||||
|
logChunks.push(stripAnsi(String(chunk)));
|
||||||
|
});
|
||||||
|
stderrStream.on("data", (chunk) => {
|
||||||
|
logChunks.push(stripAnsi(String(chunk)));
|
||||||
|
});
|
||||||
|
docker.modem.demuxStream(startupLogs, stdoutStream, stderrStream);
|
||||||
|
const stopStartupLogs = () => {
|
||||||
|
const stream = startupLogs as NodeJS.ReadableStream & { destroy?: () => void };
|
||||||
|
try { stream.destroy?.(); } catch {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const inspect = await container.inspect();
|
||||||
|
const mappedPorts = inspect.NetworkSettings?.Ports?.[`${port}/tcp`];
|
||||||
|
const mappedHostPort = mappedPorts?.[0]?.HostPort;
|
||||||
|
if (!mappedHostPort) {
|
||||||
|
throw new Error(`Failed to resolve mapped host port for container port ${port}`);
|
||||||
|
}
|
||||||
|
const baseUrl = `http://127.0.0.1:${mappedHostPort}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await waitForHealth({ baseUrl });
|
||||||
|
} catch (err) {
|
||||||
|
stopStartupLogs();
|
||||||
|
console.error(" Container logs:");
|
||||||
|
for (const chunk of logChunks) {
|
||||||
|
process.stderr.write(` ${chunk}`);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
stopStartupLogs();
|
||||||
|
console.log(` Ready (${baseUrl})`);
|
||||||
|
|
||||||
|
const cleanup = async () => {
|
||||||
|
stopStartupLogs();
|
||||||
|
try { await container.stop({ t: 5 }); } catch {}
|
||||||
|
try { await container.remove({ force: true }); } catch {}
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
process.once("SIGINT", cleanup);
|
||||||
|
process.once("SIGTERM", cleanup);
|
||||||
|
|
||||||
|
return { baseUrl, cleanup };
|
||||||
|
}
|
||||||
|
|
@ -3,11 +3,7 @@
|
||||||
* Provides minimal helpers for connecting to and interacting with sandbox-agent servers.
|
* Provides minimal helpers for connecting to and interacting with sandbox-agent servers.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createInterface } from "node:readline/promises";
|
|
||||||
import { randomUUID } from "node:crypto";
|
|
||||||
import { setTimeout as delay } from "node:timers/promises";
|
import { setTimeout as delay } from "node:timers/promises";
|
||||||
import { SandboxAgent } from "sandbox-agent";
|
|
||||||
import type { PermissionEventData, QuestionEventData } from "sandbox-agent";
|
|
||||||
|
|
||||||
function normalizeBaseUrl(baseUrl: string): string {
|
function normalizeBaseUrl(baseUrl: string): string {
|
||||||
return baseUrl.replace(/\/+$/, "");
|
return baseUrl.replace(/\/+$/, "");
|
||||||
|
|
@ -27,10 +23,12 @@ export function buildInspectorUrl({
|
||||||
baseUrl,
|
baseUrl,
|
||||||
token,
|
token,
|
||||||
headers,
|
headers,
|
||||||
|
sessionId,
|
||||||
}: {
|
}: {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
|
sessionId?: string;
|
||||||
}): string {
|
}): string {
|
||||||
const normalized = normalizeBaseUrl(ensureUrl(baseUrl));
|
const normalized = normalizeBaseUrl(ensureUrl(baseUrl));
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
@ -41,7 +39,8 @@ export function buildInspectorUrl({
|
||||||
params.set("headers", JSON.stringify(headers));
|
params.set("headers", JSON.stringify(headers));
|
||||||
}
|
}
|
||||||
const queryString = params.toString();
|
const queryString = params.toString();
|
||||||
return `${normalized}/ui/${queryString ? `?${queryString}` : ""}`;
|
const sessionPath = sessionId ? `sessions/${sessionId}` : "";
|
||||||
|
return `${normalized}/ui/${sessionPath}${queryString ? `?${queryString}` : ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logInspectorUrl({
|
export function logInspectorUrl({
|
||||||
|
|
@ -110,125 +109,39 @@ export async function waitForHealth({
|
||||||
throw (lastError ?? new Error("Timed out waiting for /v1/health")) as Error;
|
throw (lastError ?? new Error("Timed out waiting for /v1/health")) as Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectAgent(): string {
|
export function generateSessionId(): string {
|
||||||
|
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
let id = "session-";
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
id += chars[Math.floor(Math.random() * chars.length)];
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function detectAgent(): string {
|
||||||
if (process.env.SANDBOX_AGENT) return process.env.SANDBOX_AGENT;
|
if (process.env.SANDBOX_AGENT) return process.env.SANDBOX_AGENT;
|
||||||
if (process.env.ANTHROPIC_API_KEY) return "claude";
|
const hasClaude = Boolean(
|
||||||
if (process.env.OPENAI_API_KEY) return "codex";
|
process.env.ANTHROPIC_API_KEY ||
|
||||||
|
process.env.CLAUDE_API_KEY ||
|
||||||
|
process.env.CLAUDE_CODE_OAUTH_TOKEN ||
|
||||||
|
process.env.ANTHROPIC_AUTH_TOKEN,
|
||||||
|
);
|
||||||
|
const openAiLikeKey = process.env.OPENAI_API_KEY || process.env.CODEX_API_KEY || "";
|
||||||
|
const hasCodexApiKey = openAiLikeKey.startsWith("sk-");
|
||||||
|
if (hasCodexApiKey && hasClaude) {
|
||||||
|
console.log("Both Claude and Codex API keys detected; defaulting to codex. Set SANDBOX_AGENT to override.");
|
||||||
|
return "codex";
|
||||||
|
}
|
||||||
|
if (!hasCodexApiKey && openAiLikeKey) {
|
||||||
|
console.log("OpenAI/Codex credential is not an API key (expected sk-...), skipping codex auto-select.");
|
||||||
|
}
|
||||||
|
if (hasCodexApiKey) return "codex";
|
||||||
|
if (hasClaude) {
|
||||||
|
if (openAiLikeKey && !hasCodexApiKey) {
|
||||||
|
console.log("Using claude by default.");
|
||||||
|
}
|
||||||
|
return "claude";
|
||||||
|
}
|
||||||
return "claude";
|
return "claude";
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runPrompt(baseUrl: string): Promise<void> {
|
|
||||||
console.log(`UI: ${buildInspectorUrl({ baseUrl })}`);
|
|
||||||
|
|
||||||
const client = await SandboxAgent.connect({ baseUrl });
|
|
||||||
|
|
||||||
const agent = detectAgent();
|
|
||||||
console.log(`Using agent: ${agent}`);
|
|
||||||
const sessionId = randomUUID();
|
|
||||||
await client.createSession(sessionId, { agent });
|
|
||||||
console.log(`Session ${sessionId}. Press Ctrl+C to quit.`);
|
|
||||||
|
|
||||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
||||||
|
|
||||||
let isThinking = false;
|
|
||||||
let hasStartedOutput = false;
|
|
||||||
let turnResolve: (() => void) | null = null;
|
|
||||||
let sessionEnded = false;
|
|
||||||
|
|
||||||
const processEvents = async () => {
|
|
||||||
for await (const event of client.streamEvents(sessionId)) {
|
|
||||||
if (event.type === "item.started") {
|
|
||||||
const item = (event.data as any)?.item;
|
|
||||||
if (item?.role === "assistant") {
|
|
||||||
isThinking = true;
|
|
||||||
hasStartedOutput = false;
|
|
||||||
process.stdout.write("Thinking...");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type === "item.delta" && isThinking) {
|
|
||||||
const delta = (event.data as any)?.delta;
|
|
||||||
if (delta) {
|
|
||||||
if (!hasStartedOutput) {
|
|
||||||
process.stdout.write("\r\x1b[K");
|
|
||||||
hasStartedOutput = true;
|
|
||||||
}
|
|
||||||
const text = typeof delta === "string" ? delta : delta.type === "text" ? delta.text || "" : "";
|
|
||||||
if (text) process.stdout.write(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type === "item.completed") {
|
|
||||||
const item = (event.data as any)?.item;
|
|
||||||
if (item?.role === "assistant") {
|
|
||||||
isThinking = false;
|
|
||||||
process.stdout.write("\n");
|
|
||||||
turnResolve?.();
|
|
||||||
turnResolve = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type === "permission.requested") {
|
|
||||||
const data = event.data as PermissionEventData;
|
|
||||||
if (isThinking && !hasStartedOutput) {
|
|
||||||
process.stdout.write("\r\x1b[K");
|
|
||||||
}
|
|
||||||
console.log(`[Auto-approved] ${data.action}`);
|
|
||||||
await client.replyPermission(sessionId, data.permission_id, { reply: "once" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type === "question.requested") {
|
|
||||||
const data = event.data as QuestionEventData;
|
|
||||||
if (isThinking && !hasStartedOutput) {
|
|
||||||
process.stdout.write("\r\x1b[K");
|
|
||||||
}
|
|
||||||
console.log(`[Question rejected] ${data.prompt}`);
|
|
||||||
await client.rejectQuestion(sessionId, data.question_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type === "error") {
|
|
||||||
const data = event.data as any;
|
|
||||||
console.error(`\nError: ${data?.message || JSON.stringify(data)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type === "session.ended") {
|
|
||||||
const data = event.data as any;
|
|
||||||
const reason = data?.reason || "unknown";
|
|
||||||
if (reason === "error") {
|
|
||||||
console.error(`\nAgent exited with error: ${data?.message || ""}`);
|
|
||||||
if (data?.exit_code !== undefined) {
|
|
||||||
console.error(` Exit code: ${data.exit_code}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`Agent session ${reason}`);
|
|
||||||
}
|
|
||||||
sessionEnded = true;
|
|
||||||
turnResolve?.();
|
|
||||||
turnResolve = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
processEvents().catch((err) => {
|
|
||||||
if (!sessionEnded) {
|
|
||||||
console.error("Event stream error:", err instanceof Error ? err.message : err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const line = await rl.question("> ");
|
|
||||||
if (!line.trim()) continue;
|
|
||||||
|
|
||||||
const turnComplete = new Promise<void>((resolve) => {
|
|
||||||
turnResolve = resolve;
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await client.postMessage(sessionId, { message: line.trim() });
|
|
||||||
await turnComplete;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error instanceof Error ? error.message : error);
|
|
||||||
turnResolve = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
12
examples/skills-custom-tool/SKILL.md
Normal file
12
examples/skills-custom-tool/SKILL.md
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
name: random-number
|
||||||
|
description: Generate a random integer between min and max (inclusive). Use when the user asks for a random number.
|
||||||
|
---
|
||||||
|
|
||||||
|
To generate a random number, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node /opt/skills/random-number/random-number.cjs <min> <max>
|
||||||
|
```
|
||||||
|
|
||||||
|
This prints a single random integer between min and max (inclusive).
|
||||||
20
examples/skills-custom-tool/package.json
Normal file
20
examples/skills-custom-tool/package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "@sandbox-agent/example-skills-custom-tool",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build:script": "esbuild src/random-number.ts --bundle --format=cjs --platform=node --target=node18 --minify --outfile=dist/random-number.cjs",
|
||||||
|
"start": "pnpm build:script && tsx src/index.ts",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sandbox-agent/example-shared": "workspace:*",
|
||||||
|
"sandbox-agent": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "latest",
|
||||||
|
"esbuild": "latest",
|
||||||
|
"tsx": "latest",
|
||||||
|
"typescript": "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
53
examples/skills-custom-tool/src/index.ts
Normal file
53
examples/skills-custom-tool/src/index.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
import { detectAgent, buildInspectorUrl, generateSessionId } from "@sandbox-agent/example-shared";
|
||||||
|
import { startDockerSandbox } from "@sandbox-agent/example-shared/docker";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
// Verify the bundled script exists (built by `pnpm build:script`).
|
||||||
|
const scriptFile = path.resolve(__dirname, "../dist/random-number.cjs");
|
||||||
|
if (!fs.existsSync(scriptFile)) {
|
||||||
|
console.error("Error: dist/random-number.cjs not found. Run `pnpm build:script` first.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a Docker container running sandbox-agent.
|
||||||
|
console.log("Starting sandbox...");
|
||||||
|
const { baseUrl, cleanup } = await startDockerSandbox({ port: 3005 });
|
||||||
|
|
||||||
|
// Upload the bundled script and SKILL.md into the sandbox filesystem.
|
||||||
|
console.log("Uploading script and skill file...");
|
||||||
|
const client = await SandboxAgent.connect({ baseUrl });
|
||||||
|
|
||||||
|
const script = await fs.promises.readFile(scriptFile);
|
||||||
|
const scriptResult = await client.writeFsFile(
|
||||||
|
{ path: "/opt/skills/random-number/random-number.cjs" },
|
||||||
|
script,
|
||||||
|
);
|
||||||
|
console.log(` Script: ${scriptResult.path} (${scriptResult.bytesWritten} bytes)`);
|
||||||
|
|
||||||
|
const skillMd = await fs.promises.readFile(path.resolve(__dirname, "../SKILL.md"));
|
||||||
|
const skillResult = await client.writeFsFile(
|
||||||
|
{ path: "/opt/skills/random-number/SKILL.md" },
|
||||||
|
skillMd,
|
||||||
|
);
|
||||||
|
console.log(` Skill: ${skillResult.path} (${skillResult.bytesWritten} bytes)`);
|
||||||
|
|
||||||
|
// Create a session with the uploaded skill as a local source.
|
||||||
|
console.log("Creating session with custom skill...");
|
||||||
|
const sessionId = generateSessionId();
|
||||||
|
await client.createSession(sessionId, {
|
||||||
|
agent: detectAgent(),
|
||||||
|
skills: {
|
||||||
|
sources: [{ type: "local", source: "/opt/skills/random-number" }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
|
||||||
|
console.log(' Try: "generate a random number between 1 and 100"');
|
||||||
|
console.log(" Press Ctrl+C to stop.");
|
||||||
|
|
||||||
|
const keepAlive = setInterval(() => {}, 60_000);
|
||||||
|
process.on("SIGINT", () => { clearInterval(keepAlive); cleanup().then(() => process.exit(0)); });
|
||||||
9
examples/skills-custom-tool/src/random-number.ts
Normal file
9
examples/skills-custom-tool/src/random-number.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
const min = Number(process.argv[2]);
|
||||||
|
const max = Number(process.argv[3]);
|
||||||
|
|
||||||
|
if (Number.isNaN(min) || Number.isNaN(max)) {
|
||||||
|
console.error("Usage: random-number <min> <max>");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(Math.floor(Math.random() * (max - min + 1)) + min);
|
||||||
16
examples/skills-custom-tool/tsconfig.json
Normal file
16
examples/skills-custom-tool/tsconfig.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022", "DOM"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "**/*.test.ts"]
|
||||||
|
}
|
||||||
18
examples/skills/package.json
Normal file
18
examples/skills/package.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"name": "@sandbox-agent/example-skills",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "tsx src/index.ts",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sandbox-agent/example-shared": "workspace:*",
|
||||||
|
"sandbox-agent": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "latest",
|
||||||
|
"tsx": "latest",
|
||||||
|
"typescript": "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
26
examples/skills/src/index.ts
Normal file
26
examples/skills/src/index.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
import { detectAgent, buildInspectorUrl, generateSessionId } from "@sandbox-agent/example-shared";
|
||||||
|
import { startDockerSandbox } from "@sandbox-agent/example-shared/docker";
|
||||||
|
|
||||||
|
console.log("Starting sandbox...");
|
||||||
|
const { baseUrl, cleanup } = await startDockerSandbox({
|
||||||
|
port: 3001,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Creating session with skill source...");
|
||||||
|
const client = await SandboxAgent.connect({ baseUrl });
|
||||||
|
const sessionId = generateSessionId();
|
||||||
|
await client.createSession(sessionId, {
|
||||||
|
agent: detectAgent(),
|
||||||
|
skills: {
|
||||||
|
sources: [
|
||||||
|
{ type: "github", source: "rivet-dev/skills", skills: ["sandbox-agent"] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
|
||||||
|
console.log(' Try: "How do I start sandbox-agent?"');
|
||||||
|
console.log(" Press Ctrl+C to stop.");
|
||||||
|
|
||||||
|
const keepAlive = setInterval(() => {}, 60_000);
|
||||||
|
process.on("SIGINT", () => { clearInterval(keepAlive); cleanup().then(() => process.exit(0)); });
|
||||||
16
examples/skills/tsconfig.json
Normal file
16
examples/skills/tsconfig.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022", "DOM"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "**/*.test.ts"]
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "tsx src/vercel.ts",
|
"start": "tsx src/index.ts",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Sandbox } from "@vercel/sandbox";
|
import { Sandbox } from "@vercel/sandbox";
|
||||||
import { runPrompt, waitForHealth } from "@sandbox-agent/example-shared";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
import { detectAgent, buildInspectorUrl, generateSessionId, waitForHealth } from "@sandbox-agent/example-shared";
|
||||||
|
|
||||||
const envs: Record<string, string> = {};
|
const envs: Record<string, string> = {};
|
||||||
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||||
|
|
@ -40,12 +41,18 @@ const baseUrl = sandbox.domain(3000);
|
||||||
console.log("Waiting for server...");
|
console.log("Waiting for server...");
|
||||||
await waitForHealth({ baseUrl });
|
await waitForHealth({ baseUrl });
|
||||||
|
|
||||||
|
const client = await SandboxAgent.connect({ baseUrl });
|
||||||
|
const sessionId = generateSessionId();
|
||||||
|
await client.createSession(sessionId, { agent: detectAgent() });
|
||||||
|
|
||||||
|
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
|
||||||
|
console.log(" Press Ctrl+C to stop.");
|
||||||
|
|
||||||
|
const keepAlive = setInterval(() => {}, 60_000);
|
||||||
const cleanup = async () => {
|
const cleanup = async () => {
|
||||||
|
clearInterval(keepAlive);
|
||||||
await sandbox.stop();
|
await sandbox.stop();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
process.once("SIGINT", cleanup);
|
process.once("SIGINT", cleanup);
|
||||||
process.once("SIGTERM", cleanup);
|
process.once("SIGTERM", cleanup);
|
||||||
|
|
||||||
await runPrompt(baseUrl);
|
|
||||||
await cleanup();
|
|
||||||
|
|
@ -336,6 +336,12 @@
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.banner.config-note {
|
||||||
|
background: rgba(255, 159, 10, 0.12);
|
||||||
|
border-left: 3px solid var(--warning);
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
.banner.success {
|
.banner.success {
|
||||||
background: rgba(48, 209, 88, 0.1);
|
background: rgba(48, 209, 88, 0.1);
|
||||||
border-left: 3px solid var(--success);
|
border-left: 3px solid var(--success);
|
||||||
|
|
@ -471,11 +477,12 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-add-menu {
|
.sidebar-add-menu,
|
||||||
|
.session-create-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 36px;
|
top: 36px;
|
||||||
left: 0;
|
left: 0;
|
||||||
min-width: 200px;
|
min-width: 220px;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--border-2);
|
border: 1px solid var(--border-2);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
@ -487,6 +494,405 @@
|
||||||
z-index: 60;
|
z-index: 60;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.session-create-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 6px 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-back {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border-2);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all var(--transition);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-back:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-agent-name {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
padding: 4px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-form .setup-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-form .setup-label {
|
||||||
|
width: 72px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-form .setup-select,
|
||||||
|
.session-create-form .setup-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-section {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-section-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-section-toggle:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-section-toggle .setup-label {
|
||||||
|
width: 72px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-section-count {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-section-arrow {
|
||||||
|
margin-left: auto;
|
||||||
|
color: var(--muted-2);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-section-body {
|
||||||
|
margin: 4px 0 6px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid var(--border-2);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--surface-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-textarea {
|
||||||
|
width: 100%;
|
||||||
|
background: var(--surface-2);
|
||||||
|
border: 1px solid var(--border-2);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text);
|
||||||
|
outline: none;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 60px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Consolas, monospace;
|
||||||
|
transition: border-color var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-textarea:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-textarea::placeholder {
|
||||||
|
color: var(--muted-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-inline-error {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--danger);
|
||||||
|
margin-top: 4px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-skill-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-skill-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 3px 4px 3px 8px;
|
||||||
|
background: var(--surface-2);
|
||||||
|
border: 1px solid var(--border-2);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-skill-path {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-skill-remove {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: all var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-skill-remove:hover {
|
||||||
|
color: var(--danger);
|
||||||
|
background: rgba(255, 59, 48, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-skill-add-row {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-skill-input {
|
||||||
|
width: 100%;
|
||||||
|
background: var(--surface-2);
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text);
|
||||||
|
outline: none;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Consolas, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-skill-input::placeholder {
|
||||||
|
color: var(--muted-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-skill-type-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
background: rgba(255, 79, 0, 0.15);
|
||||||
|
color: var(--accent);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-skill-type-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-skill-type-select {
|
||||||
|
width: 80px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: var(--surface-2);
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text);
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-mcp-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-mcp-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 3px 4px 3px 8px;
|
||||||
|
background: var(--surface-2);
|
||||||
|
border: 1px solid var(--border-2);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-mcp-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-mcp-name {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-mcp-type {
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
color: var(--muted);
|
||||||
|
background: var(--surface);
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-mcp-summary {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--muted);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-mcp-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-mcp-edit {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-mcp-name-input {
|
||||||
|
width: 100%;
|
||||||
|
background: var(--surface-2);
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-mcp-name-input:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-mcp-name-input::placeholder {
|
||||||
|
color: var(--muted-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-mcp-edit-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-mcp-save,
|
||||||
|
.session-create-mcp-cancel {
|
||||||
|
flex: 1;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-mcp-save {
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-mcp-save:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-mcp-cancel {
|
||||||
|
background: var(--border-2);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-mcp-cancel:hover {
|
||||||
|
background: var(--muted-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-add-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px dashed var(--border-2);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-add-btn:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-actions {
|
||||||
|
padding: 4px 2px 2px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-create-actions .button.primary {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty state variant of session-create-menu */
|
||||||
|
.empty-state-menu-wrapper .session-create-menu {
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-add-option {
|
.sidebar-add-option {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
|
|
@ -515,12 +921,40 @@
|
||||||
.agent-option-left {
|
.agent-option-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-option-name {
|
.agent-option-name {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-option-version {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-add-option:hover .agent-option-version {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-option-badges {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-option-arrow {
|
||||||
|
color: var(--muted-2);
|
||||||
|
transition: color var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-add-option:hover .agent-option-arrow {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-badge {
|
.agent-badge {
|
||||||
|
|
@ -535,9 +969,6 @@
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-badge.version {
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-add-status {
|
.sidebar-add-status {
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
|
|
@ -1043,6 +1474,36 @@
|
||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Session Config Bar */
|
||||||
|
.session-config-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 10px 16px 12px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-config-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-config-label {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-config-value {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8e8e93;
|
||||||
|
}
|
||||||
|
|
||||||
/* Setup Row */
|
/* Setup Row */
|
||||||
.setup-row {
|
.setup-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -1207,6 +1668,29 @@
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.setup-config-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-config-btn {
|
||||||
|
border: 1px solid var(--border-2);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-config-btn:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-config-btn.error {
|
||||||
|
color: var(--danger);
|
||||||
|
border-color: rgba(255, 59, 48, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
.setup-version {
|
.setup-version {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
|
@ -1311,6 +1795,15 @@
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.config-textarea {
|
||||||
|
min-height: 130px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-inline-error {
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -1319,6 +1812,16 @@
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-header-pills {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-icon {
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@ import {
|
||||||
SandboxAgentError,
|
SandboxAgentError,
|
||||||
SandboxAgent,
|
SandboxAgent,
|
||||||
type AgentInfo,
|
type AgentInfo,
|
||||||
|
type CreateSessionRequest,
|
||||||
type AgentModelInfo,
|
type AgentModelInfo,
|
||||||
type AgentModeInfo,
|
type AgentModeInfo,
|
||||||
type PermissionEventData,
|
type PermissionEventData,
|
||||||
type QuestionEventData,
|
type QuestionEventData,
|
||||||
type SessionInfo,
|
type SessionInfo,
|
||||||
|
type SkillSource,
|
||||||
type UniversalEvent,
|
type UniversalEvent,
|
||||||
type UniversalItem
|
type UniversalItem
|
||||||
} from "sandbox-agent";
|
} from "sandbox-agent";
|
||||||
|
|
@ -32,6 +34,41 @@ type ItemDeltaEventData = {
|
||||||
delta: string;
|
delta: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type McpServerEntry = {
|
||||||
|
name: string;
|
||||||
|
configJson: string;
|
||||||
|
error: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ParsedMcpConfig = {
|
||||||
|
value: NonNullable<CreateSessionRequest["mcp"]>;
|
||||||
|
count: number;
|
||||||
|
error: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildMcpConfig = (entries: McpServerEntry[]): ParsedMcpConfig => {
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return { value: {}, count: 0, error: null };
|
||||||
|
}
|
||||||
|
const firstError = entries.find((e) => e.error);
|
||||||
|
if (firstError) {
|
||||||
|
return { value: {}, count: entries.length, error: `${firstError.name}: ${firstError.error}` };
|
||||||
|
}
|
||||||
|
const value: NonNullable<CreateSessionRequest["mcp"]> = {};
|
||||||
|
for (const entry of entries) {
|
||||||
|
try {
|
||||||
|
value[entry.name] = JSON.parse(entry.configJson);
|
||||||
|
} catch {
|
||||||
|
return { value: {}, count: entries.length, error: `${entry.name}: Invalid JSON` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { value, count: entries.length, error: null };
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildSkillsConfig = (sources: SkillSource[]): NonNullable<CreateSessionRequest["skills"]> => {
|
||||||
|
return { sources };
|
||||||
|
};
|
||||||
|
|
||||||
const buildStubItem = (itemId: string, nativeItemId?: string | null): UniversalItem => {
|
const buildStubItem = (itemId: string, nativeItemId?: string | null): UniversalItem => {
|
||||||
return {
|
return {
|
||||||
item_id: itemId,
|
item_id: itemId,
|
||||||
|
|
@ -53,6 +90,23 @@ const getCurrentOriginEndpoint = () => {
|
||||||
return window.location.origin;
|
return window.location.origin;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getSessionIdFromPath = (): string => {
|
||||||
|
const basePath = import.meta.env.BASE_URL;
|
||||||
|
const path = window.location.pathname;
|
||||||
|
const relative = path.startsWith(basePath) ? path.slice(basePath.length) : path;
|
||||||
|
const match = relative.match(/^sessions\/(.+)/);
|
||||||
|
return match ? match[1] : "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSessionPath = (id: string) => {
|
||||||
|
const basePath = import.meta.env.BASE_URL;
|
||||||
|
const params = window.location.search;
|
||||||
|
const newPath = id ? `${basePath}sessions/${id}${params}` : `${basePath}${params}`;
|
||||||
|
if (window.location.pathname + window.location.search !== newPath) {
|
||||||
|
window.history.replaceState(null, "", newPath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getInitialConnection = () => {
|
const getInitialConnection = () => {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return { endpoint: "http://127.0.0.1:2468", token: "", headers: {} as Record<string, string>, hasUrlParam: false };
|
return { endpoint: "http://127.0.0.1:2468", token: "", headers: {} as Record<string, string>, hasUrlParam: false };
|
||||||
|
|
@ -103,11 +157,7 @@ export default function App() {
|
||||||
const [modelsErrorByAgent, setModelsErrorByAgent] = useState<Record<string, string | null>>({});
|
const [modelsErrorByAgent, setModelsErrorByAgent] = useState<Record<string, string | null>>({});
|
||||||
|
|
||||||
const [agentId, setAgentId] = useState("claude");
|
const [agentId, setAgentId] = useState("claude");
|
||||||
const [agentMode, setAgentMode] = useState("");
|
const [sessionId, setSessionId] = useState(getSessionIdFromPath());
|
||||||
const [permissionMode, setPermissionMode] = useState("default");
|
|
||||||
const [model, setModel] = useState("");
|
|
||||||
const [variant, setVariant] = useState("");
|
|
||||||
const [sessionId, setSessionId] = useState("");
|
|
||||||
const [sessionError, setSessionError] = useState<string | null>(null);
|
const [sessionError, setSessionError] = useState<string | null>(null);
|
||||||
|
|
||||||
const [message, setMessage] = useState("");
|
const [message, setMessage] = useState("");
|
||||||
|
|
@ -115,6 +165,8 @@ export default function App() {
|
||||||
const [offset, setOffset] = useState(0);
|
const [offset, setOffset] = useState(0);
|
||||||
const offsetRef = useRef(0);
|
const offsetRef = useRef(0);
|
||||||
const [eventsLoading, setEventsLoading] = useState(false);
|
const [eventsLoading, setEventsLoading] = useState(false);
|
||||||
|
const [mcpServers, setMcpServers] = useState<McpServerEntry[]>([]);
|
||||||
|
const [skillSources, setSkillSources] = useState<SkillSource[]>([]);
|
||||||
|
|
||||||
const [polling, setPolling] = useState(false);
|
const [polling, setPolling] = useState(false);
|
||||||
const pollTimerRef = useRef<number | null>(null);
|
const pollTimerRef = useRef<number | null>(null);
|
||||||
|
|
@ -377,50 +429,52 @@ export default function App() {
|
||||||
stopSse();
|
stopSse();
|
||||||
stopTurnStream();
|
stopTurnStream();
|
||||||
setSessionId(session.sessionId);
|
setSessionId(session.sessionId);
|
||||||
|
updateSessionPath(session.sessionId);
|
||||||
setAgentId(session.agent);
|
setAgentId(session.agent);
|
||||||
setAgentMode(session.agentMode);
|
|
||||||
setPermissionMode(session.permissionMode);
|
|
||||||
setModel(session.model ?? "");
|
|
||||||
setVariant(session.variant ?? "");
|
|
||||||
setEvents([]);
|
setEvents([]);
|
||||||
setOffset(0);
|
setOffset(0);
|
||||||
offsetRef.current = 0;
|
offsetRef.current = 0;
|
||||||
setSessionError(null);
|
setSessionError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createNewSession = async (nextAgentId?: string) => {
|
const createNewSession = async (
|
||||||
|
nextAgentId: string,
|
||||||
|
config: { model: string; agentMode: string; permissionMode: string; variant: string }
|
||||||
|
) => {
|
||||||
stopPolling();
|
stopPolling();
|
||||||
stopSse();
|
stopSse();
|
||||||
stopTurnStream();
|
stopTurnStream();
|
||||||
const selectedAgent = nextAgentId ?? agentId;
|
setAgentId(nextAgentId);
|
||||||
if (nextAgentId) {
|
if (parsedMcpConfig.error) {
|
||||||
setAgentId(nextAgentId);
|
setSessionError(parsedMcpConfig.error);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
let id = "session-";
|
let id = "session-";
|
||||||
for (let i = 0; i < 8; i++) {
|
for (let i = 0; i < 8; i++) {
|
||||||
id += chars[Math.floor(Math.random() * chars.length)];
|
id += chars[Math.floor(Math.random() * chars.length)];
|
||||||
}
|
}
|
||||||
setSessionId(id);
|
|
||||||
setEvents([]);
|
|
||||||
setOffset(0);
|
|
||||||
offsetRef.current = 0;
|
|
||||||
setSessionError(null);
|
setSessionError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body: {
|
const body: CreateSessionRequest = { agent: nextAgentId };
|
||||||
agent: string;
|
if (config.agentMode) body.agentMode = config.agentMode;
|
||||||
agentMode?: string;
|
if (config.permissionMode) body.permissionMode = config.permissionMode;
|
||||||
permissionMode?: string;
|
if (config.model) body.model = config.model;
|
||||||
model?: string;
|
if (config.variant) body.variant = config.variant;
|
||||||
variant?: string;
|
if (parsedMcpConfig.count > 0) {
|
||||||
} = { agent: selectedAgent };
|
body.mcp = parsedMcpConfig.value;
|
||||||
if (agentMode) body.agentMode = agentMode;
|
}
|
||||||
if (permissionMode) body.permissionMode = permissionMode;
|
if (parsedSkillsConfig.sources.length > 0) {
|
||||||
if (model) body.model = model;
|
body.skills = parsedSkillsConfig;
|
||||||
if (variant) body.variant = variant;
|
}
|
||||||
|
|
||||||
await getClient().createSession(id, body);
|
await getClient().createSession(id, body);
|
||||||
|
setSessionId(id);
|
||||||
|
updateSessionPath(id);
|
||||||
|
setEvents([]);
|
||||||
|
setOffset(0);
|
||||||
|
offsetRef.current = 0;
|
||||||
await fetchSessions();
|
await fetchSessions();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSessionError(getErrorMessage(error, "Unable to create session"));
|
setSessionError(getErrorMessage(error, "Unable to create session"));
|
||||||
|
|
@ -876,38 +930,10 @@ export default function App() {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
}, [transcriptEntries]);
|
}, [transcriptEntries]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (connected && agentId && !modesByAgent[agentId]) {
|
|
||||||
loadModes(agentId);
|
|
||||||
}
|
|
||||||
}, [connected, agentId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (connected && agentId && !modelsByAgent[agentId]) {
|
|
||||||
loadModels(agentId);
|
|
||||||
}
|
|
||||||
}, [connected, agentId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const modes = modesByAgent[agentId];
|
|
||||||
if (modes && modes.length > 0 && !agentMode) {
|
|
||||||
setAgentMode(modes[0].id);
|
|
||||||
}
|
|
||||||
}, [modesByAgent, agentId]);
|
|
||||||
|
|
||||||
const currentAgent = agents.find((agent) => agent.id === agentId);
|
const currentAgent = agents.find((agent) => agent.id === agentId);
|
||||||
const activeModes = modesByAgent[agentId] ?? [];
|
const currentSessionInfo = sessions.find((s) => s.sessionId === sessionId);
|
||||||
const modesLoading = modesLoadingByAgent[agentId] ?? false;
|
const parsedMcpConfig = useMemo(() => buildMcpConfig(mcpServers), [mcpServers]);
|
||||||
const modesError = modesErrorByAgent[agentId] ?? null;
|
const parsedSkillsConfig = useMemo(() => buildSkillsConfig(skillSources), [skillSources]);
|
||||||
const modelOptions = modelsByAgent[agentId] ?? [];
|
|
||||||
const modelsLoading = modelsLoadingByAgent[agentId] ?? false;
|
|
||||||
const modelsError = modelsErrorByAgent[agentId] ?? null;
|
|
||||||
const defaultModel = defaultModelByAgent[agentId] ?? "";
|
|
||||||
const selectedModelId = model || defaultModel;
|
|
||||||
const selectedModel = modelOptions.find((entry) => entry.id === selectedModelId);
|
|
||||||
const variantOptions = selectedModel?.variants ?? [];
|
|
||||||
const defaultVariant = selectedModel?.defaultVariant ?? "";
|
|
||||||
const supportsVariants = Boolean(currentAgent?.capabilities?.variants);
|
|
||||||
const agentDisplayNames: Record<string, string> = {
|
const agentDisplayNames: Record<string, string> = {
|
||||||
claude: "Claude Code",
|
claude: "Claude Code",
|
||||||
codex: "Codex",
|
codex: "Codex",
|
||||||
|
|
@ -917,6 +943,15 @@ export default function App() {
|
||||||
};
|
};
|
||||||
const agentLabel = agentDisplayNames[agentId] ?? agentId;
|
const agentLabel = agentDisplayNames[agentId] ?? agentId;
|
||||||
|
|
||||||
|
const handleSelectAgent = useCallback((targetAgentId: string) => {
|
||||||
|
if (connected && !modesByAgent[targetAgentId]) {
|
||||||
|
loadModes(targetAgentId);
|
||||||
|
}
|
||||||
|
if (connected && !modelsByAgent[targetAgentId]) {
|
||||||
|
loadModels(targetAgentId);
|
||||||
|
}
|
||||||
|
}, [connected, modesByAgent, modelsByAgent]);
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (event.key === "Enter" && !event.shiftKey) {
|
if (event.key === "Enter" && !event.shiftKey) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -980,17 +1015,28 @@ export default function App() {
|
||||||
onSelectSession={selectSession}
|
onSelectSession={selectSession}
|
||||||
onRefresh={fetchSessions}
|
onRefresh={fetchSessions}
|
||||||
onCreateSession={createNewSession}
|
onCreateSession={createNewSession}
|
||||||
|
onSelectAgent={handleSelectAgent}
|
||||||
agents={agents.length ? agents : defaultAgents.map((id) => ({ id, installed: false, capabilities: {} }) as AgentInfo)}
|
agents={agents.length ? agents : defaultAgents.map((id) => ({ id, installed: false, capabilities: {} }) as AgentInfo)}
|
||||||
agentsLoading={agentsLoading}
|
agentsLoading={agentsLoading}
|
||||||
agentsError={agentsError}
|
agentsError={agentsError}
|
||||||
sessionsLoading={sessionsLoading}
|
sessionsLoading={sessionsLoading}
|
||||||
sessionsError={sessionsError}
|
sessionsError={sessionsError}
|
||||||
|
modesByAgent={modesByAgent}
|
||||||
|
modelsByAgent={modelsByAgent}
|
||||||
|
defaultModelByAgent={defaultModelByAgent}
|
||||||
|
modesLoadingByAgent={modesLoadingByAgent}
|
||||||
|
modelsLoadingByAgent={modelsLoadingByAgent}
|
||||||
|
modesErrorByAgent={modesErrorByAgent}
|
||||||
|
modelsErrorByAgent={modelsErrorByAgent}
|
||||||
|
mcpServers={mcpServers}
|
||||||
|
onMcpServersChange={setMcpServers}
|
||||||
|
mcpConfigError={parsedMcpConfig.error}
|
||||||
|
skillSources={skillSources}
|
||||||
|
onSkillSourcesChange={setSkillSources}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ChatPanel
|
<ChatPanel
|
||||||
sessionId={sessionId}
|
sessionId={sessionId}
|
||||||
polling={polling}
|
|
||||||
turnStreaming={turnStreaming}
|
|
||||||
transcriptEntries={transcriptEntries}
|
transcriptEntries={transcriptEntries}
|
||||||
sessionError={sessionError}
|
sessionError={sessionError}
|
||||||
message={message}
|
message={message}
|
||||||
|
|
@ -998,36 +1044,19 @@ export default function App() {
|
||||||
onSendMessage={sendMessage}
|
onSendMessage={sendMessage}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onCreateSession={createNewSession}
|
onCreateSession={createNewSession}
|
||||||
|
onSelectAgent={handleSelectAgent}
|
||||||
agents={agents.length ? agents : defaultAgents.map((id) => ({ id, installed: false, capabilities: {} }) as AgentInfo)}
|
agents={agents.length ? agents : defaultAgents.map((id) => ({ id, installed: false, capabilities: {} }) as AgentInfo)}
|
||||||
agentsLoading={agentsLoading}
|
agentsLoading={agentsLoading}
|
||||||
agentsError={agentsError}
|
agentsError={agentsError}
|
||||||
messagesEndRef={messagesEndRef}
|
messagesEndRef={messagesEndRef}
|
||||||
agentId={agentId}
|
|
||||||
agentLabel={agentLabel}
|
agentLabel={agentLabel}
|
||||||
agentMode={agentMode}
|
|
||||||
permissionMode={permissionMode}
|
|
||||||
model={model}
|
|
||||||
variant={variant}
|
|
||||||
modelOptions={modelOptions}
|
|
||||||
defaultModel={defaultModel}
|
|
||||||
modelsLoading={modelsLoading}
|
|
||||||
modelsError={modelsError}
|
|
||||||
variantOptions={variantOptions}
|
|
||||||
defaultVariant={defaultVariant}
|
|
||||||
supportsVariants={supportsVariants}
|
|
||||||
streamMode={streamMode}
|
|
||||||
activeModes={activeModes}
|
|
||||||
currentAgentVersion={currentAgent?.version ?? null}
|
currentAgentVersion={currentAgent?.version ?? null}
|
||||||
modesLoading={modesLoading}
|
sessionModel={currentSessionInfo?.model ?? null}
|
||||||
modesError={modesError}
|
sessionVariant={currentSessionInfo?.variant ?? null}
|
||||||
onAgentModeChange={setAgentMode}
|
sessionPermissionMode={currentSessionInfo?.permissionMode ?? null}
|
||||||
onPermissionModeChange={setPermissionMode}
|
sessionMcpServerCount={currentSessionInfo?.mcp ? Object.keys(currentSessionInfo.mcp).length : 0}
|
||||||
onModelChange={setModel}
|
sessionSkillSourceCount={currentSessionInfo?.skills?.sources?.length ?? 0}
|
||||||
onVariantChange={setVariant}
|
|
||||||
onStreamModeChange={setStreamMode}
|
|
||||||
onToggleStream={toggleStream}
|
|
||||||
onEndSession={endSession}
|
onEndSession={endSession}
|
||||||
hasSession={Boolean(sessionId)}
|
|
||||||
eventError={eventError}
|
eventError={eventError}
|
||||||
questionRequests={questionRequests}
|
questionRequests={questionRequests}
|
||||||
permissionRequests={permissionRequests}
|
permissionRequests={permissionRequests}
|
||||||
|
|
@ -1036,6 +1065,18 @@ export default function App() {
|
||||||
onAnswerQuestion={answerQuestion}
|
onAnswerQuestion={answerQuestion}
|
||||||
onRejectQuestion={rejectQuestion}
|
onRejectQuestion={rejectQuestion}
|
||||||
onReplyPermission={replyPermission}
|
onReplyPermission={replyPermission}
|
||||||
|
modesByAgent={modesByAgent}
|
||||||
|
modelsByAgent={modelsByAgent}
|
||||||
|
defaultModelByAgent={defaultModelByAgent}
|
||||||
|
modesLoadingByAgent={modesLoadingByAgent}
|
||||||
|
modelsLoadingByAgent={modelsLoadingByAgent}
|
||||||
|
modesErrorByAgent={modesErrorByAgent}
|
||||||
|
modelsErrorByAgent={modelsErrorByAgent}
|
||||||
|
mcpServers={mcpServers}
|
||||||
|
onMcpServersChange={setMcpServers}
|
||||||
|
mcpConfigError={parsedMcpConfig.error}
|
||||||
|
skillSources={skillSources}
|
||||||
|
onSkillSourcesChange={setSkillSources}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DebugPanel
|
<DebugPanel
|
||||||
|
|
|
||||||
750
frontend/packages/inspector/src/components/SessionCreateMenu.tsx
Normal file
750
frontend/packages/inspector/src/components/SessionCreateMenu.tsx
Normal file
|
|
@ -0,0 +1,750 @@
|
||||||
|
import { ArrowLeft, ArrowRight, ChevronDown, ChevronRight, Pencil, Plus, X } from "lucide-react";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import type { AgentInfo, AgentModelInfo, AgentModeInfo, SkillSource } from "sandbox-agent";
|
||||||
|
import type { McpServerEntry } from "../App";
|
||||||
|
|
||||||
|
export type SessionConfig = {
|
||||||
|
model: string;
|
||||||
|
agentMode: string;
|
||||||
|
permissionMode: string;
|
||||||
|
variant: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const agentLabels: Record<string, string> = {
|
||||||
|
claude: "Claude Code",
|
||||||
|
codex: "Codex",
|
||||||
|
opencode: "OpenCode",
|
||||||
|
amp: "Amp",
|
||||||
|
mock: "Mock"
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateServerJson = (json: string): string | null => {
|
||||||
|
const trimmed = json.trim();
|
||||||
|
if (!trimmed) return "Config is required";
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(trimmed);
|
||||||
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||||
|
return "Must be a JSON object";
|
||||||
|
}
|
||||||
|
if (!parsed.type) return 'Missing "type" field';
|
||||||
|
if (parsed.type !== "local" && parsed.type !== "remote") {
|
||||||
|
return 'Type must be "local" or "remote"';
|
||||||
|
}
|
||||||
|
if (parsed.type === "local" && !parsed.command) return 'Local server requires "command"';
|
||||||
|
if (parsed.type === "remote" && !parsed.url) return 'Remote server requires "url"';
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return "Invalid JSON";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getServerType = (configJson: string): string | null => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(configJson);
|
||||||
|
return parsed?.type ?? null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getServerSummary = (configJson: string): string => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(configJson);
|
||||||
|
if (parsed?.type === "local") {
|
||||||
|
const cmd = Array.isArray(parsed.command) ? parsed.command.join(" ") : parsed.command;
|
||||||
|
return cmd ?? "local";
|
||||||
|
}
|
||||||
|
if (parsed?.type === "remote") {
|
||||||
|
return parsed.url ?? "remote";
|
||||||
|
}
|
||||||
|
return parsed?.type ?? "";
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const skillSourceSummary = (source: SkillSource): string => {
|
||||||
|
let summary = source.source;
|
||||||
|
if (source.skills && source.skills.length > 0) {
|
||||||
|
summary += ` [${source.skills.join(", ")}]`;
|
||||||
|
}
|
||||||
|
return summary;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SessionCreateMenu = ({
|
||||||
|
agents,
|
||||||
|
agentsLoading,
|
||||||
|
agentsError,
|
||||||
|
modesByAgent,
|
||||||
|
modelsByAgent,
|
||||||
|
defaultModelByAgent,
|
||||||
|
modesLoadingByAgent,
|
||||||
|
modelsLoadingByAgent,
|
||||||
|
modesErrorByAgent,
|
||||||
|
modelsErrorByAgent,
|
||||||
|
mcpServers,
|
||||||
|
onMcpServersChange,
|
||||||
|
mcpConfigError,
|
||||||
|
skillSources,
|
||||||
|
onSkillSourcesChange,
|
||||||
|
onSelectAgent,
|
||||||
|
onCreateSession,
|
||||||
|
open,
|
||||||
|
onClose
|
||||||
|
}: {
|
||||||
|
agents: AgentInfo[];
|
||||||
|
agentsLoading: boolean;
|
||||||
|
agentsError: string | null;
|
||||||
|
modesByAgent: Record<string, AgentModeInfo[]>;
|
||||||
|
modelsByAgent: Record<string, AgentModelInfo[]>;
|
||||||
|
defaultModelByAgent: Record<string, string>;
|
||||||
|
modesLoadingByAgent: Record<string, boolean>;
|
||||||
|
modelsLoadingByAgent: Record<string, boolean>;
|
||||||
|
modesErrorByAgent: Record<string, string | null>;
|
||||||
|
modelsErrorByAgent: Record<string, string | null>;
|
||||||
|
mcpServers: McpServerEntry[];
|
||||||
|
onMcpServersChange: (servers: McpServerEntry[]) => void;
|
||||||
|
mcpConfigError: string | null;
|
||||||
|
skillSources: SkillSource[];
|
||||||
|
onSkillSourcesChange: (sources: SkillSource[]) => void;
|
||||||
|
onSelectAgent: (agentId: string) => void;
|
||||||
|
onCreateSession: (agentId: string, config: SessionConfig) => void;
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}) => {
|
||||||
|
const [phase, setPhase] = useState<"agent" | "config">("agent");
|
||||||
|
const [selectedAgent, setSelectedAgent] = useState("");
|
||||||
|
const [agentMode, setAgentMode] = useState("");
|
||||||
|
const [permissionMode, setPermissionMode] = useState("default");
|
||||||
|
const [model, setModel] = useState("");
|
||||||
|
const [variant, setVariant] = useState("");
|
||||||
|
|
||||||
|
const [mcpExpanded, setMcpExpanded] = useState(false);
|
||||||
|
const [skillsExpanded, setSkillsExpanded] = useState(false);
|
||||||
|
|
||||||
|
// Skill add/edit state
|
||||||
|
const [addingSkill, setAddingSkill] = useState(false);
|
||||||
|
const [editingSkillIndex, setEditingSkillIndex] = useState<number | null>(null);
|
||||||
|
const [skillType, setSkillType] = useState<"github" | "local" | "git">("github");
|
||||||
|
const [skillSource, setSkillSource] = useState("");
|
||||||
|
const [skillFilter, setSkillFilter] = useState("");
|
||||||
|
const [skillRef, setSkillRef] = useState("");
|
||||||
|
const [skillSubpath, setSkillSubpath] = useState("");
|
||||||
|
const [skillLocalError, setSkillLocalError] = useState<string | null>(null);
|
||||||
|
const skillSourceRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// MCP add/edit state
|
||||||
|
const [addingMcp, setAddingMcp] = useState(false);
|
||||||
|
const [editingMcpIndex, setEditingMcpIndex] = useState<number | null>(null);
|
||||||
|
const [mcpName, setMcpName] = useState("");
|
||||||
|
const [mcpJson, setMcpJson] = useState("");
|
||||||
|
const [mcpLocalError, setMcpLocalError] = useState<string | null>(null);
|
||||||
|
const mcpNameRef = useRef<HTMLInputElement>(null);
|
||||||
|
const mcpJsonRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
const cancelSkillEdit = () => {
|
||||||
|
setAddingSkill(false);
|
||||||
|
setEditingSkillIndex(null);
|
||||||
|
setSkillType("github");
|
||||||
|
setSkillSource("");
|
||||||
|
setSkillFilter("");
|
||||||
|
setSkillRef("");
|
||||||
|
setSkillSubpath("");
|
||||||
|
setSkillLocalError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset state when menu closes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setPhase("agent");
|
||||||
|
setSelectedAgent("");
|
||||||
|
setAgentMode("");
|
||||||
|
setPermissionMode("default");
|
||||||
|
setModel("");
|
||||||
|
setVariant("");
|
||||||
|
setMcpExpanded(false);
|
||||||
|
setSkillsExpanded(false);
|
||||||
|
cancelSkillEdit();
|
||||||
|
setAddingMcp(false);
|
||||||
|
setEditingMcpIndex(null);
|
||||||
|
setMcpName("");
|
||||||
|
setMcpJson("");
|
||||||
|
setMcpLocalError(null);
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
// Auto-select first mode when modes load for selected agent
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedAgent) return;
|
||||||
|
const modes = modesByAgent[selectedAgent];
|
||||||
|
if (modes && modes.length > 0 && !agentMode) {
|
||||||
|
setAgentMode(modes[0].id);
|
||||||
|
}
|
||||||
|
}, [modesByAgent, selectedAgent, agentMode]);
|
||||||
|
|
||||||
|
// Focus skill source input when adding
|
||||||
|
useEffect(() => {
|
||||||
|
if ((addingSkill || editingSkillIndex !== null) && skillSourceRef.current) {
|
||||||
|
skillSourceRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [addingSkill, editingSkillIndex]);
|
||||||
|
|
||||||
|
// Focus MCP name input when adding
|
||||||
|
useEffect(() => {
|
||||||
|
if (addingMcp && mcpNameRef.current) {
|
||||||
|
mcpNameRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [addingMcp]);
|
||||||
|
|
||||||
|
// Focus MCP json textarea when editing
|
||||||
|
useEffect(() => {
|
||||||
|
if (editingMcpIndex !== null && mcpJsonRef.current) {
|
||||||
|
mcpJsonRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [editingMcpIndex]);
|
||||||
|
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
const handleAgentClick = (agentId: string) => {
|
||||||
|
setSelectedAgent(agentId);
|
||||||
|
setPhase("config");
|
||||||
|
onSelectAgent(agentId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
setPhase("agent");
|
||||||
|
setSelectedAgent("");
|
||||||
|
setAgentMode("");
|
||||||
|
setPermissionMode("default");
|
||||||
|
setModel("");
|
||||||
|
setVariant("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreate = () => {
|
||||||
|
if (mcpConfigError) return;
|
||||||
|
onCreateSession(selectedAgent, { model, agentMode, permissionMode, variant });
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Skill source helpers
|
||||||
|
const startAddSkill = () => {
|
||||||
|
setAddingSkill(true);
|
||||||
|
setEditingSkillIndex(null);
|
||||||
|
setSkillType("github");
|
||||||
|
setSkillSource("rivet-dev/skills");
|
||||||
|
setSkillFilter("sandbox-agent");
|
||||||
|
setSkillRef("");
|
||||||
|
setSkillSubpath("");
|
||||||
|
setSkillLocalError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startEditSkill = (index: number) => {
|
||||||
|
const entry = skillSources[index];
|
||||||
|
setEditingSkillIndex(index);
|
||||||
|
setAddingSkill(false);
|
||||||
|
setSkillType(entry.type as "github" | "local" | "git");
|
||||||
|
setSkillSource(entry.source);
|
||||||
|
setSkillFilter(entry.skills?.join(", ") ?? "");
|
||||||
|
setSkillRef(entry.ref ?? "");
|
||||||
|
setSkillSubpath(entry.subpath ?? "");
|
||||||
|
setSkillLocalError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const commitSkill = () => {
|
||||||
|
const src = skillSource.trim();
|
||||||
|
if (!src) {
|
||||||
|
setSkillLocalError("Source is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const entry: SkillSource = {
|
||||||
|
type: skillType,
|
||||||
|
source: src,
|
||||||
|
};
|
||||||
|
const filterList = skillFilter.trim()
|
||||||
|
? skillFilter.split(",").map((s) => s.trim()).filter(Boolean)
|
||||||
|
: undefined;
|
||||||
|
if (filterList && filterList.length > 0) entry.skills = filterList;
|
||||||
|
if (skillRef.trim()) entry.ref = skillRef.trim();
|
||||||
|
if (skillSubpath.trim()) entry.subpath = skillSubpath.trim();
|
||||||
|
|
||||||
|
if (editingSkillIndex !== null) {
|
||||||
|
const updated = [...skillSources];
|
||||||
|
updated[editingSkillIndex] = entry;
|
||||||
|
onSkillSourcesChange(updated);
|
||||||
|
} else {
|
||||||
|
onSkillSourcesChange([...skillSources, entry]);
|
||||||
|
}
|
||||||
|
cancelSkillEdit();
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSkill = (index: number) => {
|
||||||
|
onSkillSourcesChange(skillSources.filter((_, i) => i !== index));
|
||||||
|
if (editingSkillIndex === index) {
|
||||||
|
cancelSkillEdit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isEditingSkill = addingSkill || editingSkillIndex !== null;
|
||||||
|
|
||||||
|
const startAddMcp = () => {
|
||||||
|
setAddingMcp(true);
|
||||||
|
setEditingMcpIndex(null);
|
||||||
|
setMcpName("everything");
|
||||||
|
setMcpJson('{\n "type": "local",\n "command": "npx",\n "args": ["@modelcontextprotocol/server-everything"]\n}');
|
||||||
|
setMcpLocalError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startEditMcp = (index: number) => {
|
||||||
|
const entry = mcpServers[index];
|
||||||
|
setEditingMcpIndex(index);
|
||||||
|
setAddingMcp(false);
|
||||||
|
setMcpName(entry.name);
|
||||||
|
setMcpJson(entry.configJson);
|
||||||
|
setMcpLocalError(entry.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelMcpEdit = () => {
|
||||||
|
setAddingMcp(false);
|
||||||
|
setEditingMcpIndex(null);
|
||||||
|
setMcpName("");
|
||||||
|
setMcpJson("");
|
||||||
|
setMcpLocalError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const commitMcp = () => {
|
||||||
|
const name = mcpName.trim();
|
||||||
|
if (!name) {
|
||||||
|
setMcpLocalError("Server name is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const error = validateServerJson(mcpJson);
|
||||||
|
if (error) {
|
||||||
|
setMcpLocalError(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Check for duplicate names (except when editing the same entry)
|
||||||
|
const duplicate = mcpServers.findIndex((e) => e.name === name);
|
||||||
|
if (duplicate !== -1 && duplicate !== editingMcpIndex) {
|
||||||
|
setMcpLocalError(`Server "${name}" already exists`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry: McpServerEntry = { name, configJson: mcpJson.trim(), error: null };
|
||||||
|
|
||||||
|
if (editingMcpIndex !== null) {
|
||||||
|
const updated = [...mcpServers];
|
||||||
|
updated[editingMcpIndex] = entry;
|
||||||
|
onMcpServersChange(updated);
|
||||||
|
} else {
|
||||||
|
onMcpServersChange([...mcpServers, entry]);
|
||||||
|
}
|
||||||
|
cancelMcpEdit();
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeMcp = (index: number) => {
|
||||||
|
onMcpServersChange(mcpServers.filter((_, i) => i !== index));
|
||||||
|
if (editingMcpIndex === index) {
|
||||||
|
cancelMcpEdit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isEditingMcp = addingMcp || editingMcpIndex !== null;
|
||||||
|
|
||||||
|
if (phase === "agent") {
|
||||||
|
return (
|
||||||
|
<div className="session-create-menu">
|
||||||
|
{agentsLoading && <div className="sidebar-add-status">Loading agents...</div>}
|
||||||
|
{agentsError && <div className="sidebar-add-status error">{agentsError}</div>}
|
||||||
|
{!agentsLoading && !agentsError && agents.length === 0 && (
|
||||||
|
<div className="sidebar-add-status">No agents available.</div>
|
||||||
|
)}
|
||||||
|
{!agentsLoading && !agentsError &&
|
||||||
|
agents.map((agent) => (
|
||||||
|
<button
|
||||||
|
key={agent.id}
|
||||||
|
className="sidebar-add-option"
|
||||||
|
onClick={() => handleAgentClick(agent.id)}
|
||||||
|
>
|
||||||
|
<div className="agent-option-left">
|
||||||
|
<span className="agent-option-name">{agentLabels[agent.id] ?? agent.id}</span>
|
||||||
|
{agent.version && <span className="agent-option-version">{agent.version}</span>}
|
||||||
|
</div>
|
||||||
|
<div className="agent-option-badges">
|
||||||
|
{agent.installed && <span className="agent-badge installed">Installed</span>}
|
||||||
|
<ArrowRight size={12} className="agent-option-arrow" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: config form
|
||||||
|
const activeModes = modesByAgent[selectedAgent] ?? [];
|
||||||
|
const modesLoading = modesLoadingByAgent[selectedAgent] ?? false;
|
||||||
|
const modesError = modesErrorByAgent[selectedAgent] ?? null;
|
||||||
|
const modelOptions = modelsByAgent[selectedAgent] ?? [];
|
||||||
|
const modelsLoading = modelsLoadingByAgent[selectedAgent] ?? false;
|
||||||
|
const modelsError = modelsErrorByAgent[selectedAgent] ?? null;
|
||||||
|
const defaultModel = defaultModelByAgent[selectedAgent] ?? "";
|
||||||
|
const selectedModelId = model || defaultModel;
|
||||||
|
const selectedModelObj = modelOptions.find((entry) => entry.id === selectedModelId);
|
||||||
|
const variantOptions = selectedModelObj?.variants ?? [];
|
||||||
|
const showModelSelect = modelsLoading || Boolean(modelsError) || modelOptions.length > 0;
|
||||||
|
const hasModelOptions = modelOptions.length > 0;
|
||||||
|
const modelCustom =
|
||||||
|
model && hasModelOptions && !modelOptions.some((entry) => entry.id === model);
|
||||||
|
const supportsVariants =
|
||||||
|
modelsLoading ||
|
||||||
|
Boolean(modelsError) ||
|
||||||
|
modelOptions.some((entry) => (entry.variants?.length ?? 0) > 0);
|
||||||
|
const showVariantSelect =
|
||||||
|
supportsVariants && (modelsLoading || Boolean(modelsError) || variantOptions.length > 0);
|
||||||
|
const hasVariantOptions = variantOptions.length > 0;
|
||||||
|
const variantCustom = variant && hasVariantOptions && !variantOptions.includes(variant);
|
||||||
|
const agentLabel = agentLabels[selectedAgent] ?? selectedAgent;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="session-create-menu">
|
||||||
|
<div className="session-create-header">
|
||||||
|
<button className="session-create-back" onClick={handleBack} title="Back to agents">
|
||||||
|
<ArrowLeft size={14} />
|
||||||
|
</button>
|
||||||
|
<span className="session-create-agent-name">{agentLabel}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="session-create-form">
|
||||||
|
<div className="setup-field">
|
||||||
|
<span className="setup-label">Model</span>
|
||||||
|
{showModelSelect ? (
|
||||||
|
<select
|
||||||
|
className="setup-select"
|
||||||
|
value={model}
|
||||||
|
onChange={(e) => { setModel(e.target.value); setVariant(""); }}
|
||||||
|
title="Model"
|
||||||
|
disabled={modelsLoading || Boolean(modelsError)}
|
||||||
|
>
|
||||||
|
{modelsLoading ? (
|
||||||
|
<option value="">Loading models...</option>
|
||||||
|
) : modelsError ? (
|
||||||
|
<option value="">{modelsError}</option>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<option value="">
|
||||||
|
{defaultModel ? `Default (${defaultModel})` : "Default"}
|
||||||
|
</option>
|
||||||
|
{modelCustom && <option value={model}>{model} (custom)</option>}
|
||||||
|
{modelOptions.map((entry) => (
|
||||||
|
<option key={entry.id} value={entry.id}>
|
||||||
|
{entry.name ?? entry.id}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
className="setup-input"
|
||||||
|
value={model}
|
||||||
|
onChange={(e) => setModel(e.target.value)}
|
||||||
|
placeholder="Model"
|
||||||
|
title="Model"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="setup-field">
|
||||||
|
<span className="setup-label">Mode</span>
|
||||||
|
<select
|
||||||
|
className="setup-select"
|
||||||
|
value={agentMode}
|
||||||
|
onChange={(e) => setAgentMode(e.target.value)}
|
||||||
|
title="Mode"
|
||||||
|
disabled={modesLoading || Boolean(modesError)}
|
||||||
|
>
|
||||||
|
{modesLoading ? (
|
||||||
|
<option value="">Loading modes...</option>
|
||||||
|
) : modesError ? (
|
||||||
|
<option value="">{modesError}</option>
|
||||||
|
) : activeModes.length > 0 ? (
|
||||||
|
activeModes.map((m) => (
|
||||||
|
<option key={m.id} value={m.id}>
|
||||||
|
{m.name || m.id}
|
||||||
|
</option>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<option value="">Mode</option>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="setup-field">
|
||||||
|
<span className="setup-label">Permission</span>
|
||||||
|
<select
|
||||||
|
className="setup-select"
|
||||||
|
value={permissionMode}
|
||||||
|
onChange={(e) => setPermissionMode(e.target.value)}
|
||||||
|
title="Permission Mode"
|
||||||
|
>
|
||||||
|
<option value="default">Default</option>
|
||||||
|
<option value="plan">Plan</option>
|
||||||
|
<option value="bypass">Bypass</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{supportsVariants && (
|
||||||
|
<div className="setup-field">
|
||||||
|
<span className="setup-label">Variant</span>
|
||||||
|
{showVariantSelect ? (
|
||||||
|
<select
|
||||||
|
className="setup-select"
|
||||||
|
value={variant}
|
||||||
|
onChange={(e) => setVariant(e.target.value)}
|
||||||
|
title="Variant"
|
||||||
|
disabled={modelsLoading || Boolean(modelsError)}
|
||||||
|
>
|
||||||
|
{modelsLoading ? (
|
||||||
|
<option value="">Loading variants...</option>
|
||||||
|
) : modelsError ? (
|
||||||
|
<option value="">{modelsError}</option>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<option value="">Default</option>
|
||||||
|
{variantCustom && <option value={variant}>{variant} (custom)</option>}
|
||||||
|
{variantOptions.map((entry) => (
|
||||||
|
<option key={entry} value={entry}>
|
||||||
|
{entry}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
className="setup-input"
|
||||||
|
value={variant}
|
||||||
|
onChange={(e) => setVariant(e.target.value)}
|
||||||
|
placeholder="Variant"
|
||||||
|
title="Variant"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* MCP Servers - collapsible */}
|
||||||
|
<div className="session-create-section">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="session-create-section-toggle"
|
||||||
|
onClick={() => setMcpExpanded(!mcpExpanded)}
|
||||||
|
>
|
||||||
|
<span className="setup-label">MCP</span>
|
||||||
|
<span className="session-create-section-count">{mcpServers.length} server{mcpServers.length !== 1 ? "s" : ""}</span>
|
||||||
|
{mcpExpanded ? <ChevronDown size={12} className="session-create-section-arrow" /> : <ChevronRight size={12} className="session-create-section-arrow" />}
|
||||||
|
</button>
|
||||||
|
{mcpExpanded && (
|
||||||
|
<div className="session-create-section-body">
|
||||||
|
{mcpServers.length > 0 && !isEditingMcp && (
|
||||||
|
<div className="session-create-mcp-list">
|
||||||
|
{mcpServers.map((entry, index) => (
|
||||||
|
<div key={entry.name} className="session-create-mcp-item">
|
||||||
|
<div className="session-create-mcp-info">
|
||||||
|
<span className="session-create-mcp-name">{entry.name}</span>
|
||||||
|
{getServerType(entry.configJson) && (
|
||||||
|
<span className="session-create-mcp-type">{getServerType(entry.configJson)}</span>
|
||||||
|
)}
|
||||||
|
<span className="session-create-mcp-summary mono">{getServerSummary(entry.configJson)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="session-create-mcp-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="session-create-skill-remove"
|
||||||
|
onClick={() => startEditMcp(index)}
|
||||||
|
title="Edit server"
|
||||||
|
>
|
||||||
|
<Pencil size={10} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="session-create-skill-remove"
|
||||||
|
onClick={() => removeMcp(index)}
|
||||||
|
title="Remove server"
|
||||||
|
>
|
||||||
|
<X size={12} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isEditingMcp ? (
|
||||||
|
<div className="session-create-mcp-edit">
|
||||||
|
<input
|
||||||
|
ref={mcpNameRef}
|
||||||
|
className="session-create-mcp-name-input"
|
||||||
|
value={mcpName}
|
||||||
|
onChange={(e) => { setMcpName(e.target.value); setMcpLocalError(null); }}
|
||||||
|
placeholder="server-name"
|
||||||
|
disabled={editingMcpIndex !== null}
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
ref={mcpJsonRef}
|
||||||
|
className="session-create-textarea mono"
|
||||||
|
value={mcpJson}
|
||||||
|
onChange={(e) => { setMcpJson(e.target.value); setMcpLocalError(null); }}
|
||||||
|
placeholder='{"type":"local","command":"node","args":["./server.js"]}'
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
{mcpLocalError && (
|
||||||
|
<div className="session-create-inline-error">{mcpLocalError}</div>
|
||||||
|
)}
|
||||||
|
<div className="session-create-mcp-edit-actions">
|
||||||
|
<button type="button" className="session-create-mcp-save" onClick={commitMcp}>
|
||||||
|
{editingMcpIndex !== null ? "Save" : "Add"}
|
||||||
|
</button>
|
||||||
|
<button type="button" className="session-create-mcp-cancel" onClick={cancelMcpEdit}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="session-create-add-btn"
|
||||||
|
onClick={startAddMcp}
|
||||||
|
>
|
||||||
|
<Plus size={12} />
|
||||||
|
Add server
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{mcpConfigError && !isEditingMcp && (
|
||||||
|
<div className="session-create-inline-error">{mcpConfigError}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Skills - collapsible with source-based list */}
|
||||||
|
<div className="session-create-section">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="session-create-section-toggle"
|
||||||
|
onClick={() => setSkillsExpanded(!skillsExpanded)}
|
||||||
|
>
|
||||||
|
<span className="setup-label">Skills</span>
|
||||||
|
<span className="session-create-section-count">{skillSources.length} source{skillSources.length !== 1 ? "s" : ""}</span>
|
||||||
|
{skillsExpanded ? <ChevronDown size={12} className="session-create-section-arrow" /> : <ChevronRight size={12} className="session-create-section-arrow" />}
|
||||||
|
</button>
|
||||||
|
{skillsExpanded && (
|
||||||
|
<div className="session-create-section-body">
|
||||||
|
{skillSources.length > 0 && !isEditingSkill && (
|
||||||
|
<div className="session-create-skill-list">
|
||||||
|
{skillSources.map((entry, index) => (
|
||||||
|
<div key={`${entry.type}-${entry.source}-${index}`} className="session-create-skill-item">
|
||||||
|
<span className="session-create-skill-type-badge">{entry.type}</span>
|
||||||
|
<span className="session-create-skill-path mono">{skillSourceSummary(entry)}</span>
|
||||||
|
<div className="session-create-mcp-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="session-create-skill-remove"
|
||||||
|
onClick={() => startEditSkill(index)}
|
||||||
|
title="Edit source"
|
||||||
|
>
|
||||||
|
<Pencil size={10} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="session-create-skill-remove"
|
||||||
|
onClick={() => removeSkill(index)}
|
||||||
|
title="Remove source"
|
||||||
|
>
|
||||||
|
<X size={12} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isEditingSkill ? (
|
||||||
|
<div className="session-create-mcp-edit">
|
||||||
|
<div className="session-create-skill-type-row">
|
||||||
|
<select
|
||||||
|
className="session-create-skill-type-select"
|
||||||
|
value={skillType}
|
||||||
|
onChange={(e) => { setSkillType(e.target.value as "github" | "local" | "git"); setSkillLocalError(null); }}
|
||||||
|
>
|
||||||
|
<option value="github">github</option>
|
||||||
|
<option value="local">local</option>
|
||||||
|
<option value="git">git</option>
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
ref={skillSourceRef}
|
||||||
|
className="session-create-skill-input mono"
|
||||||
|
value={skillSource}
|
||||||
|
onChange={(e) => { setSkillSource(e.target.value); setSkillLocalError(null); }}
|
||||||
|
placeholder={skillType === "github" ? "owner/repo" : skillType === "local" ? "/path/to/skill" : "https://git.example.com/repo.git"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
className="session-create-skill-input mono"
|
||||||
|
value={skillFilter}
|
||||||
|
onChange={(e) => setSkillFilter(e.target.value)}
|
||||||
|
placeholder="Filter skills (comma-separated, optional)"
|
||||||
|
/>
|
||||||
|
{skillType !== "local" && (
|
||||||
|
<div className="session-create-skill-type-row">
|
||||||
|
<input
|
||||||
|
className="session-create-skill-input mono"
|
||||||
|
value={skillRef}
|
||||||
|
onChange={(e) => setSkillRef(e.target.value)}
|
||||||
|
placeholder="Branch/tag (optional)"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="session-create-skill-input mono"
|
||||||
|
value={skillSubpath}
|
||||||
|
onChange={(e) => setSkillSubpath(e.target.value)}
|
||||||
|
placeholder="Subpath (optional)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{skillLocalError && (
|
||||||
|
<div className="session-create-inline-error">{skillLocalError}</div>
|
||||||
|
)}
|
||||||
|
<div className="session-create-mcp-edit-actions">
|
||||||
|
<button type="button" className="session-create-mcp-save" onClick={commitSkill}>
|
||||||
|
{editingSkillIndex !== null ? "Save" : "Add"}
|
||||||
|
</button>
|
||||||
|
<button type="button" className="session-create-mcp-cancel" onClick={cancelSkillEdit}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="session-create-add-btn"
|
||||||
|
onClick={startAddSkill}
|
||||||
|
>
|
||||||
|
<Plus size={12} />
|
||||||
|
Add source
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="session-create-actions">
|
||||||
|
<button
|
||||||
|
className="button primary"
|
||||||
|
onClick={handleCreate}
|
||||||
|
disabled={Boolean(mcpConfigError)}
|
||||||
|
>
|
||||||
|
Create Session
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SessionCreateMenu;
|
||||||
|
|
@ -1,6 +1,16 @@
|
||||||
import { Plus, RefreshCw } from "lucide-react";
|
import { Plus, RefreshCw } from "lucide-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import type { AgentInfo, SessionInfo } from "sandbox-agent";
|
import type { AgentInfo, AgentModelInfo, AgentModeInfo, SessionInfo, SkillSource } from "sandbox-agent";
|
||||||
|
import type { McpServerEntry } from "../App";
|
||||||
|
import SessionCreateMenu, { type SessionConfig } from "./SessionCreateMenu";
|
||||||
|
|
||||||
|
const agentLabels: Record<string, string> = {
|
||||||
|
claude: "Claude Code",
|
||||||
|
codex: "Codex",
|
||||||
|
opencode: "OpenCode",
|
||||||
|
amp: "Amp",
|
||||||
|
mock: "Mock"
|
||||||
|
};
|
||||||
|
|
||||||
const SessionSidebar = ({
|
const SessionSidebar = ({
|
||||||
sessions,
|
sessions,
|
||||||
|
|
@ -8,22 +18,48 @@ const SessionSidebar = ({
|
||||||
onSelectSession,
|
onSelectSession,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onCreateSession,
|
onCreateSession,
|
||||||
|
onSelectAgent,
|
||||||
agents,
|
agents,
|
||||||
agentsLoading,
|
agentsLoading,
|
||||||
agentsError,
|
agentsError,
|
||||||
sessionsLoading,
|
sessionsLoading,
|
||||||
sessionsError
|
sessionsError,
|
||||||
|
modesByAgent,
|
||||||
|
modelsByAgent,
|
||||||
|
defaultModelByAgent,
|
||||||
|
modesLoadingByAgent,
|
||||||
|
modelsLoadingByAgent,
|
||||||
|
modesErrorByAgent,
|
||||||
|
modelsErrorByAgent,
|
||||||
|
mcpServers,
|
||||||
|
onMcpServersChange,
|
||||||
|
mcpConfigError,
|
||||||
|
skillSources,
|
||||||
|
onSkillSourcesChange
|
||||||
}: {
|
}: {
|
||||||
sessions: SessionInfo[];
|
sessions: SessionInfo[];
|
||||||
selectedSessionId: string;
|
selectedSessionId: string;
|
||||||
onSelectSession: (session: SessionInfo) => void;
|
onSelectSession: (session: SessionInfo) => void;
|
||||||
onRefresh: () => void;
|
onRefresh: () => void;
|
||||||
onCreateSession: (agentId: string) => void;
|
onCreateSession: (agentId: string, config: SessionConfig) => void;
|
||||||
|
onSelectAgent: (agentId: string) => void;
|
||||||
agents: AgentInfo[];
|
agents: AgentInfo[];
|
||||||
agentsLoading: boolean;
|
agentsLoading: boolean;
|
||||||
agentsError: string | null;
|
agentsError: string | null;
|
||||||
sessionsLoading: boolean;
|
sessionsLoading: boolean;
|
||||||
sessionsError: string | null;
|
sessionsError: string | null;
|
||||||
|
modesByAgent: Record<string, AgentModeInfo[]>;
|
||||||
|
modelsByAgent: Record<string, AgentModelInfo[]>;
|
||||||
|
defaultModelByAgent: Record<string, string>;
|
||||||
|
modesLoadingByAgent: Record<string, boolean>;
|
||||||
|
modelsLoadingByAgent: Record<string, boolean>;
|
||||||
|
modesErrorByAgent: Record<string, string | null>;
|
||||||
|
modelsErrorByAgent: Record<string, string | null>;
|
||||||
|
mcpServers: McpServerEntry[];
|
||||||
|
onMcpServersChange: (servers: McpServerEntry[]) => void;
|
||||||
|
mcpConfigError: string | null;
|
||||||
|
skillSources: SkillSource[];
|
||||||
|
onSkillSourcesChange: (sources: SkillSource[]) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [showMenu, setShowMenu] = useState(false);
|
const [showMenu, setShowMenu] = useState(false);
|
||||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
@ -40,14 +76,6 @@ const SessionSidebar = ({
|
||||||
return () => document.removeEventListener("mousedown", handler);
|
return () => document.removeEventListener("mousedown", handler);
|
||||||
}, [showMenu]);
|
}, [showMenu]);
|
||||||
|
|
||||||
const agentLabels: Record<string, string> = {
|
|
||||||
claude: "Claude Code",
|
|
||||||
codex: "Codex",
|
|
||||||
opencode: "OpenCode",
|
|
||||||
amp: "Amp",
|
|
||||||
mock: "Mock"
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="session-sidebar">
|
<div className="session-sidebar">
|
||||||
<div className="sidebar-header">
|
<div className="sidebar-header">
|
||||||
|
|
@ -64,32 +92,27 @@ const SessionSidebar = ({
|
||||||
>
|
>
|
||||||
<Plus size={14} />
|
<Plus size={14} />
|
||||||
</button>
|
</button>
|
||||||
{showMenu && (
|
<SessionCreateMenu
|
||||||
<div className="sidebar-add-menu">
|
agents={agents}
|
||||||
{agentsLoading && <div className="sidebar-add-status">Loading agents...</div>}
|
agentsLoading={agentsLoading}
|
||||||
{agentsError && <div className="sidebar-add-status error">{agentsError}</div>}
|
agentsError={agentsError}
|
||||||
{!agentsLoading && !agentsError && agents.length === 0 && (
|
modesByAgent={modesByAgent}
|
||||||
<div className="sidebar-add-status">No agents available.</div>
|
modelsByAgent={modelsByAgent}
|
||||||
)}
|
defaultModelByAgent={defaultModelByAgent}
|
||||||
{!agentsLoading && !agentsError &&
|
modesLoadingByAgent={modesLoadingByAgent}
|
||||||
agents.map((agent) => (
|
modelsLoadingByAgent={modelsLoadingByAgent}
|
||||||
<button
|
modesErrorByAgent={modesErrorByAgent}
|
||||||
key={agent.id}
|
modelsErrorByAgent={modelsErrorByAgent}
|
||||||
className="sidebar-add-option"
|
mcpServers={mcpServers}
|
||||||
onClick={() => {
|
onMcpServersChange={onMcpServersChange}
|
||||||
onCreateSession(agent.id);
|
mcpConfigError={mcpConfigError}
|
||||||
setShowMenu(false);
|
skillSources={skillSources}
|
||||||
}}
|
onSkillSourcesChange={onSkillSourcesChange}
|
||||||
>
|
onSelectAgent={onSelectAgent}
|
||||||
<div className="agent-option-left">
|
onCreateSession={onCreateSession}
|
||||||
<span className="agent-option-name">{agentLabels[agent.id] ?? agent.id}</span>
|
open={showMenu}
|
||||||
{agent.version && <span className="agent-badge version">v{agent.version}</span>}
|
onClose={() => setShowMenu(false)}
|
||||||
</div>
|
/>
|
||||||
{agent.installed && <span className="agent-badge installed">Installed</span>}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
import { MessageSquare, PauseCircle, PlayCircle, Plus, Square, Terminal } from "lucide-react";
|
import { MessageSquare, Plus, Square, Terminal } from "lucide-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import type { AgentInfo, AgentModelInfo, AgentModeInfo, PermissionEventData, QuestionEventData } from "sandbox-agent";
|
import type { AgentInfo, AgentModelInfo, AgentModeInfo, PermissionEventData, QuestionEventData, SkillSource } from "sandbox-agent";
|
||||||
|
import type { McpServerEntry } from "../../App";
|
||||||
import ApprovalsTab from "../debug/ApprovalsTab";
|
import ApprovalsTab from "../debug/ApprovalsTab";
|
||||||
|
import SessionCreateMenu, { type SessionConfig } from "../SessionCreateMenu";
|
||||||
import ChatInput from "./ChatInput";
|
import ChatInput from "./ChatInput";
|
||||||
import ChatMessages from "./ChatMessages";
|
import ChatMessages from "./ChatMessages";
|
||||||
import ChatSetup from "./ChatSetup";
|
|
||||||
import type { TimelineEntry } from "./types";
|
import type { TimelineEntry } from "./types";
|
||||||
|
|
||||||
const ChatPanel = ({
|
const ChatPanel = ({
|
||||||
sessionId,
|
sessionId,
|
||||||
polling,
|
|
||||||
turnStreaming,
|
|
||||||
transcriptEntries,
|
transcriptEntries,
|
||||||
sessionError,
|
sessionError,
|
||||||
message,
|
message,
|
||||||
|
|
@ -18,35 +17,18 @@ const ChatPanel = ({
|
||||||
onSendMessage,
|
onSendMessage,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
onCreateSession,
|
onCreateSession,
|
||||||
|
onSelectAgent,
|
||||||
agents,
|
agents,
|
||||||
agentsLoading,
|
agentsLoading,
|
||||||
agentsError,
|
agentsError,
|
||||||
messagesEndRef,
|
messagesEndRef,
|
||||||
agentId,
|
|
||||||
agentLabel,
|
agentLabel,
|
||||||
agentMode,
|
|
||||||
permissionMode,
|
|
||||||
model,
|
|
||||||
variant,
|
|
||||||
modelOptions,
|
|
||||||
defaultModel,
|
|
||||||
modelsLoading,
|
|
||||||
modelsError,
|
|
||||||
variantOptions,
|
|
||||||
defaultVariant,
|
|
||||||
supportsVariants,
|
|
||||||
streamMode,
|
|
||||||
activeModes,
|
|
||||||
currentAgentVersion,
|
currentAgentVersion,
|
||||||
hasSession,
|
sessionModel,
|
||||||
modesLoading,
|
sessionVariant,
|
||||||
modesError,
|
sessionPermissionMode,
|
||||||
onAgentModeChange,
|
sessionMcpServerCount,
|
||||||
onPermissionModeChange,
|
sessionSkillSourceCount,
|
||||||
onModelChange,
|
|
||||||
onVariantChange,
|
|
||||||
onStreamModeChange,
|
|
||||||
onToggleStream,
|
|
||||||
onEndSession,
|
onEndSession,
|
||||||
eventError,
|
eventError,
|
||||||
questionRequests,
|
questionRequests,
|
||||||
|
|
@ -55,47 +37,40 @@ const ChatPanel = ({
|
||||||
onSelectQuestionOption,
|
onSelectQuestionOption,
|
||||||
onAnswerQuestion,
|
onAnswerQuestion,
|
||||||
onRejectQuestion,
|
onRejectQuestion,
|
||||||
onReplyPermission
|
onReplyPermission,
|
||||||
|
modesByAgent,
|
||||||
|
modelsByAgent,
|
||||||
|
defaultModelByAgent,
|
||||||
|
modesLoadingByAgent,
|
||||||
|
modelsLoadingByAgent,
|
||||||
|
modesErrorByAgent,
|
||||||
|
modelsErrorByAgent,
|
||||||
|
mcpServers,
|
||||||
|
onMcpServersChange,
|
||||||
|
mcpConfigError,
|
||||||
|
skillSources,
|
||||||
|
onSkillSourcesChange
|
||||||
}: {
|
}: {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
polling: boolean;
|
|
||||||
turnStreaming: boolean;
|
|
||||||
transcriptEntries: TimelineEntry[];
|
transcriptEntries: TimelineEntry[];
|
||||||
sessionError: string | null;
|
sessionError: string | null;
|
||||||
message: string;
|
message: string;
|
||||||
onMessageChange: (value: string) => void;
|
onMessageChange: (value: string) => void;
|
||||||
onSendMessage: () => void;
|
onSendMessage: () => void;
|
||||||
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||||
onCreateSession: (agentId: string) => void;
|
onCreateSession: (agentId: string, config: SessionConfig) => void;
|
||||||
|
onSelectAgent: (agentId: string) => void;
|
||||||
agents: AgentInfo[];
|
agents: AgentInfo[];
|
||||||
agentsLoading: boolean;
|
agentsLoading: boolean;
|
||||||
agentsError: string | null;
|
agentsError: string | null;
|
||||||
messagesEndRef: React.RefObject<HTMLDivElement>;
|
messagesEndRef: React.RefObject<HTMLDivElement>;
|
||||||
agentId: string;
|
|
||||||
agentLabel: string;
|
agentLabel: string;
|
||||||
agentMode: string;
|
|
||||||
permissionMode: string;
|
|
||||||
model: string;
|
|
||||||
variant: string;
|
|
||||||
modelOptions: AgentModelInfo[];
|
|
||||||
defaultModel: string;
|
|
||||||
modelsLoading: boolean;
|
|
||||||
modelsError: string | null;
|
|
||||||
variantOptions: string[];
|
|
||||||
defaultVariant: string;
|
|
||||||
supportsVariants: boolean;
|
|
||||||
streamMode: "poll" | "sse" | "turn";
|
|
||||||
activeModes: AgentModeInfo[];
|
|
||||||
currentAgentVersion?: string | null;
|
currentAgentVersion?: string | null;
|
||||||
hasSession: boolean;
|
sessionModel?: string | null;
|
||||||
modesLoading: boolean;
|
sessionVariant?: string | null;
|
||||||
modesError: string | null;
|
sessionPermissionMode?: string | null;
|
||||||
onAgentModeChange: (value: string) => void;
|
sessionMcpServerCount: number;
|
||||||
onPermissionModeChange: (value: string) => void;
|
sessionSkillSourceCount: number;
|
||||||
onModelChange: (value: string) => void;
|
|
||||||
onVariantChange: (value: string) => void;
|
|
||||||
onStreamModeChange: (value: "poll" | "sse" | "turn") => void;
|
|
||||||
onToggleStream: () => void;
|
|
||||||
onEndSession: () => void;
|
onEndSession: () => void;
|
||||||
eventError: string | null;
|
eventError: string | null;
|
||||||
questionRequests: QuestionEventData[];
|
questionRequests: QuestionEventData[];
|
||||||
|
|
@ -105,6 +80,18 @@ const ChatPanel = ({
|
||||||
onAnswerQuestion: (request: QuestionEventData) => void;
|
onAnswerQuestion: (request: QuestionEventData) => void;
|
||||||
onRejectQuestion: (requestId: string) => void;
|
onRejectQuestion: (requestId: string) => void;
|
||||||
onReplyPermission: (requestId: string, reply: "once" | "always" | "reject") => void;
|
onReplyPermission: (requestId: string, reply: "once" | "always" | "reject") => void;
|
||||||
|
modesByAgent: Record<string, AgentModeInfo[]>;
|
||||||
|
modelsByAgent: Record<string, AgentModelInfo[]>;
|
||||||
|
defaultModelByAgent: Record<string, string>;
|
||||||
|
modesLoadingByAgent: Record<string, boolean>;
|
||||||
|
modelsLoadingByAgent: Record<string, boolean>;
|
||||||
|
modesErrorByAgent: Record<string, string | null>;
|
||||||
|
modelsErrorByAgent: Record<string, string | null>;
|
||||||
|
mcpServers: McpServerEntry[];
|
||||||
|
onMcpServersChange: (servers: McpServerEntry[]) => void;
|
||||||
|
mcpConfigError: string | null;
|
||||||
|
skillSources: SkillSource[];
|
||||||
|
onSkillSourcesChange: (sources: SkillSource[]) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [showAgentMenu, setShowAgentMenu] = useState(false);
|
const [showAgentMenu, setShowAgentMenu] = useState(false);
|
||||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
@ -121,18 +108,7 @@ const ChatPanel = ({
|
||||||
return () => document.removeEventListener("mousedown", handler);
|
return () => document.removeEventListener("mousedown", handler);
|
||||||
}, [showAgentMenu]);
|
}, [showAgentMenu]);
|
||||||
|
|
||||||
const agentLabels: Record<string, string> = {
|
|
||||||
claude: "Claude Code",
|
|
||||||
codex: "Codex",
|
|
||||||
opencode: "OpenCode",
|
|
||||||
amp: "Amp",
|
|
||||||
mock: "Mock"
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasApprovals = questionRequests.length > 0 || permissionRequests.length > 0;
|
const hasApprovals = questionRequests.length > 0 || permissionRequests.length > 0;
|
||||||
const isTurnMode = streamMode === "turn";
|
|
||||||
const isStreaming = isTurnMode ? turnStreaming : polling;
|
|
||||||
const turnLabel = turnStreaming ? "Streaming" : "On Send";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-panel">
|
<div className="chat-panel">
|
||||||
|
|
@ -141,12 +117,6 @@ const ChatPanel = ({
|
||||||
<MessageSquare className="button-icon" />
|
<MessageSquare className="button-icon" />
|
||||||
<span className="panel-title">{sessionId ? "Session" : "No Session"}</span>
|
<span className="panel-title">{sessionId ? "Session" : "No Session"}</span>
|
||||||
{sessionId && <span className="session-id-display">{sessionId}</span>}
|
{sessionId && <span className="session-id-display">{sessionId}</span>}
|
||||||
{sessionId && (
|
|
||||||
<span className="session-agent-display">
|
|
||||||
{agentLabel}
|
|
||||||
{currentAgentVersion && <span className="session-agent-version">v{currentAgentVersion}</span>}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="panel-header-right">
|
<div className="panel-header-right">
|
||||||
{sessionId && (
|
{sessionId && (
|
||||||
|
|
@ -160,42 +130,6 @@ const ChatPanel = ({
|
||||||
End
|
End
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<div className="setup-stream">
|
|
||||||
<select
|
|
||||||
className="setup-select-small"
|
|
||||||
value={streamMode}
|
|
||||||
onChange={(e) => onStreamModeChange(e.target.value as "poll" | "sse" | "turn")}
|
|
||||||
title="Stream Mode"
|
|
||||||
disabled={!sessionId}
|
|
||||||
>
|
|
||||||
<option value="poll">Poll</option>
|
|
||||||
<option value="sse">SSE</option>
|
|
||||||
<option value="turn">Turn</option>
|
|
||||||
</select>
|
|
||||||
<button
|
|
||||||
className={`setup-stream-btn ${isStreaming ? "active" : ""}`}
|
|
||||||
onClick={onToggleStream}
|
|
||||||
title={isTurnMode ? "Turn streaming starts on send" : polling ? "Stop streaming" : "Start streaming"}
|
|
||||||
disabled={!sessionId || isTurnMode}
|
|
||||||
>
|
|
||||||
{isTurnMode ? (
|
|
||||||
<>
|
|
||||||
<PlayCircle size={14} />
|
|
||||||
<span>{turnLabel}</span>
|
|
||||||
</>
|
|
||||||
) : polling ? (
|
|
||||||
<>
|
|
||||||
<PauseCircle size={14} />
|
|
||||||
<span>Pause</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<PlayCircle size={14} />
|
|
||||||
<span>Resume</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -213,32 +147,27 @@ const ChatPanel = ({
|
||||||
<Plus className="button-icon" />
|
<Plus className="button-icon" />
|
||||||
Create Session
|
Create Session
|
||||||
</button>
|
</button>
|
||||||
{showAgentMenu && (
|
<SessionCreateMenu
|
||||||
<div className="empty-state-menu">
|
agents={agents}
|
||||||
{agentsLoading && <div className="sidebar-add-status">Loading agents...</div>}
|
agentsLoading={agentsLoading}
|
||||||
{agentsError && <div className="sidebar-add-status error">{agentsError}</div>}
|
agentsError={agentsError}
|
||||||
{!agentsLoading && !agentsError && agents.length === 0 && (
|
modesByAgent={modesByAgent}
|
||||||
<div className="sidebar-add-status">No agents available.</div>
|
modelsByAgent={modelsByAgent}
|
||||||
)}
|
defaultModelByAgent={defaultModelByAgent}
|
||||||
{!agentsLoading && !agentsError &&
|
modesLoadingByAgent={modesLoadingByAgent}
|
||||||
agents.map((agent) => (
|
modelsLoadingByAgent={modelsLoadingByAgent}
|
||||||
<button
|
modesErrorByAgent={modesErrorByAgent}
|
||||||
key={agent.id}
|
modelsErrorByAgent={modelsErrorByAgent}
|
||||||
className="sidebar-add-option"
|
mcpServers={mcpServers}
|
||||||
onClick={() => {
|
onMcpServersChange={onMcpServersChange}
|
||||||
onCreateSession(agent.id);
|
mcpConfigError={mcpConfigError}
|
||||||
setShowAgentMenu(false);
|
skillSources={skillSources}
|
||||||
}}
|
onSkillSourcesChange={onSkillSourcesChange}
|
||||||
>
|
onSelectAgent={onSelectAgent}
|
||||||
<div className="agent-option-left">
|
onCreateSession={onCreateSession}
|
||||||
<span className="agent-option-name">{agentLabels[agent.id] ?? agent.id}</span>
|
open={showAgentMenu}
|
||||||
{agent.version && <span className="agent-badge version">v{agent.version}</span>}
|
onClose={() => setShowAgentMenu(false)}
|
||||||
</div>
|
/>
|
||||||
{agent.installed && <span className="agent-badge installed">Installed</span>}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : transcriptEntries.length === 0 && !sessionError ? (
|
) : transcriptEntries.length === 0 && !sessionError ? (
|
||||||
|
|
@ -246,7 +175,7 @@ const ChatPanel = ({
|
||||||
<Terminal className="empty-state-icon" />
|
<Terminal className="empty-state-icon" />
|
||||||
<div className="empty-state-title">Ready to Chat</div>
|
<div className="empty-state-title">Ready to Chat</div>
|
||||||
<p className="empty-state-text">Send a message to start a conversation with the agent.</p>
|
<p className="empty-state-text">Send a message to start a conversation with the agent.</p>
|
||||||
{agentId === "mock" && (
|
{agentLabel === "Mock" && (
|
||||||
<div className="mock-agent-hint">
|
<div className="mock-agent-hint">
|
||||||
The mock agent simulates agent responses for testing the inspector UI without requiring API credentials. Send <code>help</code> for available commands.
|
The mock agent simulates agent responses for testing the inspector UI without requiring API credentials. Send <code>help</code> for available commands.
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -283,30 +212,37 @@ const ChatPanel = ({
|
||||||
onSendMessage={onSendMessage}
|
onSendMessage={onSendMessage}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
placeholder={sessionId ? "Send a message..." : "Select or create a session first"}
|
placeholder={sessionId ? "Send a message..." : "Select or create a session first"}
|
||||||
disabled={!sessionId || turnStreaming}
|
disabled={!sessionId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ChatSetup
|
{sessionId && (
|
||||||
agentMode={agentMode}
|
<div className="session-config-bar">
|
||||||
permissionMode={permissionMode}
|
<div className="session-config-field">
|
||||||
model={model}
|
<span className="session-config-label">Agent</span>
|
||||||
variant={variant}
|
<span className="session-config-value">{agentLabel}</span>
|
||||||
modelOptions={modelOptions}
|
</div>
|
||||||
defaultModel={defaultModel}
|
<div className="session-config-field">
|
||||||
modelsLoading={modelsLoading}
|
<span className="session-config-label">Model</span>
|
||||||
modelsError={modelsError}
|
<span className="session-config-value">{sessionModel || "-"}</span>
|
||||||
variantOptions={variantOptions}
|
</div>
|
||||||
defaultVariant={defaultVariant}
|
<div className="session-config-field">
|
||||||
supportsVariants={supportsVariants}
|
<span className="session-config-label">Variant</span>
|
||||||
activeModes={activeModes}
|
<span className="session-config-value">{sessionVariant || "-"}</span>
|
||||||
modesLoading={modesLoading}
|
</div>
|
||||||
modesError={modesError}
|
<div className="session-config-field">
|
||||||
onAgentModeChange={onAgentModeChange}
|
<span className="session-config-label">Permission</span>
|
||||||
onPermissionModeChange={onPermissionModeChange}
|
<span className="session-config-value">{sessionPermissionMode || "-"}</span>
|
||||||
onModelChange={onModelChange}
|
</div>
|
||||||
onVariantChange={onVariantChange}
|
<div className="session-config-field">
|
||||||
hasSession={hasSession}
|
<span className="session-config-label">MCP Servers</span>
|
||||||
/>
|
<span className="session-config-value">{sessionMcpServerCount}</span>
|
||||||
|
</div>
|
||||||
|
<div className="session-config-field">
|
||||||
|
<span className="session-config-label">Skills</span>
|
||||||
|
<span className="session-config-value">{sessionSkillSourceCount}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,179 +0,0 @@
|
||||||
import type { AgentModelInfo, AgentModeInfo } from "sandbox-agent";
|
|
||||||
|
|
||||||
const ChatSetup = ({
|
|
||||||
agentMode,
|
|
||||||
permissionMode,
|
|
||||||
model,
|
|
||||||
variant,
|
|
||||||
modelOptions,
|
|
||||||
defaultModel,
|
|
||||||
modelsLoading,
|
|
||||||
modelsError,
|
|
||||||
variantOptions,
|
|
||||||
defaultVariant,
|
|
||||||
supportsVariants,
|
|
||||||
activeModes,
|
|
||||||
hasSession,
|
|
||||||
modesLoading,
|
|
||||||
modesError,
|
|
||||||
onAgentModeChange,
|
|
||||||
onPermissionModeChange,
|
|
||||||
onModelChange,
|
|
||||||
onVariantChange
|
|
||||||
}: {
|
|
||||||
agentMode: string;
|
|
||||||
permissionMode: string;
|
|
||||||
model: string;
|
|
||||||
variant: string;
|
|
||||||
modelOptions: AgentModelInfo[];
|
|
||||||
defaultModel: string;
|
|
||||||
modelsLoading: boolean;
|
|
||||||
modelsError: string | null;
|
|
||||||
variantOptions: string[];
|
|
||||||
defaultVariant: string;
|
|
||||||
supportsVariants: boolean;
|
|
||||||
activeModes: AgentModeInfo[];
|
|
||||||
hasSession: boolean;
|
|
||||||
modesLoading: boolean;
|
|
||||||
modesError: string | null;
|
|
||||||
onAgentModeChange: (value: string) => void;
|
|
||||||
onPermissionModeChange: (value: string) => void;
|
|
||||||
onModelChange: (value: string) => void;
|
|
||||||
onVariantChange: (value: string) => void;
|
|
||||||
}) => {
|
|
||||||
const showModelSelect = modelsLoading || Boolean(modelsError) || modelOptions.length > 0;
|
|
||||||
const hasModelOptions = modelOptions.length > 0;
|
|
||||||
const showVariantSelect =
|
|
||||||
supportsVariants && (modelsLoading || Boolean(modelsError) || variantOptions.length > 0);
|
|
||||||
const hasVariantOptions = variantOptions.length > 0;
|
|
||||||
const modelCustom =
|
|
||||||
model && hasModelOptions && !modelOptions.some((entry) => entry.id === model);
|
|
||||||
const variantCustom =
|
|
||||||
variant && hasVariantOptions && !variantOptions.includes(variant);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="setup-row">
|
|
||||||
<div className="setup-field">
|
|
||||||
<span className="setup-label">Mode</span>
|
|
||||||
<select
|
|
||||||
className="setup-select"
|
|
||||||
value={agentMode}
|
|
||||||
onChange={(e) => onAgentModeChange(e.target.value)}
|
|
||||||
title="Mode"
|
|
||||||
disabled={!hasSession || modesLoading || Boolean(modesError)}
|
|
||||||
>
|
|
||||||
{modesLoading ? (
|
|
||||||
<option value="">Loading modes...</option>
|
|
||||||
) : modesError ? (
|
|
||||||
<option value="">{modesError}</option>
|
|
||||||
) : activeModes.length > 0 ? (
|
|
||||||
activeModes.map((mode) => (
|
|
||||||
<option key={mode.id} value={mode.id}>
|
|
||||||
{mode.name || mode.id}
|
|
||||||
</option>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<option value="">Mode</option>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="setup-field">
|
|
||||||
<span className="setup-label">Permission</span>
|
|
||||||
<select
|
|
||||||
className="setup-select"
|
|
||||||
value={permissionMode}
|
|
||||||
onChange={(e) => onPermissionModeChange(e.target.value)}
|
|
||||||
title="Permission Mode"
|
|
||||||
disabled={!hasSession}
|
|
||||||
>
|
|
||||||
<option value="default">Default</option>
|
|
||||||
<option value="plan">Plan</option>
|
|
||||||
<option value="bypass">Bypass</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="setup-field">
|
|
||||||
<span className="setup-label">Model</span>
|
|
||||||
{showModelSelect ? (
|
|
||||||
<select
|
|
||||||
className="setup-select"
|
|
||||||
value={model}
|
|
||||||
onChange={(e) => onModelChange(e.target.value)}
|
|
||||||
title="Model"
|
|
||||||
disabled={!hasSession || modelsLoading || Boolean(modelsError)}
|
|
||||||
>
|
|
||||||
{modelsLoading ? (
|
|
||||||
<option value="">Loading models...</option>
|
|
||||||
) : modelsError ? (
|
|
||||||
<option value="">{modelsError}</option>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<option value="">
|
|
||||||
{defaultModel ? `Default (${defaultModel})` : "Default"}
|
|
||||||
</option>
|
|
||||||
{modelCustom && <option value={model}>{model} (custom)</option>}
|
|
||||||
{modelOptions.map((entry) => (
|
|
||||||
<option key={entry.id} value={entry.id}>
|
|
||||||
{entry.name ?? entry.id}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
) : (
|
|
||||||
<input
|
|
||||||
className="setup-input"
|
|
||||||
value={model}
|
|
||||||
onChange={(e) => onModelChange(e.target.value)}
|
|
||||||
placeholder="Model"
|
|
||||||
title="Model"
|
|
||||||
disabled={!hasSession}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="setup-field">
|
|
||||||
<span className="setup-label">Variant</span>
|
|
||||||
{showVariantSelect ? (
|
|
||||||
<select
|
|
||||||
className="setup-select"
|
|
||||||
value={variant}
|
|
||||||
onChange={(e) => onVariantChange(e.target.value)}
|
|
||||||
title="Variant"
|
|
||||||
disabled={!hasSession || !supportsVariants || modelsLoading || Boolean(modelsError)}
|
|
||||||
>
|
|
||||||
{modelsLoading ? (
|
|
||||||
<option value="">Loading variants...</option>
|
|
||||||
) : modelsError ? (
|
|
||||||
<option value="">{modelsError}</option>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<option value="">
|
|
||||||
{defaultVariant ? `Default (${defaultVariant})` : "Default"}
|
|
||||||
</option>
|
|
||||||
{variantCustom && <option value={variant}>{variant} (custom)</option>}
|
|
||||||
{variantOptions.map((entry) => (
|
|
||||||
<option key={entry} value={entry}>
|
|
||||||
{entry}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
) : (
|
|
||||||
<input
|
|
||||||
className="setup-input"
|
|
||||||
value={variant}
|
|
||||||
onChange={(e) => onVariantChange(e.target.value)}
|
|
||||||
placeholder={supportsVariants ? "Variant" : "Variants unsupported"}
|
|
||||||
title="Variant"
|
|
||||||
disabled={!hasSession || !supportsVariants}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ChatSetup;
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Download, RefreshCw } from "lucide-react";
|
import { Download, Loader2, RefreshCw } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
import type { AgentInfo, AgentModeInfo } from "sandbox-agent";
|
import type { AgentInfo, AgentModeInfo } from "sandbox-agent";
|
||||||
import FeatureCoverageBadges from "../agents/FeatureCoverageBadges";
|
import FeatureCoverageBadges from "../agents/FeatureCoverageBadges";
|
||||||
import { emptyFeatureCoverage } from "../../types/agents";
|
import { emptyFeatureCoverage } from "../../types/agents";
|
||||||
|
|
@ -16,10 +17,21 @@ const AgentsTab = ({
|
||||||
defaultAgents: string[];
|
defaultAgents: string[];
|
||||||
modesByAgent: Record<string, AgentModeInfo[]>;
|
modesByAgent: Record<string, AgentModeInfo[]>;
|
||||||
onRefresh: () => void;
|
onRefresh: () => void;
|
||||||
onInstall: (agentId: string, reinstall: boolean) => void;
|
onInstall: (agentId: string, reinstall: boolean) => Promise<void>;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}) => {
|
}) => {
|
||||||
|
const [installingAgent, setInstallingAgent] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleInstall = async (agentId: string, reinstall: boolean) => {
|
||||||
|
setInstallingAgent(agentId);
|
||||||
|
try {
|
||||||
|
await onInstall(agentId, reinstall);
|
||||||
|
} finally {
|
||||||
|
setInstallingAgent(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="inline-row" style={{ marginBottom: 16 }}>
|
<div className="inline-row" style={{ marginBottom: 16 }}>
|
||||||
|
|
@ -43,42 +55,53 @@ const AgentsTab = ({
|
||||||
version: undefined,
|
version: undefined,
|
||||||
path: undefined,
|
path: undefined,
|
||||||
capabilities: emptyFeatureCoverage
|
capabilities: emptyFeatureCoverage
|
||||||
}))).map((agent) => (
|
}))).map((agent) => {
|
||||||
<div key={agent.id} className="card">
|
const isInstalling = installingAgent === agent.id;
|
||||||
<div className="card-header">
|
return (
|
||||||
<span className="card-title">{agent.id}</span>
|
<div key={agent.id} className="card">
|
||||||
<span className={`pill ${agent.installed ? "success" : "danger"}`}>
|
<div className="card-header">
|
||||||
{agent.installed ? "Installed" : "Missing"}
|
<span className="card-title">{agent.id}</span>
|
||||||
</span>
|
<div className="card-header-pills">
|
||||||
<span className={`pill ${agent.credentialsAvailable ? "success" : "warning"}`}>
|
<span className={`pill ${agent.installed ? "success" : "danger"}`}>
|
||||||
{agent.credentialsAvailable ? "Authenticated" : "No Credentials"}
|
{agent.installed ? "Installed" : "Missing"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
<span className={`pill ${agent.credentialsAvailable ? "success" : "warning"}`}>
|
||||||
<div className="card-meta">
|
{agent.credentialsAvailable ? "Authenticated" : "No Credentials"}
|
||||||
{agent.version ? `v${agent.version}` : "Version unknown"}
|
</span>
|
||||||
{agent.path && <span className="mono muted" style={{ marginLeft: 8 }}>{agent.path}</span>}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-meta" style={{ marginTop: 8 }}>
|
<div className="card-meta">
|
||||||
Feature coverage
|
{agent.version ?? "Version unknown"}
|
||||||
</div>
|
{agent.path && <span className="mono muted" style={{ marginLeft: 8 }}>{agent.path}</span>}
|
||||||
<div style={{ marginTop: 8 }}>
|
</div>
|
||||||
<FeatureCoverageBadges featureCoverage={agent.capabilities ?? emptyFeatureCoverage} />
|
<div className="card-meta" style={{ marginTop: 8 }}>
|
||||||
</div>
|
Feature coverage
|
||||||
{modesByAgent[agent.id] && modesByAgent[agent.id].length > 0 && (
|
</div>
|
||||||
<div className="card-meta" style={{ marginTop: 8 }}>
|
<div style={{ marginTop: 8 }}>
|
||||||
Modes: {modesByAgent[agent.id].map((mode) => mode.id).join(", ")}
|
<FeatureCoverageBadges featureCoverage={agent.capabilities ?? emptyFeatureCoverage} />
|
||||||
|
</div>
|
||||||
|
{modesByAgent[agent.id] && modesByAgent[agent.id].length > 0 && (
|
||||||
|
<div className="card-meta" style={{ marginTop: 8 }}>
|
||||||
|
Modes: {modesByAgent[agent.id].map((mode) => mode.id).join(", ")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="card-actions">
|
||||||
|
<button
|
||||||
|
className="button secondary small"
|
||||||
|
onClick={() => handleInstall(agent.id, agent.installed)}
|
||||||
|
disabled={isInstalling}
|
||||||
|
>
|
||||||
|
{isInstalling ? (
|
||||||
|
<Loader2 className="button-icon spinner-icon" />
|
||||||
|
) : (
|
||||||
|
<Download className="button-icon" />
|
||||||
|
)}
|
||||||
|
{isInstalling ? "Installing..." : agent.installed ? "Reinstall" : "Install"}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div className="card-actions">
|
|
||||||
<button className="button secondary small" onClick={() => onInstall(agent.id, false)}>
|
|
||||||
<Download className="button-icon" /> Install
|
|
||||||
</button>
|
|
||||||
<button className="button ghost small" onClick={() => onInstall(agent.id, true)}>
|
|
||||||
Reinstall
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
))}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ const DebugPanel = ({
|
||||||
defaultAgents: string[];
|
defaultAgents: string[];
|
||||||
modesByAgent: Record<string, AgentModeInfo[]>;
|
modesByAgent: Record<string, AgentModeInfo[]>;
|
||||||
onRefreshAgents: () => void;
|
onRefreshAgents: () => void;
|
||||||
onInstallAgent: (agentId: string, reinstall: boolean) => void;
|
onInstallAgent: (agentId: string, reinstall: boolean) => Promise<void>;
|
||||||
agentsLoading: boolean;
|
agentsLoading: boolean;
|
||||||
agentsError: string | null;
|
agentsError: string | null;
|
||||||
}) => {
|
}) => {
|
||||||
|
|
|
||||||
14
justfile
14
justfile
|
|
@ -27,8 +27,12 @@ release-build-all:
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
[group('dev')]
|
[group('dev')]
|
||||||
dev:
|
dev-daemon:
|
||||||
pnpm dev -F @sandbox-agent/inspector
|
SANDBOX_AGENT_SKIP_INSPECTOR=1 cargo run -p sandbox-agent -- daemon start --upgrade
|
||||||
|
|
||||||
|
[group('dev')]
|
||||||
|
dev: dev-daemon
|
||||||
|
pnpm dev -F @sandbox-agent/inspector -- --host 0.0.0.0
|
||||||
|
|
||||||
[group('dev')]
|
[group('dev')]
|
||||||
build:
|
build:
|
||||||
|
|
@ -60,13 +64,17 @@ install-gigacode:
|
||||||
rm -f ~/.cargo/bin/gigacode
|
rm -f ~/.cargo/bin/gigacode
|
||||||
cp target/release/gigacode ~/.cargo/bin/gigacode
|
cp target/release/gigacode ~/.cargo/bin/gigacode
|
||||||
|
|
||||||
|
[group('dev')]
|
||||||
|
run-sa *ARGS:
|
||||||
|
SANDBOX_AGENT_SKIP_INSPECTOR=1 cargo run -p sandbox-agent -- {{ ARGS }}
|
||||||
|
|
||||||
[group('dev')]
|
[group('dev')]
|
||||||
run-gigacode *ARGS:
|
run-gigacode *ARGS:
|
||||||
SANDBOX_AGENT_SKIP_INSPECTOR=1 cargo run -p gigacode -- {{ ARGS }}
|
SANDBOX_AGENT_SKIP_INSPECTOR=1 cargo run -p gigacode -- {{ ARGS }}
|
||||||
|
|
||||||
[group('dev')]
|
[group('dev')]
|
||||||
dev-docs:
|
dev-docs:
|
||||||
cd docs && pnpm dlx mintlify dev
|
cd docs && pnpm dlx mintlify dev --host 0.0.0.0
|
||||||
|
|
||||||
install:
|
install:
|
||||||
pnpm install
|
pnpm install
|
||||||
|
|
|
||||||
1098
pnpm-lock.yaml
generated
1098
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
442
research/wip-agent-support.md
Normal file
442
research/wip-agent-support.md
Normal file
|
|
@ -0,0 +1,442 @@
|
||||||
|
# Universal Agent Configuration Support
|
||||||
|
|
||||||
|
Work-in-progress research on configuration features across agents and what can be made universal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TODO: Features Needed for Full Coverage
|
||||||
|
|
||||||
|
### Currently Implemented (in `CreateSessionRequest`)
|
||||||
|
|
||||||
|
- [x] `agent` - Agent selection (claude, codex, opencode, amp)
|
||||||
|
- [x] `agentMode` - Agent mode (plan, build, default)
|
||||||
|
- [x] `permissionMode` - Permission mode (default, plan, bypass)
|
||||||
|
- [x] `model` - Model selection
|
||||||
|
- [x] `variant` - Reasoning variant
|
||||||
|
- [x] `agentVersion` - Agent version selection
|
||||||
|
- [x] `mcp` - MCP server configuration (Claude/Codex/OpenCode/Amp)
|
||||||
|
- [x] `skills` - Skill path configuration (link or copy into agent skill roots)
|
||||||
|
|
||||||
|
### Tier 1: Universal Features (High Priority)
|
||||||
|
|
||||||
|
- [ ] `projectInstructions` - Inject CLAUDE.md / AGENTS.md content
|
||||||
|
- Write to appropriate file before agent spawn
|
||||||
|
- All agents support this natively
|
||||||
|
- [ ] `workingDirectory` - Set working directory for session
|
||||||
|
- Currently captures server `cwd` on session creation; not yet user-configurable
|
||||||
|
- [x] `mcp` - MCP server configuration
|
||||||
|
- Claude: Writes `.mcp.json` entries under `mcpServers`
|
||||||
|
- Codex: Updates `.codex/config.toml` with `mcp_servers`
|
||||||
|
- Amp: Calls `amp mcp add` for each server
|
||||||
|
- OpenCode: Uses `/mcp` API
|
||||||
|
- [x] `skills` - Skill path configuration
|
||||||
|
- Claude: Link to `./.claude/skills/<name>/`
|
||||||
|
- Codex: Link to `./.agents/skills/<name>/`
|
||||||
|
- OpenCode: Link to `./.opencode/skill/<name>/` + config `skills.paths`
|
||||||
|
- Amp: Link to Claude/Codex-style directories
|
||||||
|
- [ ] `credentials` - Pass credentials via API (not just env vars)
|
||||||
|
- Currently extracted from host env
|
||||||
|
- Need API-level credential injection
|
||||||
|
|
||||||
|
### Filesystem API (Implemented)
|
||||||
|
|
||||||
|
- [x] `/v1/fs` - Read/write/list/move/delete/stat files and upload batches
|
||||||
|
- Batch upload is tar-only (`application/x-tar`) with path output capped at 1024
|
||||||
|
- Relative paths resolve from session working dir when `sessionId` is provided
|
||||||
|
- CLI `sandbox-agent api fs ...` covers all filesystem endpoints
|
||||||
|
|
||||||
|
### Message Attachments (Implemented)
|
||||||
|
|
||||||
|
- [x] `MessageRequest.attachments` - Attach uploaded files when sending prompts
|
||||||
|
- OpenCode receives file parts; other agents get attachment paths appended to the prompt
|
||||||
|
|
||||||
|
### Tier 2: Partial Support (Medium Priority)
|
||||||
|
|
||||||
|
- [ ] `appendSystemPrompt` - High-priority system prompt additions
|
||||||
|
- Claude: `--append-system-prompt` flag
|
||||||
|
- Codex: `developer_instructions` config
|
||||||
|
- OpenCode: Custom agent definition
|
||||||
|
- Amp: Not supported (fallback to projectInstructions)
|
||||||
|
- [ ] `resumeSession` / native session resume
|
||||||
|
- Claude: `--resume SESSION_ID`
|
||||||
|
- Codex: Thread persistence (automatic)
|
||||||
|
- OpenCode: `-c/--continue`
|
||||||
|
- Amp: `--continue SESSION_ID`
|
||||||
|
|
||||||
|
### Tier 3: Agent-Specific Pass-through (Low Priority)
|
||||||
|
|
||||||
|
- [ ] `agentSpecific.claude` - Raw Claude options
|
||||||
|
- [ ] `agentSpecific.codex` - Raw Codex options (e.g., `replaceSystemPrompt`)
|
||||||
|
- [ ] `agentSpecific.opencode` - Raw OpenCode options (e.g., `customAgent`)
|
||||||
|
- [ ] `agentSpecific.amp` - Raw Amp options (e.g., `permissionRules`)
|
||||||
|
|
||||||
|
### Event/Feature Coverage Gaps (from compatibility matrix)
|
||||||
|
|
||||||
|
| Feature | Claude | Codex | OpenCode | Amp | Status |
|
||||||
|
|---------|--------|-------|----------|-----|--------|
|
||||||
|
| Tool Calls | —* | ✓ | ✓ | ✓ | Claude coming soon |
|
||||||
|
| Tool Results | —* | ✓ | ✓ | ✓ | Claude coming soon |
|
||||||
|
| Questions (HITL) | —* | — | ✓ | — | Only OpenCode |
|
||||||
|
| Permissions (HITL) | —* | — | ✓ | — | Only OpenCode |
|
||||||
|
| Images | — | ✓ | ✓ | — | 2/4 agents |
|
||||||
|
| File Attachments | — | ✓ | ✓ | — | 2/4 agents |
|
||||||
|
| Session Lifecycle | — | ✓ | ✓ | — | 2/4 agents |
|
||||||
|
| Reasoning/Thinking | — | ✓ | — | — | Codex only |
|
||||||
|
| Command Execution | — | ✓ | — | — | Codex only |
|
||||||
|
| File Changes | — | ✓ | — | — | Codex only |
|
||||||
|
| MCP Tools | ✓ | ✓ | ✓ | ✓ | Supported via session MCP config injection |
|
||||||
|
| Streaming Deltas | — | ✓ | ✓ | — | 2/4 agents |
|
||||||
|
|
||||||
|
\* Claude features marked as "coming imminently"
|
||||||
|
|
||||||
|
### Implementation Order (Suggested)
|
||||||
|
|
||||||
|
1. **mcp** - Done (session config injection + agent config writers)
|
||||||
|
2. **skills** - Done (session config injection + skill directory linking)
|
||||||
|
3. **projectInstructions** - Highest value, all agents support
|
||||||
|
4. **appendSystemPrompt** - High-priority instructions
|
||||||
|
5. **workingDirectory** - Basic session configuration
|
||||||
|
6. **resumeSession** - Session continuity
|
||||||
|
7. **credentials** - API-level auth injection
|
||||||
|
8. **agentSpecific** - Escape hatch for edge cases
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Legend
|
||||||
|
|
||||||
|
- ✅ Native support
|
||||||
|
- 🔄 Can be adapted/emulated
|
||||||
|
- ❌ Not supported
|
||||||
|
- ⚠️ Supported with caveats
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Instructions & System Prompt
|
||||||
|
|
||||||
|
| Feature | Claude | Codex | OpenCode | Amp | Universal? |
|
||||||
|
|---------|--------|-------|----------|-----|------------|
|
||||||
|
| **Project instructions file** | ✅ `CLAUDE.md` | ✅ `AGENTS.md` | 🔄 Config-based | ⚠️ Limited | ✅ Yes - write to agent's file |
|
||||||
|
| **Append to system prompt** | ✅ `--append-system-prompt` | ✅ `developer_instructions` | 🔄 Custom agent | ❌ | ⚠️ Partial - 3/4 agents |
|
||||||
|
| **Replace system prompt** | ❌ | ✅ `model_instructions_file` | 🔄 Custom agent | ❌ | ❌ No - Codex only |
|
||||||
|
| **Hierarchical discovery** | ✅ cwd → root | ✅ root → cwd | ❌ | ❌ | ❌ No - Claude/Codex only |
|
||||||
|
|
||||||
|
### Priority Comparison
|
||||||
|
|
||||||
|
| Agent | Priority Order (highest → lowest) |
|
||||||
|
|-------|-----------------------------------|
|
||||||
|
| Claude | `--append-system-prompt` > base prompt > `CLAUDE.md` |
|
||||||
|
| Codex | `AGENTS.md` > `developer_instructions` > base prompt |
|
||||||
|
| OpenCode | Custom agent prompt > base prompt |
|
||||||
|
| Amp | Server-controlled (opaque) |
|
||||||
|
|
||||||
|
### Key Differences
|
||||||
|
|
||||||
|
**Claude**: System prompt additions have highest priority. `CLAUDE.md` is injected as first user message (below system prompt).
|
||||||
|
|
||||||
|
**Codex**: Project instructions (`AGENTS.md`) have highest priority and can override system prompt. This is the inverse of Claude's model.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Permission Modes
|
||||||
|
|
||||||
|
| Feature | Claude | Codex | OpenCode | Amp | Universal? |
|
||||||
|
|---------|--------|-------|----------|-----|------------|
|
||||||
|
| **Read-only** | ✅ `plan` | ✅ `read-only` | 🔄 Rulesets | 🔄 Rules | ✅ Yes |
|
||||||
|
| **Write workspace** | ✅ `acceptEdits` | ✅ `workspace-write` | 🔄 Rulesets | 🔄 Rules | ✅ Yes |
|
||||||
|
| **Full bypass** | ✅ `--dangerously-skip-permissions` | ✅ `danger-full-access` | 🔄 Allow-all ruleset | ✅ `--dangerously-skip-permissions` | ✅ Yes |
|
||||||
|
| **Per-tool rules** | ❌ | ❌ | ✅ | ✅ | ❌ No - OpenCode/Amp only |
|
||||||
|
|
||||||
|
### Universal Mapping
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type PermissionMode = "readonly" | "write" | "bypass";
|
||||||
|
|
||||||
|
// Maps to:
|
||||||
|
// Claude: plan | acceptEdits | --dangerously-skip-permissions
|
||||||
|
// Codex: read-only | workspace-write | danger-full-access
|
||||||
|
// OpenCode: restrictive ruleset | permissive ruleset | allow-all
|
||||||
|
// Amp: reject rules | allow rules | dangerouslyAllowAll
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Agent Modes
|
||||||
|
|
||||||
|
| Feature | Claude | Codex | OpenCode | Amp | Universal? |
|
||||||
|
|---------|--------|-------|----------|-----|------------|
|
||||||
|
| **Plan mode** | ✅ `--permission-mode plan` | 🔄 Prompt prefix | ✅ `--agent plan` | 🔄 Mode selection | ✅ Yes |
|
||||||
|
| **Build/execute mode** | ✅ Default | ✅ Default | ✅ `--agent build` | ✅ Default | ✅ Yes |
|
||||||
|
| **Chat mode** | ❌ | 🔄 Prompt prefix | ❌ | ❌ | ❌ No - Codex only |
|
||||||
|
| **Custom agents** | ❌ | ❌ | ✅ Config-defined | ❌ | ❌ No - OpenCode only |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Model & Variant Selection
|
||||||
|
|
||||||
|
| Feature | Claude | Codex | OpenCode | Amp | Universal? |
|
||||||
|
|---------|--------|-------|----------|-----|------------|
|
||||||
|
| **Model selection** | ✅ `--model` | ✅ `-m/--model` | ✅ `-m provider/model` | ⚠️ `--mode` (abstracted) | ⚠️ Partial |
|
||||||
|
| **Model discovery API** | ✅ Anthropic API | ✅ `model/list` RPC | ✅ `GET /provider` | ❌ Server-side | ⚠️ Partial - 3/4 |
|
||||||
|
| **Reasoning variants** | ❌ | ✅ `model_reasoning_effort` | ✅ `--variant` | ✅ Deep mode levels | ⚠️ Partial |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. MCP & Tools
|
||||||
|
|
||||||
|
| Feature | Claude | Codex | OpenCode | Amp | Universal? |
|
||||||
|
|---------|--------|-------|----------|-----|------------|
|
||||||
|
| **MCP servers** | ✅ `mcpServers` in settings | ✅ `mcp_servers` in config | ✅ `/mcp` API | ✅ `--toolbox` | ✅ Yes - inject config |
|
||||||
|
| **Tool restrictions** | ❌ | ❌ | ✅ Per-tool permissions | ✅ Permission rules | ⚠️ Partial |
|
||||||
|
|
||||||
|
### MCP Config Mapping
|
||||||
|
|
||||||
|
| Agent | Local Server | Remote Server |
|
||||||
|
|-------|--------------|---------------|
|
||||||
|
| Claude | `.mcp.json` or `.claude/settings.json` → `mcpServers` | Same, with `url` |
|
||||||
|
| Codex | `.codex/config.toml` → `mcp_servers` | Same schema |
|
||||||
|
| OpenCode | `/mcp` API with `McpLocalConfig` | `McpRemoteConfig` with `url`, `headers` |
|
||||||
|
| Amp | `amp mcp add` CLI | Supports remote with headers |
|
||||||
|
|
||||||
|
Local MCP servers can be bundled (for example with `tsup`) and uploaded via the filesystem API, then referenced in the session `mcp` config to auto-start and serve custom tools.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Skills & Extensions
|
||||||
|
|
||||||
|
| Feature | Claude | Codex | OpenCode | Amp | Universal? |
|
||||||
|
|---------|--------|-------|----------|-----|------------|
|
||||||
|
| **Skills/plugins** | ✅ `.claude/skills/` | ✅ `.agents/skills/` | ✅ `.opencode/skill/` | 🔄 Claude-style | ✅ Yes - link dirs |
|
||||||
|
| **Slash commands** | ✅ `.claude/commands/` | ✅ Custom prompts (deprecated) | ❌ | ❌ | ⚠️ Partial |
|
||||||
|
|
||||||
|
### Skill Path Mapping
|
||||||
|
|
||||||
|
| Agent | Project Skills | User Skills |
|
||||||
|
|-------|----------------|-------------|
|
||||||
|
| Claude | `.claude/skills/<name>/SKILL.md` | `~/.claude/skills/<name>/SKILL.md` |
|
||||||
|
| Codex | `.agents/skills/` | `~/.agents/skills/` |
|
||||||
|
| OpenCode | `.opencode/skill/`, `.claude/skills/`, `.agents/skills/` | `~/.config/opencode/skill/` |
|
||||||
|
| Amp | Uses Claude/Codex directories | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Session Management
|
||||||
|
|
||||||
|
| Feature | Claude | Codex | OpenCode | Amp | Universal? |
|
||||||
|
|---------|--------|-------|----------|-----|------------|
|
||||||
|
| **Resume session** | ✅ `--resume` | ✅ Thread persistence | ✅ `-c/--continue` | ✅ `--continue` | ✅ Yes |
|
||||||
|
| **Session ID** | ✅ `session_id` | ✅ `thread_id` | ✅ `sessionID` | ✅ `session_id` | ✅ Yes |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Human-in-the-Loop
|
||||||
|
|
||||||
|
| Feature | Claude | Codex | OpenCode | Amp | Universal? |
|
||||||
|
|---------|--------|-------|----------|-----|------------|
|
||||||
|
| **Permission requests** | ✅ Events | ⚠️ Upfront only | ✅ SSE events | ❌ Pre-configured | ⚠️ Partial |
|
||||||
|
| **Questions** | ⚠️ Limited in headless | ❌ | ✅ Full support | ❌ | ❌ No - OpenCode best |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Credentials
|
||||||
|
|
||||||
|
| Feature | Claude | Codex | OpenCode | Amp | Universal? |
|
||||||
|
|---------|--------|-------|----------|-----|------------|
|
||||||
|
| **API key env var** | ✅ `ANTHROPIC_API_KEY` | ✅ `OPENAI_API_KEY` | ✅ Both | ✅ `ANTHROPIC_API_KEY` | ✅ Yes |
|
||||||
|
| **OAuth tokens** | ✅ | ✅ | ✅ | ✅ | ✅ Yes |
|
||||||
|
| **Config file auth** | ✅ `~/.claude.json` | ✅ `~/.codex/auth.json` | ✅ `~/.local/share/opencode/auth.json` | ✅ `~/.amp/config.json` | ✅ Yes - extract per agent |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Files Per Agent
|
||||||
|
|
||||||
|
### Claude Code
|
||||||
|
|
||||||
|
| File/Location | Purpose |
|
||||||
|
|---------------|---------|
|
||||||
|
| `CLAUDE.md` | Project instructions (hierarchical, cwd → root) |
|
||||||
|
| `~/.claude/CLAUDE.md` | Global user instructions |
|
||||||
|
| `~/.claude/settings.json` | User settings (permissions, MCP servers, env) |
|
||||||
|
| `.claude/settings.json` | Project-level settings |
|
||||||
|
| `.claude/settings.local.json` | Local overrides (gitignored) |
|
||||||
|
| `~/.claude/commands/` | Custom slash commands (user-level) |
|
||||||
|
| `.claude/commands/` | Project-level slash commands |
|
||||||
|
| `~/.claude/skills/` | Installed skills |
|
||||||
|
| `~/.claude/keybindings.json` | Custom keyboard shortcuts |
|
||||||
|
| `~/.claude/projects/<hash>/memory/MEMORY.md` | Auto-memory per project |
|
||||||
|
| `~/.claude.json` | Authentication/credentials |
|
||||||
|
| `~/.claude.json.api` | API key storage |
|
||||||
|
|
||||||
|
### OpenAI Codex
|
||||||
|
|
||||||
|
| File/Location | Purpose |
|
||||||
|
|---------------|---------|
|
||||||
|
| `AGENTS.md` | Project instructions (hierarchical, root → cwd) |
|
||||||
|
| `AGENTS.override.md` | Override file (takes precedence) |
|
||||||
|
| `~/.codex/AGENTS.md` | Global user instructions |
|
||||||
|
| `~/.codex/AGENTS.override.md` | Global override |
|
||||||
|
| `~/.codex/config.toml` | User configuration |
|
||||||
|
| `.codex/config.toml` | Project-level configuration |
|
||||||
|
| `~/.codex/auth.json` | Authentication/credentials |
|
||||||
|
|
||||||
|
Key config.toml options:
|
||||||
|
- `model` - Default model
|
||||||
|
- `developer_instructions` - Appended to system prompt
|
||||||
|
- `model_instructions_file` - Replace entire system prompt
|
||||||
|
- `project_doc_max_bytes` - Max AGENTS.md size (default 32KB)
|
||||||
|
- `project_doc_fallback_filenames` - Alternative instruction files
|
||||||
|
- `mcp_servers` - MCP server configuration
|
||||||
|
|
||||||
|
### OpenCode
|
||||||
|
|
||||||
|
| File/Location | Purpose |
|
||||||
|
|---------------|---------|
|
||||||
|
| `~/.local/share/opencode/auth.json` | Authentication |
|
||||||
|
| `~/.config/opencode/config.toml` | User configuration |
|
||||||
|
| `.opencode/config.toml` | Project configuration |
|
||||||
|
|
||||||
|
### Amp
|
||||||
|
|
||||||
|
| File/Location | Purpose |
|
||||||
|
|---------------|---------|
|
||||||
|
| `~/.amp/config.json` | Main configuration |
|
||||||
|
| `~/.config/amp/settings.json` | Additional settings |
|
||||||
|
| `.amp/rules.json` | Project permission rules |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary: Universalization Tiers
|
||||||
|
|
||||||
|
### Tier 1: Fully Universal (implement now)
|
||||||
|
|
||||||
|
| Feature | API | Notes |
|
||||||
|
|---------|-----|-------|
|
||||||
|
| Project instructions | `projectInstructions: string` | Write to CLAUDE.md / AGENTS.md |
|
||||||
|
| Permission mode | `permissionMode: "readonly" \| "write" \| "bypass"` | Map to agent-specific flags |
|
||||||
|
| Agent mode | `agentMode: "plan" \| "build"` | Map to agent-specific mechanisms |
|
||||||
|
| Model selection | `model: string` | Pass through to agent |
|
||||||
|
| Resume session | `sessionId: string` | Map to agent's resume flag |
|
||||||
|
| Credentials | `credentials: { apiKey?, oauthToken? }` | Inject via env vars |
|
||||||
|
| MCP servers | `mcp: McpConfig` | Write to agent's config (docs drafted) |
|
||||||
|
| Skills | `skills: { paths: string[] }` | Link to agent's skill dirs (docs drafted) |
|
||||||
|
|
||||||
|
### Tier 2: Partial Support (with fallbacks)
|
||||||
|
|
||||||
|
| Feature | API | Notes |
|
||||||
|
|---------|-----|-------|
|
||||||
|
| Append system prompt | `appendSystemPrompt: string` | Falls back to projectInstructions for Amp |
|
||||||
|
| Reasoning variant | `variant: string` | Ignored for Claude |
|
||||||
|
|
||||||
|
### Tier 3: Agent-Specific (pass-through)
|
||||||
|
|
||||||
|
| Feature | Notes |
|
||||||
|
|---------|-------|
|
||||||
|
| Replace system prompt | Codex only (`model_instructions_file`) |
|
||||||
|
| Per-tool permissions | OpenCode/Amp only |
|
||||||
|
| Custom agents | OpenCode only |
|
||||||
|
| Hierarchical file discovery | Let agents handle natively |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommended Universal API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface UniversalSessionConfig {
|
||||||
|
// Tier 1 - Universal
|
||||||
|
agent: "claude" | "codex" | "opencode" | "amp";
|
||||||
|
model?: string;
|
||||||
|
permissionMode?: "readonly" | "write" | "bypass";
|
||||||
|
agentMode?: "plan" | "build";
|
||||||
|
projectInstructions?: string;
|
||||||
|
sessionId?: string; // For resume
|
||||||
|
workingDirectory?: string;
|
||||||
|
credentials?: {
|
||||||
|
apiKey?: string;
|
||||||
|
oauthToken?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// MCP servers (docs drafted in docs/mcp.mdx)
|
||||||
|
mcp?: Record<string, McpServerConfig>;
|
||||||
|
|
||||||
|
// Skills (docs drafted in docs/skills.mdx)
|
||||||
|
skills?: {
|
||||||
|
paths: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tier 2 - Partial (with fallbacks)
|
||||||
|
appendSystemPrompt?: string;
|
||||||
|
variant?: string;
|
||||||
|
|
||||||
|
// Tier 3 - Pass-through
|
||||||
|
agentSpecific?: {
|
||||||
|
claude?: { /* raw Claude options */ };
|
||||||
|
codex?: { replaceSystemPrompt?: string; /* etc */ };
|
||||||
|
opencode?: { customAgent?: AgentDef; /* etc */ };
|
||||||
|
amp?: { permissionRules?: Rule[]; /* etc */ };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface McpServerConfig {
|
||||||
|
type: "local" | "remote";
|
||||||
|
// Local
|
||||||
|
command?: string;
|
||||||
|
args?: string[];
|
||||||
|
env?: Record<string, string>;
|
||||||
|
timeoutMs?: number;
|
||||||
|
// Remote
|
||||||
|
url?: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
### Priority Inversion Warning
|
||||||
|
|
||||||
|
Claude and Codex have inverted priority for project instructions vs system prompt:
|
||||||
|
|
||||||
|
- **Claude**: `--append-system-prompt` > base prompt > `CLAUDE.md`
|
||||||
|
- **Codex**: `AGENTS.md` > `developer_instructions` > base prompt
|
||||||
|
|
||||||
|
This means:
|
||||||
|
- In Claude, system prompt additions override project files
|
||||||
|
- In Codex, project files override system prompt additions
|
||||||
|
|
||||||
|
When using both `appendSystemPrompt` and `projectInstructions`, document this behavior clearly or consider normalizing by only using one mechanism.
|
||||||
|
|
||||||
|
### File Injection Strategy
|
||||||
|
|
||||||
|
For `projectInstructions`, sandbox-agent should:
|
||||||
|
|
||||||
|
1. Create a temp directory or use session working directory
|
||||||
|
2. Write instructions to the appropriate file:
|
||||||
|
- Claude: `.claude/CLAUDE.md` or `CLAUDE.md` in cwd
|
||||||
|
- Codex: `.codex/AGENTS.md` or `AGENTS.md` in cwd
|
||||||
|
- OpenCode: Config file or environment
|
||||||
|
- Amp: Limited - may only influence via context
|
||||||
|
3. Start agent in that directory
|
||||||
|
4. Agent discovers and loads instructions automatically
|
||||||
|
|
||||||
|
### MCP Server Injection
|
||||||
|
|
||||||
|
For `mcp`, sandbox-agent should:
|
||||||
|
|
||||||
|
1. Write MCP config to agent's settings file:
|
||||||
|
- Claude: `.mcp.json` or `.claude/settings.json` → `mcpServers` key
|
||||||
|
- Codex: `.codex/config.toml` → `mcp_servers`
|
||||||
|
- OpenCode: Call `/mcp` API
|
||||||
|
- Amp: Run `amp mcp add` or pass via `--toolbox`
|
||||||
|
2. Ensure MCP server binaries are available in PATH
|
||||||
|
3. Handle cleanup on session end
|
||||||
|
|
||||||
|
### Skill Linking
|
||||||
|
|
||||||
|
For `skills.paths`, sandbox-agent should:
|
||||||
|
|
||||||
|
1. For each skill path, symlink or copy to agent's skill directory:
|
||||||
|
- Claude: `.claude/skills/<name>/`
|
||||||
|
- Codex: `.agents/skills/<name>/`
|
||||||
|
- OpenCode: Update `skills.paths` in config
|
||||||
|
2. Skill directory must contain `SKILL.md`
|
||||||
|
3. Handle cleanup on session end
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "sandbox-agent",
|
"name": "sandbox-agent",
|
||||||
"version": "0.1.10",
|
"version": "0.1.10",
|
||||||
"description": "Universal API for automatic coding agents in sandboxes. Supprots Claude Code, Codex, OpenCode, and Amp.",
|
"description": "Universal API for automatic coding agents in sandboxes. Supports Claude Code, Codex, OpenCode, and Amp.",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,18 @@ import type {
|
||||||
CreateSessionResponse,
|
CreateSessionResponse,
|
||||||
EventsQuery,
|
EventsQuery,
|
||||||
EventsResponse,
|
EventsResponse,
|
||||||
|
FsActionResponse,
|
||||||
|
FsDeleteQuery,
|
||||||
|
FsEntriesQuery,
|
||||||
|
FsEntry,
|
||||||
|
FsMoveRequest,
|
||||||
|
FsMoveResponse,
|
||||||
|
FsPathQuery,
|
||||||
|
FsSessionQuery,
|
||||||
|
FsStat,
|
||||||
|
FsUploadBatchQuery,
|
||||||
|
FsUploadBatchResponse,
|
||||||
|
FsWriteResponse,
|
||||||
HealthResponse,
|
HealthResponse,
|
||||||
MessageRequest,
|
MessageRequest,
|
||||||
PermissionReplyRequest,
|
PermissionReplyRequest,
|
||||||
|
|
@ -52,6 +64,8 @@ type QueryValue = string | number | boolean | null | undefined;
|
||||||
type RequestOptions = {
|
type RequestOptions = {
|
||||||
query?: Record<string, QueryValue>;
|
query?: Record<string, QueryValue>;
|
||||||
body?: unknown;
|
body?: unknown;
|
||||||
|
rawBody?: BodyInit;
|
||||||
|
contentType?: string;
|
||||||
headers?: HeadersInit;
|
headers?: HeadersInit;
|
||||||
accept?: string;
|
accept?: string;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
|
|
@ -216,6 +230,57 @@ export class SandboxAgent {
|
||||||
await this.requestJson("POST", `${API_PREFIX}/sessions/${encodeURIComponent(sessionId)}/terminate`);
|
await this.requestJson("POST", `${API_PREFIX}/sessions/${encodeURIComponent(sessionId)}/terminate`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listFsEntries(query?: FsEntriesQuery): Promise<FsEntry[]> {
|
||||||
|
return this.requestJson("GET", `${API_PREFIX}/fs/entries`, { query });
|
||||||
|
}
|
||||||
|
|
||||||
|
async readFsFile(query: FsPathQuery): Promise<Uint8Array> {
|
||||||
|
const response = await this.requestRaw("GET", `${API_PREFIX}/fs/file`, {
|
||||||
|
query,
|
||||||
|
accept: "application/octet-stream",
|
||||||
|
});
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
return new Uint8Array(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeFsFile(query: FsPathQuery, body: BodyInit): Promise<FsWriteResponse> {
|
||||||
|
const response = await this.requestRaw("PUT", `${API_PREFIX}/fs/file`, {
|
||||||
|
query,
|
||||||
|
rawBody: body,
|
||||||
|
contentType: "application/octet-stream",
|
||||||
|
accept: "application/json",
|
||||||
|
});
|
||||||
|
const text = await response.text();
|
||||||
|
return text ? (JSON.parse(text) as FsWriteResponse) : { path: "", bytesWritten: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFsEntry(query: FsDeleteQuery): Promise<FsActionResponse> {
|
||||||
|
return this.requestJson("DELETE", `${API_PREFIX}/fs/entry`, { query });
|
||||||
|
}
|
||||||
|
|
||||||
|
async mkdirFs(query: FsPathQuery): Promise<FsActionResponse> {
|
||||||
|
return this.requestJson("POST", `${API_PREFIX}/fs/mkdir`, { query });
|
||||||
|
}
|
||||||
|
|
||||||
|
async moveFs(request: FsMoveRequest, query?: FsSessionQuery): Promise<FsMoveResponse> {
|
||||||
|
return this.requestJson("POST", `${API_PREFIX}/fs/move`, { query, body: request });
|
||||||
|
}
|
||||||
|
|
||||||
|
async statFs(query: FsPathQuery): Promise<FsStat> {
|
||||||
|
return this.requestJson("GET", `${API_PREFIX}/fs/stat`, { query });
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadFsBatch(body: BodyInit, query?: FsUploadBatchQuery): Promise<FsUploadBatchResponse> {
|
||||||
|
const response = await this.requestRaw("POST", `${API_PREFIX}/fs/upload-batch`, {
|
||||||
|
query,
|
||||||
|
rawBody: body,
|
||||||
|
contentType: "application/x-tar",
|
||||||
|
accept: "application/json",
|
||||||
|
});
|
||||||
|
const text = await response.text();
|
||||||
|
return text ? (JSON.parse(text) as FsUploadBatchResponse) : { paths: [], truncated: false };
|
||||||
|
}
|
||||||
|
|
||||||
async dispose(): Promise<void> {
|
async dispose(): Promise<void> {
|
||||||
if (this.spawnHandle) {
|
if (this.spawnHandle) {
|
||||||
await this.spawnHandle.dispose();
|
await this.spawnHandle.dispose();
|
||||||
|
|
@ -256,7 +321,15 @@ export class SandboxAgent {
|
||||||
}
|
}
|
||||||
|
|
||||||
const init: RequestInit = { method, headers, signal: options.signal };
|
const init: RequestInit = { method, headers, signal: options.signal };
|
||||||
if (options.body !== undefined) {
|
if (options.rawBody !== undefined && options.body !== undefined) {
|
||||||
|
throw new Error("requestRaw received both rawBody and body");
|
||||||
|
}
|
||||||
|
if (options.rawBody !== undefined) {
|
||||||
|
if (options.contentType) {
|
||||||
|
headers.set("Content-Type", options.contentType);
|
||||||
|
}
|
||||||
|
init.body = options.rawBody;
|
||||||
|
} else if (options.body !== undefined) {
|
||||||
headers.set("Content-Type", "application/json");
|
headers.set("Content-Type", "application/json");
|
||||||
init.body = JSON.stringify(options.body);
|
init.body = JSON.stringify(options.body);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,48 +6,162 @@
|
||||||
|
|
||||||
export interface paths {
|
export interface paths {
|
||||||
"/v1/agents": {
|
"/v1/agents": {
|
||||||
|
/**
|
||||||
|
* List Agents
|
||||||
|
* @description Returns all available coding agents and their installation status.
|
||||||
|
*/
|
||||||
get: operations["list_agents"];
|
get: operations["list_agents"];
|
||||||
};
|
};
|
||||||
"/v1/agents/{agent}/install": {
|
"/v1/agents/{agent}/install": {
|
||||||
|
/**
|
||||||
|
* Install Agent
|
||||||
|
* @description Installs or updates a coding agent (e.g. claude, codex, opencode, amp).
|
||||||
|
*/
|
||||||
post: operations["install_agent"];
|
post: operations["install_agent"];
|
||||||
};
|
};
|
||||||
"/v1/agents/{agent}/models": {
|
"/v1/agents/{agent}/models": {
|
||||||
|
/**
|
||||||
|
* List Agent Models
|
||||||
|
* @description Returns the available LLM models for an agent.
|
||||||
|
*/
|
||||||
get: operations["get_agent_models"];
|
get: operations["get_agent_models"];
|
||||||
};
|
};
|
||||||
"/v1/agents/{agent}/modes": {
|
"/v1/agents/{agent}/modes": {
|
||||||
|
/**
|
||||||
|
* List Agent Modes
|
||||||
|
* @description Returns the available interaction modes for an agent.
|
||||||
|
*/
|
||||||
get: operations["get_agent_modes"];
|
get: operations["get_agent_modes"];
|
||||||
};
|
};
|
||||||
|
"/v1/fs/entries": {
|
||||||
|
/**
|
||||||
|
* List Directory
|
||||||
|
* @description Lists files and directories at the given path.
|
||||||
|
*/
|
||||||
|
get: operations["fs_entries"];
|
||||||
|
};
|
||||||
|
"/v1/fs/entry": {
|
||||||
|
/**
|
||||||
|
* Delete Entry
|
||||||
|
* @description Deletes a file or directory.
|
||||||
|
*/
|
||||||
|
delete: operations["fs_delete_entry"];
|
||||||
|
};
|
||||||
|
"/v1/fs/file": {
|
||||||
|
/**
|
||||||
|
* Read File
|
||||||
|
* @description Reads the raw bytes of a file.
|
||||||
|
*/
|
||||||
|
get: operations["fs_read_file"];
|
||||||
|
/**
|
||||||
|
* Write File
|
||||||
|
* @description Writes raw bytes to a file, creating it if it doesn't exist.
|
||||||
|
*/
|
||||||
|
put: operations["fs_write_file"];
|
||||||
|
};
|
||||||
|
"/v1/fs/mkdir": {
|
||||||
|
/**
|
||||||
|
* Create Directory
|
||||||
|
* @description Creates a directory, including any missing parent directories.
|
||||||
|
*/
|
||||||
|
post: operations["fs_mkdir"];
|
||||||
|
};
|
||||||
|
"/v1/fs/move": {
|
||||||
|
/**
|
||||||
|
* Move Entry
|
||||||
|
* @description Moves or renames a file or directory.
|
||||||
|
*/
|
||||||
|
post: operations["fs_move"];
|
||||||
|
};
|
||||||
|
"/v1/fs/stat": {
|
||||||
|
/**
|
||||||
|
* Get File Info
|
||||||
|
* @description Returns metadata (size, timestamps, type) for a path.
|
||||||
|
*/
|
||||||
|
get: operations["fs_stat"];
|
||||||
|
};
|
||||||
|
"/v1/fs/upload-batch": {
|
||||||
|
/**
|
||||||
|
* Upload Files
|
||||||
|
* @description Uploads a tar.gz archive and extracts it to the destination directory.
|
||||||
|
*/
|
||||||
|
post: operations["fs_upload_batch"];
|
||||||
|
};
|
||||||
"/v1/health": {
|
"/v1/health": {
|
||||||
|
/**
|
||||||
|
* Health Check
|
||||||
|
* @description Returns the server health status.
|
||||||
|
*/
|
||||||
get: operations["get_health"];
|
get: operations["get_health"];
|
||||||
};
|
};
|
||||||
"/v1/sessions": {
|
"/v1/sessions": {
|
||||||
|
/**
|
||||||
|
* List Sessions
|
||||||
|
* @description Returns all active sessions.
|
||||||
|
*/
|
||||||
get: operations["list_sessions"];
|
get: operations["list_sessions"];
|
||||||
};
|
};
|
||||||
"/v1/sessions/{session_id}": {
|
"/v1/sessions/{session_id}": {
|
||||||
|
/**
|
||||||
|
* Create Session
|
||||||
|
* @description Creates a new agent session with the given configuration.
|
||||||
|
*/
|
||||||
post: operations["create_session"];
|
post: operations["create_session"];
|
||||||
};
|
};
|
||||||
"/v1/sessions/{session_id}/events": {
|
"/v1/sessions/{session_id}/events": {
|
||||||
|
/**
|
||||||
|
* Get Events
|
||||||
|
* @description Returns session events with optional offset-based pagination.
|
||||||
|
*/
|
||||||
get: operations["get_events"];
|
get: operations["get_events"];
|
||||||
};
|
};
|
||||||
"/v1/sessions/{session_id}/events/sse": {
|
"/v1/sessions/{session_id}/events/sse": {
|
||||||
|
/**
|
||||||
|
* Subscribe to Events (SSE)
|
||||||
|
* @description Opens an SSE stream for real-time session events.
|
||||||
|
*/
|
||||||
get: operations["get_events_sse"];
|
get: operations["get_events_sse"];
|
||||||
};
|
};
|
||||||
"/v1/sessions/{session_id}/messages": {
|
"/v1/sessions/{session_id}/messages": {
|
||||||
|
/**
|
||||||
|
* Send Message
|
||||||
|
* @description Sends a message to a session and returns immediately.
|
||||||
|
*/
|
||||||
post: operations["post_message"];
|
post: operations["post_message"];
|
||||||
};
|
};
|
||||||
"/v1/sessions/{session_id}/messages/stream": {
|
"/v1/sessions/{session_id}/messages/stream": {
|
||||||
|
/**
|
||||||
|
* Send Message (Streaming)
|
||||||
|
* @description Sends a message and returns an SSE event stream of the agent's response.
|
||||||
|
*/
|
||||||
post: operations["post_message_stream"];
|
post: operations["post_message_stream"];
|
||||||
};
|
};
|
||||||
"/v1/sessions/{session_id}/permissions/{permission_id}/reply": {
|
"/v1/sessions/{session_id}/permissions/{permission_id}/reply": {
|
||||||
|
/**
|
||||||
|
* Reply to Permission
|
||||||
|
* @description Approves or denies a permission request from the agent.
|
||||||
|
*/
|
||||||
post: operations["reply_permission"];
|
post: operations["reply_permission"];
|
||||||
};
|
};
|
||||||
"/v1/sessions/{session_id}/questions/{question_id}/reject": {
|
"/v1/sessions/{session_id}/questions/{question_id}/reject": {
|
||||||
|
/**
|
||||||
|
* Reject Question
|
||||||
|
* @description Rejects a human-in-the-loop question from the agent.
|
||||||
|
*/
|
||||||
post: operations["reject_question"];
|
post: operations["reject_question"];
|
||||||
};
|
};
|
||||||
"/v1/sessions/{session_id}/questions/{question_id}/reply": {
|
"/v1/sessions/{session_id}/questions/{question_id}/reply": {
|
||||||
|
/**
|
||||||
|
* Reply to Question
|
||||||
|
* @description Replies to a human-in-the-loop question from the agent.
|
||||||
|
*/
|
||||||
post: operations["reply_question"];
|
post: operations["reply_question"];
|
||||||
};
|
};
|
||||||
"/v1/sessions/{session_id}/terminate": {
|
"/v1/sessions/{session_id}/terminate": {
|
||||||
|
/**
|
||||||
|
* Terminate Session
|
||||||
|
* @description Terminates a running session and cleans up resources.
|
||||||
|
*/
|
||||||
post: operations["terminate_session"];
|
post: operations["terminate_session"];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -76,7 +190,6 @@ export interface components {
|
||||||
textMessages: boolean;
|
textMessages: boolean;
|
||||||
toolCalls: boolean;
|
toolCalls: boolean;
|
||||||
toolResults: boolean;
|
toolResults: boolean;
|
||||||
variants: boolean;
|
|
||||||
};
|
};
|
||||||
AgentError: {
|
AgentError: {
|
||||||
agent?: string | null;
|
agent?: string | null;
|
||||||
|
|
@ -170,8 +283,12 @@ export interface components {
|
||||||
agentMode?: string | null;
|
agentMode?: string | null;
|
||||||
agentVersion?: string | null;
|
agentVersion?: string | null;
|
||||||
directory?: string | null;
|
directory?: string | null;
|
||||||
|
mcp?: {
|
||||||
|
[key: string]: components["schemas"]["McpServerConfig"];
|
||||||
|
} | null;
|
||||||
model?: string | null;
|
model?: string | null;
|
||||||
permissionMode?: string | null;
|
permissionMode?: string | null;
|
||||||
|
skills?: components["schemas"]["SkillsConfig"] | null;
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
variant?: string | null;
|
variant?: string | null;
|
||||||
};
|
};
|
||||||
|
|
@ -202,6 +319,64 @@ export interface components {
|
||||||
};
|
};
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
FileAction: "read" | "write" | "patch";
|
FileAction: "read" | "write" | "patch";
|
||||||
|
FsActionResponse: {
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
FsDeleteQuery: {
|
||||||
|
path: string;
|
||||||
|
recursive?: boolean | null;
|
||||||
|
sessionId?: string | null;
|
||||||
|
};
|
||||||
|
FsEntriesQuery: {
|
||||||
|
path?: string | null;
|
||||||
|
sessionId?: string | null;
|
||||||
|
};
|
||||||
|
FsEntry: {
|
||||||
|
entryType: components["schemas"]["FsEntryType"];
|
||||||
|
modified?: string | null;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
/** Format: int64 */
|
||||||
|
size: number;
|
||||||
|
};
|
||||||
|
/** @enum {string} */
|
||||||
|
FsEntryType: "file" | "directory";
|
||||||
|
FsMoveRequest: {
|
||||||
|
from: string;
|
||||||
|
overwrite?: boolean | null;
|
||||||
|
to: string;
|
||||||
|
};
|
||||||
|
FsMoveResponse: {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
};
|
||||||
|
FsPathQuery: {
|
||||||
|
path: string;
|
||||||
|
sessionId?: string | null;
|
||||||
|
};
|
||||||
|
FsSessionQuery: {
|
||||||
|
sessionId?: string | null;
|
||||||
|
};
|
||||||
|
FsStat: {
|
||||||
|
entryType: components["schemas"]["FsEntryType"];
|
||||||
|
modified?: string | null;
|
||||||
|
path: string;
|
||||||
|
/** Format: int64 */
|
||||||
|
size: number;
|
||||||
|
};
|
||||||
|
FsUploadBatchQuery: {
|
||||||
|
path?: string | null;
|
||||||
|
sessionId?: string | null;
|
||||||
|
};
|
||||||
|
FsUploadBatchResponse: {
|
||||||
|
paths: string[];
|
||||||
|
truncated: boolean;
|
||||||
|
};
|
||||||
|
FsWriteResponse: {
|
||||||
|
/** Format: int64 */
|
||||||
|
bytesWritten: number;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
HealthResponse: {
|
HealthResponse: {
|
||||||
status: string;
|
status: string;
|
||||||
};
|
};
|
||||||
|
|
@ -219,7 +394,51 @@ export interface components {
|
||||||
ItemRole: "user" | "assistant" | "system" | "tool";
|
ItemRole: "user" | "assistant" | "system" | "tool";
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
ItemStatus: "in_progress" | "completed" | "failed";
|
ItemStatus: "in_progress" | "completed" | "failed";
|
||||||
|
McpCommand: string | string[];
|
||||||
|
McpOAuthConfig: {
|
||||||
|
clientId?: string | null;
|
||||||
|
clientSecret?: string | null;
|
||||||
|
scope?: string | null;
|
||||||
|
};
|
||||||
|
McpOAuthConfigOrDisabled: components["schemas"]["McpOAuthConfig"] | boolean;
|
||||||
|
/** @enum {string} */
|
||||||
|
McpRemoteTransport: "http" | "sse";
|
||||||
|
McpServerConfig: ({
|
||||||
|
args?: string[];
|
||||||
|
command: components["schemas"]["McpCommand"];
|
||||||
|
cwd?: string | null;
|
||||||
|
enabled?: boolean | null;
|
||||||
|
env?: {
|
||||||
|
[key: string]: string;
|
||||||
|
} | null;
|
||||||
|
/** Format: int64 */
|
||||||
|
timeoutMs?: number | null;
|
||||||
|
/** @enum {string} */
|
||||||
|
type: "local";
|
||||||
|
}) | ({
|
||||||
|
bearerTokenEnvVar?: string | null;
|
||||||
|
enabled?: boolean | null;
|
||||||
|
envHeaders?: {
|
||||||
|
[key: string]: string;
|
||||||
|
} | null;
|
||||||
|
headers?: {
|
||||||
|
[key: string]: string;
|
||||||
|
} | null;
|
||||||
|
oauth?: components["schemas"]["McpOAuthConfigOrDisabled"] | null;
|
||||||
|
/** Format: int64 */
|
||||||
|
timeoutMs?: number | null;
|
||||||
|
transport?: components["schemas"]["McpRemoteTransport"] | null;
|
||||||
|
/** @enum {string} */
|
||||||
|
type: "remote";
|
||||||
|
url: string;
|
||||||
|
});
|
||||||
|
MessageAttachment: {
|
||||||
|
filename?: string | null;
|
||||||
|
mime?: string | null;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
MessageRequest: {
|
MessageRequest: {
|
||||||
|
attachments?: components["schemas"]["MessageAttachment"][];
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
PermissionEventData: {
|
PermissionEventData: {
|
||||||
|
|
@ -295,10 +514,14 @@ export interface components {
|
||||||
ended: boolean;
|
ended: boolean;
|
||||||
/** Format: int64 */
|
/** Format: int64 */
|
||||||
eventCount: number;
|
eventCount: number;
|
||||||
|
mcp?: {
|
||||||
|
[key: string]: components["schemas"]["McpServerConfig"];
|
||||||
|
} | null;
|
||||||
model?: string | null;
|
model?: string | null;
|
||||||
nativeSessionId?: string | null;
|
nativeSessionId?: string | null;
|
||||||
permissionMode: string;
|
permissionMode: string;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
|
skills?: components["schemas"]["SkillsConfig"] | null;
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
/** Format: int64 */
|
/** Format: int64 */
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
|
|
@ -310,6 +533,16 @@ export interface components {
|
||||||
SessionStartedData: {
|
SessionStartedData: {
|
||||||
metadata?: unknown;
|
metadata?: unknown;
|
||||||
};
|
};
|
||||||
|
SkillSource: {
|
||||||
|
ref?: string | null;
|
||||||
|
skills?: string[] | null;
|
||||||
|
source: string;
|
||||||
|
subpath?: string | null;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
SkillsConfig: {
|
||||||
|
sources: components["schemas"]["SkillSource"][];
|
||||||
|
};
|
||||||
StderrOutput: {
|
StderrOutput: {
|
||||||
/** @description First N lines of stderr (if truncated) or full stderr (if not truncated) */
|
/** @description First N lines of stderr (if truncated) or full stderr (if not truncated) */
|
||||||
head?: string | null;
|
head?: string | null;
|
||||||
|
|
@ -371,8 +604,13 @@ export type external = Record<string, never>;
|
||||||
|
|
||||||
export interface operations {
|
export interface operations {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List Agents
|
||||||
|
* @description Returns all available coding agents and their installation status.
|
||||||
|
*/
|
||||||
list_agents: {
|
list_agents: {
|
||||||
responses: {
|
responses: {
|
||||||
|
/** @description List of available agents */
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["AgentListResponse"];
|
"application/json": components["schemas"]["AgentListResponse"];
|
||||||
|
|
@ -380,6 +618,10 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Install Agent
|
||||||
|
* @description Installs or updates a coding agent (e.g. claude, codex, opencode, amp).
|
||||||
|
*/
|
||||||
install_agent: {
|
install_agent: {
|
||||||
parameters: {
|
parameters: {
|
||||||
path: {
|
path: {
|
||||||
|
|
@ -397,16 +639,19 @@ export interface operations {
|
||||||
204: {
|
204: {
|
||||||
content: never;
|
content: never;
|
||||||
};
|
};
|
||||||
|
/** @description Invalid request */
|
||||||
400: {
|
400: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["ProblemDetails"];
|
"application/json": components["schemas"]["ProblemDetails"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/** @description Agent not found */
|
||||||
404: {
|
404: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["ProblemDetails"];
|
"application/json": components["schemas"]["ProblemDetails"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/** @description Installation failed */
|
||||||
500: {
|
500: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["ProblemDetails"];
|
"application/json": components["schemas"]["ProblemDetails"];
|
||||||
|
|
@ -414,6 +659,10 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* List Agent Models
|
||||||
|
* @description Returns the available LLM models for an agent.
|
||||||
|
*/
|
||||||
get_agent_models: {
|
get_agent_models: {
|
||||||
parameters: {
|
parameters: {
|
||||||
path: {
|
path: {
|
||||||
|
|
@ -422,18 +671,24 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
|
/** @description Available models */
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["AgentModelsResponse"];
|
"application/json": components["schemas"]["AgentModelsResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
400: {
|
/** @description Agent not found */
|
||||||
|
404: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["ProblemDetails"];
|
"application/json": components["schemas"]["ProblemDetails"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* List Agent Modes
|
||||||
|
* @description Returns the available interaction modes for an agent.
|
||||||
|
*/
|
||||||
get_agent_modes: {
|
get_agent_modes: {
|
||||||
parameters: {
|
parameters: {
|
||||||
path: {
|
path: {
|
||||||
|
|
@ -442,11 +697,13 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
|
/** @description Available modes */
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["AgentModesResponse"];
|
"application/json": components["schemas"]["AgentModesResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/** @description Invalid request */
|
||||||
400: {
|
400: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["ProblemDetails"];
|
"application/json": components["schemas"]["ProblemDetails"];
|
||||||
|
|
@ -454,8 +711,204 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* List Directory
|
||||||
|
* @description Lists files and directories at the given path.
|
||||||
|
*/
|
||||||
|
fs_entries: {
|
||||||
|
parameters: {
|
||||||
|
query?: {
|
||||||
|
/** @description Path to list (relative or absolute) */
|
||||||
|
path?: string | null;
|
||||||
|
/** @description Session id for relative paths */
|
||||||
|
session_id?: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description Directory listing */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["FsEntry"][];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Delete Entry
|
||||||
|
* @description Deletes a file or directory.
|
||||||
|
*/
|
||||||
|
fs_delete_entry: {
|
||||||
|
parameters: {
|
||||||
|
query: {
|
||||||
|
/** @description File or directory path */
|
||||||
|
path: string;
|
||||||
|
/** @description Session id for relative paths */
|
||||||
|
session_id?: string | null;
|
||||||
|
/** @description Delete directories recursively */
|
||||||
|
recursive?: boolean | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description Delete result */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["FsActionResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Read File
|
||||||
|
* @description Reads the raw bytes of a file.
|
||||||
|
*/
|
||||||
|
fs_read_file: {
|
||||||
|
parameters: {
|
||||||
|
query: {
|
||||||
|
/** @description File path (relative or absolute) */
|
||||||
|
path: string;
|
||||||
|
/** @description Session id for relative paths */
|
||||||
|
session_id?: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description File content */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/octet-stream": string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Write File
|
||||||
|
* @description Writes raw bytes to a file, creating it if it doesn't exist.
|
||||||
|
*/
|
||||||
|
fs_write_file: {
|
||||||
|
parameters: {
|
||||||
|
query: {
|
||||||
|
/** @description File path (relative or absolute) */
|
||||||
|
path: string;
|
||||||
|
/** @description Session id for relative paths */
|
||||||
|
session_id?: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/octet-stream": string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description Write result */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["FsWriteResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Create Directory
|
||||||
|
* @description Creates a directory, including any missing parent directories.
|
||||||
|
*/
|
||||||
|
fs_mkdir: {
|
||||||
|
parameters: {
|
||||||
|
query: {
|
||||||
|
/** @description Directory path to create */
|
||||||
|
path: string;
|
||||||
|
/** @description Session id for relative paths */
|
||||||
|
session_id?: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description Directory created */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["FsActionResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Move Entry
|
||||||
|
* @description Moves or renames a file or directory.
|
||||||
|
*/
|
||||||
|
fs_move: {
|
||||||
|
parameters: {
|
||||||
|
query?: {
|
||||||
|
/** @description Session id for relative paths */
|
||||||
|
session_id?: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["FsMoveRequest"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description Move result */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["FsMoveResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Get File Info
|
||||||
|
* @description Returns metadata (size, timestamps, type) for a path.
|
||||||
|
*/
|
||||||
|
fs_stat: {
|
||||||
|
parameters: {
|
||||||
|
query: {
|
||||||
|
/** @description Path to stat */
|
||||||
|
path: string;
|
||||||
|
/** @description Session id for relative paths */
|
||||||
|
session_id?: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description File metadata */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["FsStat"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Upload Files
|
||||||
|
* @description Uploads a tar.gz archive and extracts it to the destination directory.
|
||||||
|
*/
|
||||||
|
fs_upload_batch: {
|
||||||
|
parameters: {
|
||||||
|
query?: {
|
||||||
|
/** @description Destination directory for extraction */
|
||||||
|
path?: string | null;
|
||||||
|
/** @description Session id for relative paths */
|
||||||
|
session_id?: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/octet-stream": string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description Upload result */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["FsUploadBatchResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Health Check
|
||||||
|
* @description Returns the server health status.
|
||||||
|
*/
|
||||||
get_health: {
|
get_health: {
|
||||||
responses: {
|
responses: {
|
||||||
|
/** @description Server is healthy */
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["HealthResponse"];
|
"application/json": components["schemas"]["HealthResponse"];
|
||||||
|
|
@ -463,8 +916,13 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* List Sessions
|
||||||
|
* @description Returns all active sessions.
|
||||||
|
*/
|
||||||
list_sessions: {
|
list_sessions: {
|
||||||
responses: {
|
responses: {
|
||||||
|
/** @description List of active sessions */
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["SessionListResponse"];
|
"application/json": components["schemas"]["SessionListResponse"];
|
||||||
|
|
@ -472,6 +930,10 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Create Session
|
||||||
|
* @description Creates a new agent session with the given configuration.
|
||||||
|
*/
|
||||||
create_session: {
|
create_session: {
|
||||||
parameters: {
|
parameters: {
|
||||||
path: {
|
path: {
|
||||||
|
|
@ -485,16 +947,19 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
|
/** @description Session created */
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["CreateSessionResponse"];
|
"application/json": components["schemas"]["CreateSessionResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/** @description Invalid request */
|
||||||
400: {
|
400: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["ProblemDetails"];
|
"application/json": components["schemas"]["ProblemDetails"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/** @description Session already exists */
|
||||||
409: {
|
409: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["ProblemDetails"];
|
"application/json": components["schemas"]["ProblemDetails"];
|
||||||
|
|
@ -502,6 +967,10 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Get Events
|
||||||
|
* @description Returns session events with optional offset-based pagination.
|
||||||
|
*/
|
||||||
get_events: {
|
get_events: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: {
|
query?: {
|
||||||
|
|
@ -518,11 +987,13 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
|
/** @description Session events */
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["EventsResponse"];
|
"application/json": components["schemas"]["EventsResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/** @description Session not found */
|
||||||
404: {
|
404: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["ProblemDetails"];
|
"application/json": components["schemas"]["ProblemDetails"];
|
||||||
|
|
@ -530,6 +1001,10 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Subscribe to Events (SSE)
|
||||||
|
* @description Opens an SSE stream for real-time session events.
|
||||||
|
*/
|
||||||
get_events_sse: {
|
get_events_sse: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: {
|
query?: {
|
||||||
|
|
@ -550,6 +1025,10 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Send Message
|
||||||
|
* @description Sends a message to a session and returns immediately.
|
||||||
|
*/
|
||||||
post_message: {
|
post_message: {
|
||||||
parameters: {
|
parameters: {
|
||||||
path: {
|
path: {
|
||||||
|
|
@ -567,6 +1046,7 @@ export interface operations {
|
||||||
204: {
|
204: {
|
||||||
content: never;
|
content: never;
|
||||||
};
|
};
|
||||||
|
/** @description Session not found */
|
||||||
404: {
|
404: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["ProblemDetails"];
|
"application/json": components["schemas"]["ProblemDetails"];
|
||||||
|
|
@ -574,6 +1054,10 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Send Message (Streaming)
|
||||||
|
* @description Sends a message and returns an SSE event stream of the agent's response.
|
||||||
|
*/
|
||||||
post_message_stream: {
|
post_message_stream: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: {
|
query?: {
|
||||||
|
|
@ -595,6 +1079,7 @@ export interface operations {
|
||||||
200: {
|
200: {
|
||||||
content: never;
|
content: never;
|
||||||
};
|
};
|
||||||
|
/** @description Session not found */
|
||||||
404: {
|
404: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["ProblemDetails"];
|
"application/json": components["schemas"]["ProblemDetails"];
|
||||||
|
|
@ -602,6 +1087,10 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Reply to Permission
|
||||||
|
* @description Approves or denies a permission request from the agent.
|
||||||
|
*/
|
||||||
reply_permission: {
|
reply_permission: {
|
||||||
parameters: {
|
parameters: {
|
||||||
path: {
|
path: {
|
||||||
|
|
@ -621,6 +1110,7 @@ export interface operations {
|
||||||
204: {
|
204: {
|
||||||
content: never;
|
content: never;
|
||||||
};
|
};
|
||||||
|
/** @description Session or permission not found */
|
||||||
404: {
|
404: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["ProblemDetails"];
|
"application/json": components["schemas"]["ProblemDetails"];
|
||||||
|
|
@ -628,6 +1118,10 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Reject Question
|
||||||
|
* @description Rejects a human-in-the-loop question from the agent.
|
||||||
|
*/
|
||||||
reject_question: {
|
reject_question: {
|
||||||
parameters: {
|
parameters: {
|
||||||
path: {
|
path: {
|
||||||
|
|
@ -642,6 +1136,7 @@ export interface operations {
|
||||||
204: {
|
204: {
|
||||||
content: never;
|
content: never;
|
||||||
};
|
};
|
||||||
|
/** @description Session or question not found */
|
||||||
404: {
|
404: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["ProblemDetails"];
|
"application/json": components["schemas"]["ProblemDetails"];
|
||||||
|
|
@ -649,6 +1144,10 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Reply to Question
|
||||||
|
* @description Replies to a human-in-the-loop question from the agent.
|
||||||
|
*/
|
||||||
reply_question: {
|
reply_question: {
|
||||||
parameters: {
|
parameters: {
|
||||||
path: {
|
path: {
|
||||||
|
|
@ -668,6 +1167,7 @@ export interface operations {
|
||||||
204: {
|
204: {
|
||||||
content: never;
|
content: never;
|
||||||
};
|
};
|
||||||
|
/** @description Session or question not found */
|
||||||
404: {
|
404: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["ProblemDetails"];
|
"application/json": components["schemas"]["ProblemDetails"];
|
||||||
|
|
@ -675,6 +1175,10 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Terminate Session
|
||||||
|
* @description Terminates a running session and cleans up resources.
|
||||||
|
*/
|
||||||
terminate_session: {
|
terminate_session: {
|
||||||
parameters: {
|
parameters: {
|
||||||
path: {
|
path: {
|
||||||
|
|
@ -687,6 +1191,7 @@ export interface operations {
|
||||||
204: {
|
204: {
|
||||||
content: never;
|
content: never;
|
||||||
};
|
};
|
||||||
|
/** @description Session not found */
|
||||||
404: {
|
404: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["ProblemDetails"];
|
"application/json": components["schemas"]["ProblemDetails"];
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,26 @@ export type {
|
||||||
EventsQuery,
|
EventsQuery,
|
||||||
EventsResponse,
|
EventsResponse,
|
||||||
FileAction,
|
FileAction,
|
||||||
|
FsActionResponse,
|
||||||
|
FsDeleteQuery,
|
||||||
|
FsEntriesQuery,
|
||||||
|
FsEntry,
|
||||||
|
FsEntryType,
|
||||||
|
FsMoveRequest,
|
||||||
|
FsMoveResponse,
|
||||||
|
FsPathQuery,
|
||||||
|
FsSessionQuery,
|
||||||
|
FsStat,
|
||||||
|
FsUploadBatchQuery,
|
||||||
|
FsUploadBatchResponse,
|
||||||
|
FsWriteResponse,
|
||||||
HealthResponse,
|
HealthResponse,
|
||||||
ItemDeltaData,
|
ItemDeltaData,
|
||||||
ItemEventData,
|
ItemEventData,
|
||||||
ItemKind,
|
ItemKind,
|
||||||
ItemRole,
|
ItemRole,
|
||||||
ItemStatus,
|
ItemStatus,
|
||||||
|
MessageAttachment,
|
||||||
MessageRequest,
|
MessageRequest,
|
||||||
PermissionEventData,
|
PermissionEventData,
|
||||||
PermissionReply,
|
PermissionReply,
|
||||||
|
|
@ -50,6 +64,13 @@ export type {
|
||||||
UniversalEventData,
|
UniversalEventData,
|
||||||
UniversalEventType,
|
UniversalEventType,
|
||||||
UniversalItem,
|
UniversalItem,
|
||||||
|
McpServerConfig,
|
||||||
|
McpCommand,
|
||||||
|
McpRemoteTransport,
|
||||||
|
McpOAuthConfig,
|
||||||
|
McpOAuthConfigOrDisabled,
|
||||||
|
SkillSource,
|
||||||
|
SkillsConfig,
|
||||||
} from "./types.ts";
|
} from "./types.ts";
|
||||||
export type { components, paths } from "./generated/openapi.ts";
|
export type { components, paths } from "./generated/openapi.ts";
|
||||||
export type { SandboxAgentSpawnOptions, SandboxAgentSpawnLogMode } from "./spawn.ts";
|
export type { SandboxAgentSpawnOptions, SandboxAgentSpawnLogMode } from "./spawn.ts";
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,19 @@ export type EventSource = S["EventSource"];
|
||||||
export type EventsQuery = S["EventsQuery"];
|
export type EventsQuery = S["EventsQuery"];
|
||||||
export type EventsResponse = S["EventsResponse"];
|
export type EventsResponse = S["EventsResponse"];
|
||||||
export type FileAction = S["FileAction"];
|
export type FileAction = S["FileAction"];
|
||||||
|
export type FsActionResponse = S["FsActionResponse"];
|
||||||
|
export type FsDeleteQuery = S["FsDeleteQuery"];
|
||||||
|
export type FsEntriesQuery = S["FsEntriesQuery"];
|
||||||
|
export type FsEntry = S["FsEntry"];
|
||||||
|
export type FsEntryType = S["FsEntryType"];
|
||||||
|
export type FsMoveRequest = S["FsMoveRequest"];
|
||||||
|
export type FsMoveResponse = S["FsMoveResponse"];
|
||||||
|
export type FsPathQuery = S["FsPathQuery"];
|
||||||
|
export type FsSessionQuery = S["FsSessionQuery"];
|
||||||
|
export type FsStat = S["FsStat"];
|
||||||
|
export type FsUploadBatchQuery = S["FsUploadBatchQuery"];
|
||||||
|
export type FsUploadBatchResponse = S["FsUploadBatchResponse"];
|
||||||
|
export type FsWriteResponse = S["FsWriteResponse"];
|
||||||
export type HealthResponse = S["HealthResponse"];
|
export type HealthResponse = S["HealthResponse"];
|
||||||
export type ItemDeltaData = S["ItemDeltaData"];
|
export type ItemDeltaData = S["ItemDeltaData"];
|
||||||
export type ItemEventData = S["ItemEventData"];
|
export type ItemEventData = S["ItemEventData"];
|
||||||
|
|
@ -26,6 +39,7 @@ export type ItemKind = S["ItemKind"];
|
||||||
export type ItemRole = S["ItemRole"];
|
export type ItemRole = S["ItemRole"];
|
||||||
export type ItemStatus = S["ItemStatus"];
|
export type ItemStatus = S["ItemStatus"];
|
||||||
export type MessageRequest = S["MessageRequest"];
|
export type MessageRequest = S["MessageRequest"];
|
||||||
|
export type MessageAttachment = S["MessageAttachment"];
|
||||||
export type PermissionEventData = S["PermissionEventData"];
|
export type PermissionEventData = S["PermissionEventData"];
|
||||||
export type PermissionReply = S["PermissionReply"];
|
export type PermissionReply = S["PermissionReply"];
|
||||||
export type PermissionReplyRequest = S["PermissionReplyRequest"];
|
export type PermissionReplyRequest = S["PermissionReplyRequest"];
|
||||||
|
|
@ -46,3 +60,11 @@ export type UniversalEvent = S["UniversalEvent"];
|
||||||
export type UniversalEventData = S["UniversalEventData"];
|
export type UniversalEventData = S["UniversalEventData"];
|
||||||
export type UniversalEventType = S["UniversalEventType"];
|
export type UniversalEventType = S["UniversalEventType"];
|
||||||
export type UniversalItem = S["UniversalItem"];
|
export type UniversalItem = S["UniversalItem"];
|
||||||
|
|
||||||
|
export type McpServerConfig = S["McpServerConfig"];
|
||||||
|
export type McpCommand = S["McpCommand"];
|
||||||
|
export type McpRemoteTransport = S["McpRemoteTransport"];
|
||||||
|
export type McpOAuthConfig = S["McpOAuthConfig"];
|
||||||
|
export type McpOAuthConfigOrDisabled = S["McpOAuthConfigOrDisabled"];
|
||||||
|
export type SkillSource = S["SkillSource"];
|
||||||
|
export type SkillsConfig = S["SkillsConfig"];
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
See [ARCHITECTURE.md](./ARCHITECTURE.md) for detailed architecture documentation covering the daemon, agent schema pipeline, session management, agent execution patterns, and SDK modes.
|
See [ARCHITECTURE.md](./ARCHITECTURE.md) for detailed architecture documentation covering the daemon, agent schema pipeline, session management, agent execution patterns, and SDK modes.
|
||||||
|
|
||||||
|
## Skill Source Installation
|
||||||
|
|
||||||
|
Skills are installed via `skills.sources` in the session create request. The [vercel-labs/skills](https://github.com/vercel-labs/skills) repo (`~/misc/skills`) provides reference for skill installation patterns and source parsing logic. The server handles fetching GitHub repos (via zip download) and git repos (via clone) to `~/.sandbox-agent/skills-cache/`, discovering `SKILL.md` files, and symlinking into agent skill roots.
|
||||||
|
|
||||||
# Server Testing
|
# Server Testing
|
||||||
|
|
||||||
## Test placement
|
## Test placement
|
||||||
|
|
|
||||||
|
|
@ -743,7 +743,13 @@ fn parse_version_output(output: &std::process::Output) -> Option<String> {
|
||||||
.lines()
|
.lines()
|
||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
.find(|line| !line.is_empty())
|
.find(|line| !line.is_empty())
|
||||||
.map(|line| line.to_string())
|
.map(|line| {
|
||||||
|
// Strip trailing metadata like " (released ...)" from version strings
|
||||||
|
match line.find(" (") {
|
||||||
|
Some(pos) => line[..pos].to_string(),
|
||||||
|
None => line.to_string(),
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_jsonl(text: &str) -> Vec<Value> {
|
fn parse_jsonl(text: &str) -> Vec<Value> {
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,9 @@ tracing-logfmt.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
include_dir.workspace = true
|
include_dir.workspace = true
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
|
toml_edit.workspace = true
|
||||||
|
tar.workspace = true
|
||||||
|
zip.workspace = true
|
||||||
tempfile = { workspace = true, optional = true }
|
tempfile = { workspace = true, optional = true }
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::{Command as ProcessCommand, Stdio};
|
use std::process::{Command as ProcessCommand, Stdio};
|
||||||
|
|
@ -13,12 +14,14 @@ mod build_version {
|
||||||
}
|
}
|
||||||
use crate::router::{build_router_with_state, shutdown_servers};
|
use crate::router::{build_router_with_state, shutdown_servers};
|
||||||
use crate::router::{
|
use crate::router::{
|
||||||
AgentInstallRequest, AppState, AuthConfig, BrandingMode, CreateSessionRequest, MessageRequest,
|
AgentInstallRequest, AppState, AuthConfig, BrandingMode, CreateSessionRequest, McpServerConfig,
|
||||||
PermissionReply, PermissionReplyRequest, QuestionReplyRequest,
|
MessageRequest, PermissionReply, PermissionReplyRequest, QuestionReplyRequest, SkillSource,
|
||||||
|
SkillsConfig,
|
||||||
};
|
};
|
||||||
use crate::router::{
|
use crate::router::{
|
||||||
AgentListResponse, AgentModelsResponse, AgentModesResponse, CreateSessionResponse,
|
AgentListResponse, AgentModelsResponse, AgentModesResponse, CreateSessionResponse, EventsResponse,
|
||||||
EventsResponse, SessionListResponse,
|
FsActionResponse, FsEntry, FsMoveRequest, FsMoveResponse, FsStat, FsUploadBatchResponse,
|
||||||
|
FsWriteResponse, SessionListResponse,
|
||||||
};
|
};
|
||||||
use crate::server_logs::ServerLogs;
|
use crate::server_logs::ServerLogs;
|
||||||
use crate::telemetry;
|
use crate::telemetry;
|
||||||
|
|
@ -176,6 +179,10 @@ pub struct DaemonStartArgs {
|
||||||
|
|
||||||
#[arg(long, short = 'p', default_value_t = DEFAULT_PORT)]
|
#[arg(long, short = 'p', default_value_t = DEFAULT_PORT)]
|
||||||
port: u16,
|
port: u16,
|
||||||
|
|
||||||
|
/// If the daemon is already running but outdated, stop and restart it.
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
upgrade: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args, Debug)]
|
#[derive(Args, Debug)]
|
||||||
|
|
@ -202,6 +209,8 @@ pub enum ApiCommand {
|
||||||
Agents(AgentsArgs),
|
Agents(AgentsArgs),
|
||||||
/// Create sessions and interact with session events.
|
/// Create sessions and interact with session events.
|
||||||
Sessions(SessionsArgs),
|
Sessions(SessionsArgs),
|
||||||
|
/// Manage filesystem entries.
|
||||||
|
Fs(FsArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
|
|
@ -225,6 +234,12 @@ pub struct SessionsArgs {
|
||||||
command: SessionsCommand,
|
command: SessionsCommand,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct FsArgs {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: FsCommand,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
pub enum AgentsCommand {
|
pub enum AgentsCommand {
|
||||||
/// List all agents and install status.
|
/// List all agents and install status.
|
||||||
|
|
@ -272,6 +287,27 @@ pub enum SessionsCommand {
|
||||||
ReplyPermission(PermissionReplyArgs),
|
ReplyPermission(PermissionReplyArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
pub enum FsCommand {
|
||||||
|
/// List directory entries.
|
||||||
|
Entries(FsEntriesArgs),
|
||||||
|
/// Read a file.
|
||||||
|
Read(FsReadArgs),
|
||||||
|
/// Write a file.
|
||||||
|
Write(FsWriteArgs),
|
||||||
|
/// Delete a file or directory.
|
||||||
|
Delete(FsDeleteArgs),
|
||||||
|
/// Create a directory.
|
||||||
|
Mkdir(FsMkdirArgs),
|
||||||
|
/// Move a file or directory.
|
||||||
|
Move(FsMoveArgs),
|
||||||
|
/// Stat a file or directory.
|
||||||
|
Stat(FsStatArgs),
|
||||||
|
/// Upload a tar archive and extract it.
|
||||||
|
#[command(name = "upload-batch")]
|
||||||
|
UploadBatch(FsUploadBatchArgs),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Args, Debug, Clone)]
|
#[derive(Args, Debug, Clone)]
|
||||||
pub struct ClientArgs {
|
pub struct ClientArgs {
|
||||||
#[arg(long, short = 'e')]
|
#[arg(long, short = 'e')]
|
||||||
|
|
@ -323,6 +359,10 @@ pub struct CreateSessionArgs {
|
||||||
variant: Option<String>,
|
variant: Option<String>,
|
||||||
#[arg(long, short = 'A')]
|
#[arg(long, short = 'A')]
|
||||||
agent_version: Option<String>,
|
agent_version: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
mcp_config: Option<PathBuf>,
|
||||||
|
#[arg(long)]
|
||||||
|
skill: Vec<PathBuf>,
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
client: ClientArgs,
|
client: ClientArgs,
|
||||||
}
|
}
|
||||||
|
|
@ -406,6 +446,91 @@ pub struct PermissionReplyArgs {
|
||||||
client: ClientArgs,
|
client: ClientArgs,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct FsEntriesArgs {
|
||||||
|
#[arg(long)]
|
||||||
|
path: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
session_id: Option<String>,
|
||||||
|
#[command(flatten)]
|
||||||
|
client: ClientArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct FsReadArgs {
|
||||||
|
path: String,
|
||||||
|
#[arg(long)]
|
||||||
|
session_id: Option<String>,
|
||||||
|
#[command(flatten)]
|
||||||
|
client: ClientArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct FsWriteArgs {
|
||||||
|
path: String,
|
||||||
|
#[arg(long)]
|
||||||
|
content: Option<String>,
|
||||||
|
#[arg(long = "from-file")]
|
||||||
|
from_file: Option<PathBuf>,
|
||||||
|
#[arg(long)]
|
||||||
|
session_id: Option<String>,
|
||||||
|
#[command(flatten)]
|
||||||
|
client: ClientArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct FsDeleteArgs {
|
||||||
|
path: String,
|
||||||
|
#[arg(long)]
|
||||||
|
recursive: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
session_id: Option<String>,
|
||||||
|
#[command(flatten)]
|
||||||
|
client: ClientArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct FsMkdirArgs {
|
||||||
|
path: String,
|
||||||
|
#[arg(long)]
|
||||||
|
session_id: Option<String>,
|
||||||
|
#[command(flatten)]
|
||||||
|
client: ClientArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct FsMoveArgs {
|
||||||
|
from: String,
|
||||||
|
to: String,
|
||||||
|
#[arg(long)]
|
||||||
|
overwrite: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
session_id: Option<String>,
|
||||||
|
#[command(flatten)]
|
||||||
|
client: ClientArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct FsStatArgs {
|
||||||
|
path: String,
|
||||||
|
#[arg(long)]
|
||||||
|
session_id: Option<String>,
|
||||||
|
#[command(flatten)]
|
||||||
|
client: ClientArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct FsUploadBatchArgs {
|
||||||
|
#[arg(long = "tar")]
|
||||||
|
tar_path: PathBuf,
|
||||||
|
#[arg(long)]
|
||||||
|
path: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
session_id: Option<String>,
|
||||||
|
#[command(flatten)]
|
||||||
|
client: ClientArgs,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Args, Debug)]
|
#[derive(Args, Debug)]
|
||||||
pub struct CredentialsExtractArgs {
|
pub struct CredentialsExtractArgs {
|
||||||
#[arg(long, short = 'a', value_enum)]
|
#[arg(long, short = 'a', value_enum)]
|
||||||
|
|
@ -433,6 +558,8 @@ pub struct CredentialsExtractEnvArgs {
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum CliError {
|
pub enum CliError {
|
||||||
|
#[error("missing --token or --no-token for server mode")]
|
||||||
|
MissingToken,
|
||||||
#[error("invalid cors origin: {0}")]
|
#[error("invalid cors origin: {0}")]
|
||||||
InvalidCorsOrigin(String),
|
InvalidCorsOrigin(String),
|
||||||
#[error("invalid cors method: {0}")]
|
#[error("invalid cors method: {0}")]
|
||||||
|
|
@ -590,6 +717,7 @@ fn run_api(command: &ApiCommand, cli: &CliConfig) -> Result<(), CliError> {
|
||||||
match command {
|
match command {
|
||||||
ApiCommand::Agents(subcommand) => run_agents(&subcommand.command, cli),
|
ApiCommand::Agents(subcommand) => run_agents(&subcommand.command, cli),
|
||||||
ApiCommand::Sessions(subcommand) => run_sessions(&subcommand.command, cli),
|
ApiCommand::Sessions(subcommand) => run_sessions(&subcommand.command, cli),
|
||||||
|
ApiCommand::Fs(subcommand) => run_fs(&subcommand.command, cli),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -672,6 +800,9 @@ fn run_opencode(cli: &CliConfig, args: &OpencodeArgs) -> Result<(), CliError> {
|
||||||
fn run_daemon(command: &DaemonCommand, cli: &CliConfig) -> Result<(), CliError> {
|
fn run_daemon(command: &DaemonCommand, cli: &CliConfig) -> Result<(), CliError> {
|
||||||
let token = cli.token.as_deref();
|
let token = cli.token.as_deref();
|
||||||
match command {
|
match command {
|
||||||
|
DaemonCommand::Start(args) if args.upgrade => {
|
||||||
|
crate::daemon::ensure_running(cli, &args.host, args.port, token)
|
||||||
|
}
|
||||||
DaemonCommand::Start(args) => crate::daemon::start(cli, &args.host, args.port, token),
|
DaemonCommand::Start(args) => crate::daemon::start(cli, &args.host, args.port, token),
|
||||||
DaemonCommand::Stop(args) => crate::daemon::stop(&args.host, args.port),
|
DaemonCommand::Stop(args) => crate::daemon::stop(&args.host, args.port),
|
||||||
DaemonCommand::Status(args) => {
|
DaemonCommand::Status(args) => {
|
||||||
|
|
@ -722,6 +853,33 @@ fn run_sessions(command: &SessionsCommand, cli: &CliConfig) -> Result<(), CliErr
|
||||||
}
|
}
|
||||||
SessionsCommand::Create(args) => {
|
SessionsCommand::Create(args) => {
|
||||||
let ctx = ClientContext::new(cli, &args.client)?;
|
let ctx = ClientContext::new(cli, &args.client)?;
|
||||||
|
let mcp = if let Some(path) = &args.mcp_config {
|
||||||
|
let text = std::fs::read_to_string(path)?;
|
||||||
|
let parsed =
|
||||||
|
serde_json::from_str::<std::collections::BTreeMap<String, McpServerConfig>>(
|
||||||
|
&text,
|
||||||
|
)?;
|
||||||
|
Some(parsed)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let skills = if args.skill.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(SkillsConfig {
|
||||||
|
sources: args
|
||||||
|
.skill
|
||||||
|
.iter()
|
||||||
|
.map(|path| SkillSource {
|
||||||
|
source_type: "local".to_string(),
|
||||||
|
source: path.to_string_lossy().to_string(),
|
||||||
|
skills: None,
|
||||||
|
git_ref: None,
|
||||||
|
subpath: None,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
};
|
||||||
let body = CreateSessionRequest {
|
let body = CreateSessionRequest {
|
||||||
agent: args.agent.clone(),
|
agent: args.agent.clone(),
|
||||||
agent_mode: args.agent_mode.clone(),
|
agent_mode: args.agent_mode.clone(),
|
||||||
|
|
@ -731,6 +889,8 @@ fn run_sessions(command: &SessionsCommand, cli: &CliConfig) -> Result<(), CliErr
|
||||||
agent_version: args.agent_version.clone(),
|
agent_version: args.agent_version.clone(),
|
||||||
directory: None,
|
directory: None,
|
||||||
title: None,
|
title: None,
|
||||||
|
mcp,
|
||||||
|
skills,
|
||||||
};
|
};
|
||||||
let path = format!("{API_PREFIX}/sessions/{}", args.session_id);
|
let path = format!("{API_PREFIX}/sessions/{}", args.session_id);
|
||||||
let response = ctx.post(&path, &body)?;
|
let response = ctx.post(&path, &body)?;
|
||||||
|
|
@ -740,6 +900,7 @@ fn run_sessions(command: &SessionsCommand, cli: &CliConfig) -> Result<(), CliErr
|
||||||
let ctx = ClientContext::new(cli, &args.client)?;
|
let ctx = ClientContext::new(cli, &args.client)?;
|
||||||
let body = MessageRequest {
|
let body = MessageRequest {
|
||||||
message: args.message.clone(),
|
message: args.message.clone(),
|
||||||
|
attachments: Vec::new(),
|
||||||
};
|
};
|
||||||
let path = format!("{API_PREFIX}/sessions/{}/messages", args.session_id);
|
let path = format!("{API_PREFIX}/sessions/{}/messages", args.session_id);
|
||||||
let response = ctx.post(&path, &body)?;
|
let response = ctx.post(&path, &body)?;
|
||||||
|
|
@ -749,6 +910,7 @@ fn run_sessions(command: &SessionsCommand, cli: &CliConfig) -> Result<(), CliErr
|
||||||
let ctx = ClientContext::new(cli, &args.client)?;
|
let ctx = ClientContext::new(cli, &args.client)?;
|
||||||
let body = MessageRequest {
|
let body = MessageRequest {
|
||||||
message: args.message.clone(),
|
message: args.message.clone(),
|
||||||
|
attachments: Vec::new(),
|
||||||
};
|
};
|
||||||
let path = format!("{API_PREFIX}/sessions/{}/messages/stream", args.session_id);
|
let path = format!("{API_PREFIX}/sessions/{}/messages/stream", args.session_id);
|
||||||
let response = ctx.post_with_query(
|
let response = ctx.post_with_query(
|
||||||
|
|
@ -845,6 +1007,129 @@ fn run_sessions(command: &SessionsCommand, cli: &CliConfig) -> Result<(), CliErr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_fs(command: &FsCommand, cli: &CliConfig) -> Result<(), CliError> {
|
||||||
|
match command {
|
||||||
|
FsCommand::Entries(args) => {
|
||||||
|
let ctx = ClientContext::new(cli, &args.client)?;
|
||||||
|
let response = ctx.get_with_query(
|
||||||
|
&format!("{API_PREFIX}/fs/entries"),
|
||||||
|
&[
|
||||||
|
("path", args.path.clone()),
|
||||||
|
("session_id", args.session_id.clone()),
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
print_json_response::<Vec<FsEntry>>(response)
|
||||||
|
}
|
||||||
|
FsCommand::Read(args) => {
|
||||||
|
let ctx = ClientContext::new(cli, &args.client)?;
|
||||||
|
let response = ctx.get_with_query(
|
||||||
|
&format!("{API_PREFIX}/fs/file"),
|
||||||
|
&[
|
||||||
|
("path", Some(args.path.clone())),
|
||||||
|
("session_id", args.session_id.clone()),
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
print_binary_response(response)
|
||||||
|
}
|
||||||
|
FsCommand::Write(args) => {
|
||||||
|
let ctx = ClientContext::new(cli, &args.client)?;
|
||||||
|
let body = match (&args.content, &args.from_file) {
|
||||||
|
(Some(_), Some(_)) => {
|
||||||
|
return Err(CliError::Server(
|
||||||
|
"use --content or --from-file, not both".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
(None, None) => {
|
||||||
|
return Err(CliError::Server(
|
||||||
|
"write requires --content or --from-file".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
(Some(content), None) => content.clone().into_bytes(),
|
||||||
|
(None, Some(path)) => std::fs::read(path)?,
|
||||||
|
};
|
||||||
|
let response = ctx.put_raw_with_query(
|
||||||
|
&format!("{API_PREFIX}/fs/file"),
|
||||||
|
body,
|
||||||
|
"application/octet-stream",
|
||||||
|
&[
|
||||||
|
("path", Some(args.path.clone())),
|
||||||
|
("session_id", args.session_id.clone()),
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
print_json_response::<FsWriteResponse>(response)
|
||||||
|
}
|
||||||
|
FsCommand::Delete(args) => {
|
||||||
|
let ctx = ClientContext::new(cli, &args.client)?;
|
||||||
|
let response = ctx.delete_with_query(
|
||||||
|
&format!("{API_PREFIX}/fs/entry"),
|
||||||
|
&[
|
||||||
|
("path", Some(args.path.clone())),
|
||||||
|
("session_id", args.session_id.clone()),
|
||||||
|
(
|
||||||
|
"recursive",
|
||||||
|
if args.recursive {
|
||||||
|
Some("true".to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
print_json_response::<FsActionResponse>(response)
|
||||||
|
}
|
||||||
|
FsCommand::Mkdir(args) => {
|
||||||
|
let ctx = ClientContext::new(cli, &args.client)?;
|
||||||
|
let response = ctx.post_empty_with_query(
|
||||||
|
&format!("{API_PREFIX}/fs/mkdir"),
|
||||||
|
&[
|
||||||
|
("path", Some(args.path.clone())),
|
||||||
|
("session_id", args.session_id.clone()),
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
print_json_response::<FsActionResponse>(response)
|
||||||
|
}
|
||||||
|
FsCommand::Move(args) => {
|
||||||
|
let ctx = ClientContext::new(cli, &args.client)?;
|
||||||
|
let body = FsMoveRequest {
|
||||||
|
from: args.from.clone(),
|
||||||
|
to: args.to.clone(),
|
||||||
|
overwrite: if args.overwrite { Some(true) } else { None },
|
||||||
|
};
|
||||||
|
let response = ctx.post_with_query(
|
||||||
|
&format!("{API_PREFIX}/fs/move"),
|
||||||
|
&body,
|
||||||
|
&[("session_id", args.session_id.clone())],
|
||||||
|
)?;
|
||||||
|
print_json_response::<FsMoveResponse>(response)
|
||||||
|
}
|
||||||
|
FsCommand::Stat(args) => {
|
||||||
|
let ctx = ClientContext::new(cli, &args.client)?;
|
||||||
|
let response = ctx.get_with_query(
|
||||||
|
&format!("{API_PREFIX}/fs/stat"),
|
||||||
|
&[
|
||||||
|
("path", Some(args.path.clone())),
|
||||||
|
("session_id", args.session_id.clone()),
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
print_json_response::<FsStat>(response)
|
||||||
|
}
|
||||||
|
FsCommand::UploadBatch(args) => {
|
||||||
|
let ctx = ClientContext::new(cli, &args.client)?;
|
||||||
|
let file = File::open(&args.tar_path)?;
|
||||||
|
let response = ctx.post_raw_with_query(
|
||||||
|
&format!("{API_PREFIX}/fs/upload-batch"),
|
||||||
|
file,
|
||||||
|
"application/x-tar",
|
||||||
|
&[
|
||||||
|
("path", args.path.clone()),
|
||||||
|
("session_id", args.session_id.clone()),
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
print_json_response::<FsUploadBatchResponse>(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn create_opencode_session(
|
fn create_opencode_session(
|
||||||
base_url: &str,
|
base_url: &str,
|
||||||
token: Option<&str>,
|
token: Option<&str>,
|
||||||
|
|
@ -1275,9 +1560,75 @@ impl ClientContext {
|
||||||
Ok(request.send()?)
|
Ok(request.send()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn put_raw_with_query<B: Into<reqwest::blocking::Body>>(
|
||||||
|
&self,
|
||||||
|
path: &str,
|
||||||
|
body: B,
|
||||||
|
content_type: &str,
|
||||||
|
query: &[(&str, Option<String>)],
|
||||||
|
) -> Result<reqwest::blocking::Response, CliError> {
|
||||||
|
let mut request = self
|
||||||
|
.request(Method::PUT, path)
|
||||||
|
.header(reqwest::header::CONTENT_TYPE, content_type)
|
||||||
|
.header(reqwest::header::ACCEPT, "application/json");
|
||||||
|
for (key, value) in query {
|
||||||
|
if let Some(value) = value {
|
||||||
|
request = request.query(&[(key, value)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(request.body(body).send()?)
|
||||||
|
}
|
||||||
|
|
||||||
fn post_empty(&self, path: &str) -> Result<reqwest::blocking::Response, CliError> {
|
fn post_empty(&self, path: &str) -> Result<reqwest::blocking::Response, CliError> {
|
||||||
Ok(self.request(Method::POST, path).send()?)
|
Ok(self.request(Method::POST, path).send()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn post_empty_with_query(
|
||||||
|
&self,
|
||||||
|
path: &str,
|
||||||
|
query: &[(&str, Option<String>)],
|
||||||
|
) -> Result<reqwest::blocking::Response, CliError> {
|
||||||
|
let mut request = self.request(Method::POST, path);
|
||||||
|
for (key, value) in query {
|
||||||
|
if let Some(value) = value {
|
||||||
|
request = request.query(&[(key, value)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(request.send()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_with_query(
|
||||||
|
&self,
|
||||||
|
path: &str,
|
||||||
|
query: &[(&str, Option<String>)],
|
||||||
|
) -> Result<reqwest::blocking::Response, CliError> {
|
||||||
|
let mut request = self.request(Method::DELETE, path);
|
||||||
|
for (key, value) in query {
|
||||||
|
if let Some(value) = value {
|
||||||
|
request = request.query(&[(key, value)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(request.send()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn post_raw_with_query<B: Into<reqwest::blocking::Body>>(
|
||||||
|
&self,
|
||||||
|
path: &str,
|
||||||
|
body: B,
|
||||||
|
content_type: &str,
|
||||||
|
query: &[(&str, Option<String>)],
|
||||||
|
) -> Result<reqwest::blocking::Response, CliError> {
|
||||||
|
let mut request = self
|
||||||
|
.request(Method::POST, path)
|
||||||
|
.header(reqwest::header::CONTENT_TYPE, content_type)
|
||||||
|
.header(reqwest::header::ACCEPT, "application/json");
|
||||||
|
for (key, value) in query {
|
||||||
|
if let Some(value) = value {
|
||||||
|
request = request.query(&[(key, value)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(request.body(body).send()?)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_json_response<T: serde::de::DeserializeOwned + Serialize>(
|
fn print_json_response<T: serde::de::DeserializeOwned + Serialize>(
|
||||||
|
|
@ -1310,6 +1661,25 @@ fn print_text_response(response: reqwest::blocking::Response) -> Result<(), CliE
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn print_binary_response(response: reqwest::blocking::Response) -> Result<(), CliError> {
|
||||||
|
let status = response.status();
|
||||||
|
let bytes = response.bytes()?;
|
||||||
|
|
||||||
|
if !status.is_success() {
|
||||||
|
if let Ok(text) = std::str::from_utf8(&bytes) {
|
||||||
|
print_error_body(text)?;
|
||||||
|
} else {
|
||||||
|
write_stderr_line("Request failed with non-text response body")?;
|
||||||
|
}
|
||||||
|
return Err(CliError::HttpStatus(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out = std::io::stdout();
|
||||||
|
out.write_all(&bytes)?;
|
||||||
|
out.flush()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn print_empty_response(response: reqwest::blocking::Response) -> Result<(), CliError> {
|
fn print_empty_response(response: reqwest::blocking::Response) -> Result<(), CliError> {
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
if status.is_success() {
|
if status.is_success() {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
use sandbox_agent::cli::run_sandbox_agent;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
if let Err(err) = sandbox_agent::cli::run_sandbox_agent() {
|
if let Err(err) = run_sandbox_agent() {
|
||||||
tracing::error!(error = %err, "sandbox-agent failed");
|
tracing::error!(error = %err, "sandbox-agent failed");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -524,6 +524,8 @@ async fn ensure_backing_session(
|
||||||
agent_version: None,
|
agent_version: None,
|
||||||
directory,
|
directory,
|
||||||
title,
|
title,
|
||||||
|
mcp: None,
|
||||||
|
skills: None,
|
||||||
};
|
};
|
||||||
let manager = state.inner.session_manager();
|
let manager = state.inner.session_manager();
|
||||||
match manager
|
match manager
|
||||||
|
|
@ -4264,7 +4266,7 @@ async fn oc_session_message_create(
|
||||||
if let Err(err) = state
|
if let Err(err) = state
|
||||||
.inner
|
.inner
|
||||||
.session_manager()
|
.session_manager()
|
||||||
.send_message(session_id.clone(), prompt_text)
|
.send_message(session_id.clone(), prompt_text, Vec::new())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
let mut should_emit_idle = false;
|
let mut should_emit_idle = false;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -186,3 +186,130 @@ async fn agent_endpoints_snapshots() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn create_session_with_skill_sources() {
|
||||||
|
let app = TestApp::new();
|
||||||
|
|
||||||
|
// Create a temp skill directory with SKILL.md
|
||||||
|
let skill_dir = tempfile::tempdir().expect("create skill dir");
|
||||||
|
let skill_path = skill_dir.path().join("my-test-skill");
|
||||||
|
std::fs::create_dir_all(&skill_path).expect("create skill subdir");
|
||||||
|
std::fs::write(skill_path.join("SKILL.md"), "# Test Skill\nA test skill.").expect("write SKILL.md");
|
||||||
|
|
||||||
|
// Create session with local skill source
|
||||||
|
let (status, payload) = send_json(
|
||||||
|
&app.app,
|
||||||
|
Method::POST,
|
||||||
|
"/v1/sessions/skill-test-session",
|
||||||
|
Some(json!({
|
||||||
|
"agent": "mock",
|
||||||
|
"skills": {
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"source": skill_dir.path().to_string_lossy()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(status, StatusCode::OK, "create session with skills: {payload}");
|
||||||
|
assert!(
|
||||||
|
payload.get("healthy").and_then(Value::as_bool).unwrap_or(false),
|
||||||
|
"session should be healthy"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn create_session_with_skill_sources_filter() {
|
||||||
|
let app = TestApp::new();
|
||||||
|
|
||||||
|
// Create a temp directory with two skills
|
||||||
|
let skill_dir = tempfile::tempdir().expect("create skill dir");
|
||||||
|
let wanted = skill_dir.path().join("wanted-skill");
|
||||||
|
let unwanted = skill_dir.path().join("unwanted-skill");
|
||||||
|
std::fs::create_dir_all(&wanted).expect("create wanted dir");
|
||||||
|
std::fs::create_dir_all(&unwanted).expect("create unwanted dir");
|
||||||
|
std::fs::write(wanted.join("SKILL.md"), "# Wanted").expect("write wanted SKILL.md");
|
||||||
|
std::fs::write(unwanted.join("SKILL.md"), "# Unwanted").expect("write unwanted SKILL.md");
|
||||||
|
|
||||||
|
// Create session with filter
|
||||||
|
let (status, payload) = send_json(
|
||||||
|
&app.app,
|
||||||
|
Method::POST,
|
||||||
|
"/v1/sessions/skill-filter-session",
|
||||||
|
Some(json!({
|
||||||
|
"agent": "mock",
|
||||||
|
"skills": {
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"source": skill_dir.path().to_string_lossy(),
|
||||||
|
"skills": ["wanted-skill"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(status, StatusCode::OK, "create session with skill filter: {payload}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn create_session_with_invalid_skill_source() {
|
||||||
|
let app = TestApp::new();
|
||||||
|
|
||||||
|
// Use a non-existent path
|
||||||
|
let (status, _payload) = send_json(
|
||||||
|
&app.app,
|
||||||
|
Method::POST,
|
||||||
|
"/v1/sessions/skill-invalid-session",
|
||||||
|
Some(json!({
|
||||||
|
"agent": "mock",
|
||||||
|
"skills": {
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"source": "/nonexistent/path/to/skills"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
// Should fail with a 4xx or 5xx error
|
||||||
|
assert_ne!(status, StatusCode::OK, "session with invalid skill source should fail");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn create_session_with_skill_filter_no_match() {
|
||||||
|
let app = TestApp::new();
|
||||||
|
|
||||||
|
let skill_dir = tempfile::tempdir().expect("create skill dir");
|
||||||
|
let skill_path = skill_dir.path().join("alpha");
|
||||||
|
std::fs::create_dir_all(&skill_path).expect("create alpha dir");
|
||||||
|
std::fs::write(skill_path.join("SKILL.md"), "# Alpha").expect("write SKILL.md");
|
||||||
|
|
||||||
|
// Filter for a skill that doesn't exist
|
||||||
|
let (status, _payload) = send_json(
|
||||||
|
&app.app,
|
||||||
|
Method::POST,
|
||||||
|
"/v1/sessions/skill-nomatch-session",
|
||||||
|
Some(json!({
|
||||||
|
"agent": "mock",
|
||||||
|
"skills": {
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"source": skill_dir.path().to_string_lossy(),
|
||||||
|
"skills": ["nonexistent"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_ne!(status, StatusCode::OK, "session with no matching skills should fail");
|
||||||
|
}
|
||||||
|
|
|
||||||
267
server/packages/sandbox-agent/tests/http/fs_endpoints.rs
Normal file
267
server/packages/sandbox-agent/tests/http/fs_endpoints.rs
Normal file
|
|
@ -0,0 +1,267 @@
|
||||||
|
// Filesystem HTTP endpoints.
|
||||||
|
include!("../common/http.rs");
|
||||||
|
|
||||||
|
use std::fs as stdfs;
|
||||||
|
|
||||||
|
use tar::{Builder, Header};
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn fs_read_write_move_delete() {
|
||||||
|
let app = TestApp::new();
|
||||||
|
let cwd = std::env::current_dir().expect("cwd");
|
||||||
|
let temp = tempfile::tempdir_in(&cwd).expect("tempdir");
|
||||||
|
|
||||||
|
let dir_path = temp.path();
|
||||||
|
let file_path = dir_path.join("hello.txt");
|
||||||
|
let file_path_str = file_path.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
let request = Request::builder()
|
||||||
|
.method(Method::PUT)
|
||||||
|
.uri(format!("/v1/fs/file?path={file_path_str}"))
|
||||||
|
.header(header::CONTENT_TYPE, "application/octet-stream")
|
||||||
|
.body(Body::from("hello"))
|
||||||
|
.expect("write request");
|
||||||
|
let (status, _headers, _payload) = send_json_request(&app.app, request).await;
|
||||||
|
assert_eq!(status, StatusCode::OK, "write file");
|
||||||
|
|
||||||
|
let request = Request::builder()
|
||||||
|
.method(Method::GET)
|
||||||
|
.uri(format!("/v1/fs/file?path={file_path_str}"))
|
||||||
|
.body(Body::empty())
|
||||||
|
.expect("read request");
|
||||||
|
let (status, headers, bytes) = send_request(&app.app, request).await;
|
||||||
|
assert_eq!(status, StatusCode::OK, "read file");
|
||||||
|
assert_eq!(
|
||||||
|
headers
|
||||||
|
.get(header::CONTENT_TYPE)
|
||||||
|
.and_then(|value| value.to_str().ok()),
|
||||||
|
Some("application/octet-stream")
|
||||||
|
);
|
||||||
|
assert_eq!(bytes.as_ref(), b"hello");
|
||||||
|
|
||||||
|
let entries_path = dir_path.to_string_lossy().to_string();
|
||||||
|
let (status, entries) = send_json(
|
||||||
|
&app.app,
|
||||||
|
Method::GET,
|
||||||
|
&format!("/v1/fs/entries?path={entries_path}"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(status, StatusCode::OK, "list entries");
|
||||||
|
let entry_list = entries.as_array().cloned().unwrap_or_default();
|
||||||
|
let entry_names: Vec<String> = entry_list
|
||||||
|
.iter()
|
||||||
|
.filter_map(|entry| entry.get("name").and_then(|value| value.as_str()))
|
||||||
|
.map(|value| value.to_string())
|
||||||
|
.collect();
|
||||||
|
assert!(entry_names.contains(&"hello.txt".to_string()));
|
||||||
|
|
||||||
|
let new_path = dir_path.join("moved.txt");
|
||||||
|
let new_path_str = new_path.to_string_lossy().to_string();
|
||||||
|
let (status, _payload) = send_json(
|
||||||
|
&app.app,
|
||||||
|
Method::POST,
|
||||||
|
"/v1/fs/move",
|
||||||
|
Some(json!({
|
||||||
|
"from": file_path_str,
|
||||||
|
"to": new_path_str,
|
||||||
|
"overwrite": true
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(status, StatusCode::OK, "move file");
|
||||||
|
assert!(new_path.exists(), "moved file exists");
|
||||||
|
|
||||||
|
let (status, _payload) = send_json(
|
||||||
|
&app.app,
|
||||||
|
Method::DELETE,
|
||||||
|
&format!("/v1/fs/entry?path={}", new_path.to_string_lossy()),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(status, StatusCode::OK, "delete file");
|
||||||
|
assert!(!new_path.exists(), "file deleted");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn fs_upload_batch_tar() {
|
||||||
|
let app = TestApp::new();
|
||||||
|
let cwd = std::env::current_dir().expect("cwd");
|
||||||
|
let dest_dir = tempfile::tempdir_in(&cwd).expect("tempdir");
|
||||||
|
|
||||||
|
let mut builder = Builder::new(Vec::new());
|
||||||
|
let mut tar_header = Header::new_gnu();
|
||||||
|
let contents = b"hello";
|
||||||
|
tar_header.set_size(contents.len() as u64);
|
||||||
|
tar_header.set_cksum();
|
||||||
|
builder
|
||||||
|
.append_data(&mut tar_header, "a.txt", &contents[..])
|
||||||
|
.expect("append tar entry");
|
||||||
|
|
||||||
|
let mut tar_header = Header::new_gnu();
|
||||||
|
let contents = b"world";
|
||||||
|
tar_header.set_size(contents.len() as u64);
|
||||||
|
tar_header.set_cksum();
|
||||||
|
builder
|
||||||
|
.append_data(&mut tar_header, "nested/b.txt", &contents[..])
|
||||||
|
.expect("append tar entry");
|
||||||
|
|
||||||
|
let tar_bytes = builder.into_inner().expect("tar bytes");
|
||||||
|
|
||||||
|
let request = Request::builder()
|
||||||
|
.method(Method::POST)
|
||||||
|
.uri(format!(
|
||||||
|
"/v1/fs/upload-batch?path={}",
|
||||||
|
dest_dir.path().to_string_lossy()
|
||||||
|
))
|
||||||
|
.header(header::CONTENT_TYPE, "application/x-tar")
|
||||||
|
.body(Body::from(tar_bytes))
|
||||||
|
.expect("tar request");
|
||||||
|
|
||||||
|
let (status, _headers, payload) = send_json_request(&app.app, request).await;
|
||||||
|
assert_eq!(status, StatusCode::OK, "upload batch");
|
||||||
|
assert!(payload
|
||||||
|
.get("paths")
|
||||||
|
.and_then(|value| value.as_array())
|
||||||
|
.map(|value| !value.is_empty())
|
||||||
|
.unwrap_or(false));
|
||||||
|
assert!(payload.get("truncated").and_then(|value| value.as_bool()) == Some(false));
|
||||||
|
|
||||||
|
let a_path = dest_dir.path().join("a.txt");
|
||||||
|
let b_path = dest_dir.path().join("nested").join("b.txt");
|
||||||
|
assert!(a_path.exists(), "a.txt extracted");
|
||||||
|
assert!(b_path.exists(), "b.txt extracted");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn fs_relative_paths_use_session_dir() {
|
||||||
|
let app = TestApp::new();
|
||||||
|
|
||||||
|
let session_id = "fs-session";
|
||||||
|
let status = send_status(
|
||||||
|
&app.app,
|
||||||
|
Method::POST,
|
||||||
|
&format!("/v1/sessions/{session_id}"),
|
||||||
|
Some(json!({ "agent": "mock" })),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(status, StatusCode::OK, "create session");
|
||||||
|
|
||||||
|
let cwd = std::env::current_dir().expect("cwd");
|
||||||
|
let temp = tempfile::tempdir_in(&cwd).expect("tempdir");
|
||||||
|
let relative_dir = temp
|
||||||
|
.path()
|
||||||
|
.strip_prefix(&cwd)
|
||||||
|
.expect("strip prefix")
|
||||||
|
.to_path_buf();
|
||||||
|
let relative_path = relative_dir.join("session.txt");
|
||||||
|
|
||||||
|
let request = Request::builder()
|
||||||
|
.method(Method::PUT)
|
||||||
|
.uri(format!(
|
||||||
|
"/v1/fs/file?session_id={session_id}&path={}",
|
||||||
|
relative_path.to_string_lossy()
|
||||||
|
))
|
||||||
|
.header(header::CONTENT_TYPE, "application/octet-stream")
|
||||||
|
.body(Body::from("session"))
|
||||||
|
.expect("write request");
|
||||||
|
let (status, _headers, _payload) = send_json_request(&app.app, request).await;
|
||||||
|
assert_eq!(status, StatusCode::OK, "write relative file");
|
||||||
|
|
||||||
|
let absolute_path = cwd.join(relative_path);
|
||||||
|
let content = stdfs::read_to_string(&absolute_path).expect("read file");
|
||||||
|
assert_eq!(content, "session");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn fs_upload_batch_truncates_paths() {
|
||||||
|
let app = TestApp::new();
|
||||||
|
let cwd = std::env::current_dir().expect("cwd");
|
||||||
|
let dest_dir = tempfile::tempdir_in(&cwd).expect("tempdir");
|
||||||
|
|
||||||
|
let mut builder = Builder::new(Vec::new());
|
||||||
|
for index in 0..1030 {
|
||||||
|
let mut tar_header = Header::new_gnu();
|
||||||
|
tar_header.set_size(0);
|
||||||
|
tar_header.set_cksum();
|
||||||
|
let name = format!("file_{index}.txt");
|
||||||
|
builder
|
||||||
|
.append_data(&mut tar_header, name, &[][..])
|
||||||
|
.expect("append tar entry");
|
||||||
|
}
|
||||||
|
let tar_bytes = builder.into_inner().expect("tar bytes");
|
||||||
|
|
||||||
|
let request = Request::builder()
|
||||||
|
.method(Method::POST)
|
||||||
|
.uri(format!(
|
||||||
|
"/v1/fs/upload-batch?path={}",
|
||||||
|
dest_dir.path().to_string_lossy()
|
||||||
|
))
|
||||||
|
.header(header::CONTENT_TYPE, "application/x-tar")
|
||||||
|
.body(Body::from(tar_bytes))
|
||||||
|
.expect("tar request");
|
||||||
|
|
||||||
|
let (status, _headers, payload) = send_json_request(&app.app, request).await;
|
||||||
|
assert_eq!(status, StatusCode::OK, "upload batch");
|
||||||
|
let paths = payload
|
||||||
|
.get("paths")
|
||||||
|
.and_then(|value| value.as_array())
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
assert_eq!(paths.len(), 1024);
|
||||||
|
assert_eq!(payload.get("truncated").and_then(|value| value.as_bool()), Some(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn fs_mkdir_stat_and_delete_directory() {
|
||||||
|
let app = TestApp::new();
|
||||||
|
let cwd = std::env::current_dir().expect("cwd");
|
||||||
|
let temp = tempfile::tempdir_in(&cwd).expect("tempdir");
|
||||||
|
|
||||||
|
let dir_path = temp.path().join("nested");
|
||||||
|
let dir_path_str = dir_path.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
let status = send_status(
|
||||||
|
&app.app,
|
||||||
|
Method::POST,
|
||||||
|
&format!("/v1/fs/mkdir?path={dir_path_str}"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(status, StatusCode::OK, "mkdir");
|
||||||
|
assert!(dir_path.exists(), "directory created");
|
||||||
|
|
||||||
|
let (status, stat) = send_json(
|
||||||
|
&app.app,
|
||||||
|
Method::GET,
|
||||||
|
&format!("/v1/fs/stat?path={dir_path_str}"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(status, StatusCode::OK, "stat directory");
|
||||||
|
assert_eq!(stat["entryType"], "directory");
|
||||||
|
|
||||||
|
let file_path = dir_path.join("note.txt");
|
||||||
|
stdfs::write(&file_path, "content").expect("write file");
|
||||||
|
let file_path_str = file_path.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
let (status, stat) = send_json(
|
||||||
|
&app.app,
|
||||||
|
Method::GET,
|
||||||
|
&format!("/v1/fs/stat?path={file_path_str}"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(status, StatusCode::OK, "stat file");
|
||||||
|
assert_eq!(stat["entryType"], "file");
|
||||||
|
|
||||||
|
let status = send_status(
|
||||||
|
&app.app,
|
||||||
|
Method::DELETE,
|
||||||
|
&format!("/v1/fs/entry?path={dir_path_str}&recursive=true"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(status, StatusCode::OK, "delete directory");
|
||||||
|
assert!(!dir_path.exists(), "directory deleted");
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http/agent_endpoints.rs
|
||||||
|
assertion_line: 145
|
||||||
|
expression: snapshot_status(status)
|
||||||
|
---
|
||||||
|
status: 204
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http/agent_endpoints.rs
|
||||||
|
expression: snapshot_status(status)
|
||||||
|
---
|
||||||
|
status: 204
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http/agent_endpoints.rs
|
||||||
|
assertion_line: 185
|
||||||
|
expression: "normalize_agent_models(&models, config.agent)"
|
||||||
|
---
|
||||||
|
defaultInList: true
|
||||||
|
defaultModel: amp-default
|
||||||
|
hasDefault: true
|
||||||
|
hasVariants: false
|
||||||
|
ids:
|
||||||
|
- amp-default
|
||||||
|
modelCount: 1
|
||||||
|
nonEmpty: true
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http/agent_endpoints.rs
|
||||||
|
assertion_line: 185
|
||||||
|
expression: "normalize_agent_models(&models, config.agent)"
|
||||||
|
---
|
||||||
|
defaultInList: true
|
||||||
|
hasDefault: true
|
||||||
|
hasVariants: "<redacted>"
|
||||||
|
nonEmpty: true
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http/agent_endpoints.rs
|
||||||
|
assertion_line: 185
|
||||||
|
expression: "normalize_agent_models(&models, config.agent)"
|
||||||
|
---
|
||||||
|
defaultInList: true
|
||||||
|
hasDefault: true
|
||||||
|
hasVariants: false
|
||||||
|
nonEmpty: true
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http/agent_endpoints.rs
|
||||||
|
expression: "normalize_agent_models(&models, config.agent)"
|
||||||
|
---
|
||||||
|
defaultInList: true
|
||||||
|
hasDefault: true
|
||||||
|
hasVariants: "<redacted>"
|
||||||
|
nonEmpty: true
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http/agent_endpoints.rs
|
||||||
|
assertion_line: 162
|
||||||
|
expression: normalize_agent_modes(&modes)
|
||||||
|
---
|
||||||
|
modes:
|
||||||
|
- description: true
|
||||||
|
id: build
|
||||||
|
name: Build
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http/agent_endpoints.rs
|
||||||
|
assertion_line: 162
|
||||||
|
expression: normalize_agent_modes(&modes)
|
||||||
|
---
|
||||||
|
modes:
|
||||||
|
- description: true
|
||||||
|
id: build
|
||||||
|
name: Build
|
||||||
|
- description: true
|
||||||
|
id: plan
|
||||||
|
name: Plan
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http/agent_endpoints.rs
|
||||||
|
expression: normalize_agent_modes(&modes)
|
||||||
|
---
|
||||||
|
modes:
|
||||||
|
- description: true
|
||||||
|
id: build
|
||||||
|
name: Build
|
||||||
|
- description: true
|
||||||
|
id: custom
|
||||||
|
name: Custom
|
||||||
|
- description: true
|
||||||
|
id: plan
|
||||||
|
name: Plan
|
||||||
|
|
@ -1,2 +1,4 @@
|
||||||
#[path = "http/agent_endpoints.rs"]
|
#[path = "http/agent_endpoints.rs"]
|
||||||
mod agent_endpoints;
|
mod agent_endpoints;
|
||||||
|
#[path = "http/fs_endpoints.rs"]
|
||||||
|
mod fs_endpoints;
|
||||||
|
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
---
|
|
||||||
source: server/packages/sandbox-agent/tests/sessions/multi_turn.rs
|
|
||||||
assertion_line: 15
|
|
||||||
expression: value
|
|
||||||
---
|
|
||||||
first:
|
|
||||||
- metadata: true
|
|
||||||
seq: 1
|
|
||||||
session: started
|
|
||||||
type: session.started
|
|
||||||
- seq: 2
|
|
||||||
type: turn.started
|
|
||||||
- item:
|
|
||||||
content_types:
|
|
||||||
- text
|
|
||||||
kind: message
|
|
||||||
role: user
|
|
||||||
status: in_progress
|
|
||||||
seq: 3
|
|
||||||
type: item.started
|
|
||||||
- item:
|
|
||||||
content_types:
|
|
||||||
- text
|
|
||||||
kind: message
|
|
||||||
role: user
|
|
||||||
status: completed
|
|
||||||
seq: 4
|
|
||||||
type: item.completed
|
|
||||||
- item:
|
|
||||||
content_types:
|
|
||||||
- text
|
|
||||||
kind: message
|
|
||||||
role: assistant
|
|
||||||
status: in_progress
|
|
||||||
seq: 5
|
|
||||||
type: item.started
|
|
||||||
- delta:
|
|
||||||
delta: "<redacted>"
|
|
||||||
item_id: "<redacted>"
|
|
||||||
native_item_id: "<redacted>"
|
|
||||||
seq: 6
|
|
||||||
type: item.delta
|
|
||||||
- delta:
|
|
||||||
delta: "<redacted>"
|
|
||||||
item_id: "<redacted>"
|
|
||||||
native_item_id: "<redacted>"
|
|
||||||
seq: 7
|
|
||||||
type: item.delta
|
|
||||||
- delta:
|
|
||||||
delta: "<redacted>"
|
|
||||||
item_id: "<redacted>"
|
|
||||||
native_item_id: "<redacted>"
|
|
||||||
seq: 8
|
|
||||||
type: item.delta
|
|
||||||
- delta:
|
|
||||||
delta: "<redacted>"
|
|
||||||
item_id: "<redacted>"
|
|
||||||
native_item_id: "<redacted>"
|
|
||||||
seq: 9
|
|
||||||
type: item.delta
|
|
||||||
- delta:
|
|
||||||
delta: "<redacted>"
|
|
||||||
item_id: "<redacted>"
|
|
||||||
native_item_id: "<redacted>"
|
|
||||||
seq: 10
|
|
||||||
type: item.delta
|
|
||||||
second:
|
|
||||||
- seq: 1
|
|
||||||
type: turn.started
|
|
||||||
- item:
|
|
||||||
content_types:
|
|
||||||
- text
|
|
||||||
kind: message
|
|
||||||
role: assistant
|
|
||||||
status: completed
|
|
||||||
seq: 2
|
|
||||||
type: item.completed
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
---
|
|
||||||
source: server/packages/sandbox-agent/tests/sessions/permissions.rs
|
|
||||||
assertion_line: 12
|
|
||||||
expression: value
|
|
||||||
---
|
|
||||||
- metadata: true
|
|
||||||
seq: 1
|
|
||||||
session: started
|
|
||||||
type: session.started
|
|
||||||
- seq: 2
|
|
||||||
type: turn.started
|
|
||||||
- item:
|
|
||||||
content_types:
|
|
||||||
- text
|
|
||||||
kind: message
|
|
||||||
role: user
|
|
||||||
status: in_progress
|
|
||||||
seq: 3
|
|
||||||
type: item.started
|
|
||||||
- item:
|
|
||||||
content_types:
|
|
||||||
- text
|
|
||||||
kind: message
|
|
||||||
role: user
|
|
||||||
status: completed
|
|
||||||
seq: 4
|
|
||||||
type: item.completed
|
|
||||||
- item:
|
|
||||||
content_types:
|
|
||||||
- text
|
|
||||||
kind: message
|
|
||||||
role: assistant
|
|
||||||
status: in_progress
|
|
||||||
seq: 5
|
|
||||||
type: item.started
|
|
||||||
- delta:
|
|
||||||
delta: "<redacted>"
|
|
||||||
item_id: "<redacted>"
|
|
||||||
native_item_id: "<redacted>"
|
|
||||||
seq: 6
|
|
||||||
type: item.delta
|
|
||||||
- delta:
|
|
||||||
delta: "<redacted>"
|
|
||||||
item_id: "<redacted>"
|
|
||||||
native_item_id: "<redacted>"
|
|
||||||
seq: 7
|
|
||||||
type: item.delta
|
|
||||||
- delta:
|
|
||||||
delta: "<redacted>"
|
|
||||||
item_id: "<redacted>"
|
|
||||||
native_item_id: "<redacted>"
|
|
||||||
seq: 8
|
|
||||||
type: item.delta
|
|
||||||
- delta:
|
|
||||||
delta: "<redacted>"
|
|
||||||
item_id: "<redacted>"
|
|
||||||
native_item_id: "<redacted>"
|
|
||||||
seq: 9
|
|
||||||
type: item.delta
|
|
||||||
- delta:
|
|
||||||
delta: "<redacted>"
|
|
||||||
item_id: "<redacted>"
|
|
||||||
native_item_id: "<redacted>"
|
|
||||||
seq: 10
|
|
||||||
type: item.delta
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
---
|
|
||||||
source: server/packages/sandbox-agent/tests/sessions/questions.rs
|
|
||||||
assertion_line: 12
|
|
||||||
expression: value
|
|
||||||
---
|
|
||||||
- metadata: true
|
|
||||||
seq: 1
|
|
||||||
session: started
|
|
||||||
type: session.started
|
|
||||||
- seq: 2
|
|
||||||
type: turn.started
|
|
||||||
- item:
|
|
||||||
content_types:
|
|
||||||
- text
|
|
||||||
kind: message
|
|
||||||
role: user
|
|
||||||
status: in_progress
|
|
||||||
seq: 3
|
|
||||||
type: item.started
|
|
||||||
- item:
|
|
||||||
content_types:
|
|
||||||
- text
|
|
||||||
kind: message
|
|
||||||
role: user
|
|
||||||
status: completed
|
|
||||||
seq: 4
|
|
||||||
type: item.completed
|
|
||||||
- item:
|
|
||||||
content_types:
|
|
||||||
- text
|
|
||||||
kind: message
|
|
||||||
role: assistant
|
|
||||||
status: in_progress
|
|
||||||
seq: 5
|
|
||||||
type: item.started
|
|
||||||
- delta:
|
|
||||||
delta: "<redacted>"
|
|
||||||
item_id: "<redacted>"
|
|
||||||
native_item_id: "<redacted>"
|
|
||||||
seq: 6
|
|
||||||
type: item.delta
|
|
||||||
- item:
|
|
||||||
content_types:
|
|
||||||
- text
|
|
||||||
kind: message
|
|
||||||
role: assistant
|
|
||||||
status: completed
|
|
||||||
seq: 7
|
|
||||||
type: item.completed
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
---
|
|
||||||
source: server/packages/sandbox-agent/tests/sessions/session_lifecycle.rs
|
|
||||||
assertion_line: 12
|
|
||||||
expression: value
|
|
||||||
---
|
|
||||||
session_a:
|
|
||||||
- metadata: true
|
|
||||||
seq: 1
|
|
||||||
session: started
|
|
||||||
type: session.started
|
|
||||||
- seq: 2
|
|
||||||
type: turn.started
|
|
||||||
- item:
|
|
||||||
content_types:
|
|
||||||
- text
|
|
||||||
kind: message
|
|
||||||
role: user
|
|
||||||
status: in_progress
|
|
||||||
seq: 3
|
|
||||||
type: item.started
|
|
||||||
- item:
|
|
||||||
content_types:
|
|
||||||
- text
|
|
||||||
kind: message
|
|
||||||
role: user
|
|
||||||
status: completed
|
|
||||||
seq: 4
|
|
||||||
type: item.completed
|
|
||||||
- item:
|
|
||||||
content_types:
|
|
||||||
- text
|
|
||||||
kind: message
|
|
||||||
role: assistant
|
|
||||||
status: in_progress
|
|
||||||
seq: 5
|
|
||||||
type: item.started
|
|
||||||
- delta:
|
|
||||||
delta: "<redacted>"
|
|
||||||
item_id: "<redacted>"
|
|
||||||
native_item_id: "<redacted>"
|
|
||||||
seq: 6
|
|
||||||
type: item.delta
|
|
||||||
session_b:
|
|
||||||
- metadata: true
|
|
||||||
seq: 1
|
|
||||||
session: started
|
|
||||||
type: session.started
|
|
||||||
- seq: 2
|
|
||||||
type: turn.started
|
|
||||||
- item:
|
|
||||||
content_types:
|
|
||||||
- text
|
|
||||||
kind: message
|
|
||||||
role: user
|
|
||||||
status: in_progress
|
|
||||||
seq: 3
|
|
||||||
type: item.started
|
|
||||||
- item:
|
|
||||||
content_types:
|
|
||||||
- text
|
|
||||||
kind: message
|
|
||||||
role: user
|
|
||||||
status: completed
|
|
||||||
seq: 4
|
|
||||||
type: item.completed
|
|
||||||
- item:
|
|
||||||
content_types:
|
|
||||||
- text
|
|
||||||
kind: message
|
|
||||||
role: assistant
|
|
||||||
status: in_progress
|
|
||||||
seq: 5
|
|
||||||
type: item.started
|
|
||||||
- delta:
|
|
||||||
delta: "<redacted>"
|
|
||||||
item_id: "<redacted>"
|
|
||||||
native_item_id: "<redacted>"
|
|
||||||
seq: 6
|
|
||||||
type: item.delta
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
---
|
---
|
||||||
source: server/packages/sandbox-agent/tests/sessions/session_lifecycle.rs
|
source: server/packages/sandbox-agent/tests/sessions/session_lifecycle.rs
|
||||||
assertion_line: 12
|
|
||||||
expression: value
|
expression: value
|
||||||
---
|
---
|
||||||
healthy: true
|
healthy: true
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/sessions/session_lifecycle.rs
|
||||||
|
expression: value
|
||||||
|
---
|
||||||
|
hasExpectedFields: true
|
||||||
|
sessionCount: 1
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
---
|
|
||||||
source: server/packages/sandbox-agent/tests/sessions/../common/http.rs
|
|
||||||
assertion_line: 1001
|
|
||||||
expression: normalized
|
|
||||||
---
|
|
||||||
- metadata: true
|
|
||||||
seq: 1
|
|
||||||
session: started
|
|
||||||
type: session.started
|
|
||||||
- seq: 2
|
|
||||||
type: turn.started
|
|
||||||
- item:
|
|
||||||
content_types:
|
|
||||||
- text
|
|
||||||
kind: message
|
|
||||||
role: user
|
|
||||||
status: in_progress
|
|
||||||
seq: 3
|
|
||||||
type: item.started
|
|
||||||
- item:
|
|
||||||
content_types:
|
|
||||||
- text
|
|
||||||
kind: message
|
|
||||||
role: user
|
|
||||||
status: completed
|
|
||||||
seq: 4
|
|
||||||
type: item.completed
|
|
||||||
- item:
|
|
||||||
content_types:
|
|
||||||
- text
|
|
||||||
kind: message
|
|
||||||
role: assistant
|
|
||||||
status: in_progress
|
|
||||||
seq: 5
|
|
||||||
type: item.started
|
|
||||||
- delta:
|
|
||||||
delta: "<redacted>"
|
|
||||||
item_id: "<redacted>"
|
|
||||||
native_item_id: "<redacted>"
|
|
||||||
seq: 6
|
|
||||||
type: item.delta
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue