The ACP spec defines session/update content as a ContentBlock object
({type, text}) but we decoded it as a string. This dropped every
agent_message_chunk and left prompt responses empty. E2E against the
real opencode acp binary surfaced the bug.
- internal/acp/messages.go: add ContentBlock struct; Update.Content is
now ContentBlock.
- internal/acp/client.go: handleNotification extracts Text when
Content.Type == "text"; other types are logged at debug and dropped.
- internal/acp/client_test.go: NDJSON framing test sends content as a
ContentBlock object and reads .Text.
- README.md: document /acp/* routes + CODESPACE_OPENCODE_ARGS env.
Verified end-to-end: POST /acp/prompt returns text="Hi there!", 4
messages in history (1 user + 3 agent chunks), 0 invalid session/update
log entries.
Conversation: 019f357d-4236-7010-949c-e61067618d42
206 lines
7.4 KiB
Markdown
206 lines
7.4 KiB
Markdown
# codespace
|
|
|
|
A Go backend skeleton for managing local workspaces and file operations with process orchestration.
|
|
|
|
## V1 scope
|
|
|
|
- Workspace creation, listing, lookup, and deletion backed by local directories.
|
|
- File read/write/list/mkdir/remove/rename/stat through a `FileSystem` interface with path-escape protection.
|
|
- Workspace root deletion protection — cannot remove the workspace root directory through the file API.
|
|
- Configurable per-file write size limit — oversized writes are rejected early before hitting disk.
|
|
- OpenCode process start/stop/restart/status per workspace (launched with `opencode acp` by default; raw stdio is still piped through the process WebSocket for backward compatibility).
|
|
- **ACP (Agent Client Protocol) client** — minimal JSON-RPC over NDJSON against the running opencode process. Exposes `initialize` / `session/new` / `session/prompt` / `session/cancel`. Serves `fs/read_text_file` and `fs/write_text_file` from the workspace filesystem. The web UI surfaces this as a chat panel alongside the terminal.
|
|
- HTTP API using [Gin](https://github.com/gin-gonic/gin) for routing with JSON responses.
|
|
- Explicit `http.Server` composition with configurable timeouts and max header bytes.
|
|
- Structured logging using Go standard library `log/slog` with JSON/text format and level control.
|
|
|
|
### Safety
|
|
|
|
- File paths are workspace-relative; absolute paths are rejected.
|
|
- `..` escape attempts are rejected before filesystem operations.
|
|
- Deleting the workspace root through the file API is explicitly blocked.
|
|
- File content is never logged; structured logging only includes metadata.
|
|
- Workspace root paths are not exposed in API responses or logs.
|
|
|
|
## Run
|
|
|
|
```sh
|
|
make run
|
|
```
|
|
|
|
The server listens on `:8080` by default.
|
|
|
|
## Configuration
|
|
|
|
`configs/config.yaml`:
|
|
|
|
```yaml
|
|
server:
|
|
addr: ":8080"
|
|
readTimeout: "15s"
|
|
writeTimeout: "15s"
|
|
idleTimeout: "60s"
|
|
maxHeaderBytes: 1048576
|
|
workspace:
|
|
root: "./workspaces"
|
|
process:
|
|
opencodeCommand: "opencode"
|
|
args:
|
|
- "acp"
|
|
|
|
shell:
|
|
command: "bash"
|
|
args:
|
|
- "-i"
|
|
|
|
file:
|
|
maxWriteBytes: 1048576
|
|
gin:
|
|
mode: "release"
|
|
log:
|
|
level: "info"
|
|
format: "json"
|
|
```
|
|
|
|
### Environment overrides
|
|
|
|
| Variable | Description | Default |
|
|
|---|---|---|
|
|
| `CODESPACE_ADDR` | Listen address | `:8080` |
|
|
| `CODESPACE_WORKSPACE_ROOT` | Workspace storage root | `./workspaces` |
|
|
| `CODESPACE_OPENCODE_COMMAND` | OpenCode binary path | `opencode` |
|
|
| `CODESPACE_OPENCODE_ARGS` | Comma-separated extra args passed to OpenCode (sets `process.args`) | `acp` |
|
|
| `CODESPACE_SHELL_COMMAND` | Shell binary path | `bash` |
|
|
| `CODESPACE_SHELL_ARGS` | Comma-separated shell arguments | `-i` |
|
|
| `CODESPACE_FILE_MAX_WRITE_BYTES` | Max bytes per file write | `1048576` |
|
|
| `CODESPACE_GIN_MODE` | Gin mode (`debug`, `release`, `test`) | `release` |
|
|
| `CODESPACE_READ_TIMEOUT` | HTTP read timeout | `15s` |
|
|
| `CODESPACE_WRITE_TIMEOUT` | HTTP write timeout | `15s` |
|
|
| `CODESPACE_IDLE_TIMEOUT` | HTTP idle timeout | `60s` |
|
|
| `CODESPACE_MAX_HEADER_BYTES` | Max HTTP header bytes | `1048576` |
|
|
| `CODESPACE_LOG_LEVEL` | Log level (`debug`, `info`, `warn`, `error`) | `info` |
|
|
| `CODESPACE_LOG_FORMAT` | Log format (`json`, `text`) | `json` |
|
|
|
|
## Logging
|
|
|
|
codespace uses the Go standard library `log/slog` for application logs.
|
|
All runtime logging uses slog; `fmt`, `log.Printf`, and the Gin default logger are not used.
|
|
|
|
## API
|
|
|
|
### Workspaces
|
|
|
|
| Method | Path | Description |
|
|
|---|---|---|
|
|
| `GET` | `/api/workspaces` | List all workspaces |
|
|
| `POST` | `/api/workspaces` | Create a workspace (`{"id": "name"}`) |
|
|
| `GET` | `/api/workspaces/:id` | Get workspace by ID |
|
|
| `DELETE` | `/api/workspaces/:id` | Delete workspace (stops running process first) |
|
|
|
|
### Files
|
|
|
|
| Method | Path | Description |
|
|
|---|---|---|
|
|
| `GET` | `/api/workspaces/:id/files?path=...` | List directory entries |
|
|
| `GET` | `/api/workspaces/:id/files/read?path=...` | Read file content |
|
|
| `GET` | `/api/workspaces/:id/files/stat?path=...` | Get file metadata (name, path, isDir, size, modTime) |
|
|
| `PUT` | `/api/workspaces/:id/files/write` | Write file (`{"path": "...", "content": "..."}`) |
|
|
| `POST` | `/api/workspaces/:id/files/mkdir` | Create directory (`{"path": "..."}`) |
|
|
| `DELETE` | `/api/workspaces/:id/files?path=...` | Remove file or directory recursively |
|
|
| `POST` | `/api/workspaces/:id/files/rename` | Rename file or directory (`{"oldPath": "...", "newPath": "..."}`) |
|
|
|
|
### Process
|
|
|
|
| Method | Path | Description |
|
|
|---|---|---|
|
|
| `POST` | `/api/workspaces/:id/process/start` | Start OpenCode process for workspace |
|
|
| `POST` | `/api/workspaces/:id/process/stop` | Stop OpenCode process |
|
|
| `POST` | `/api/workspaces/:id/process/restart` | Restart OpenCode process |
|
|
| `GET` | `/api/workspaces/:id/process/status` | Get process running status and PID |
|
|
| `GET` | `/api/workspaces/:id/process/ws` | WebSocket: stream process stdout/stderr, send stdin (text frames, raw bytes) |
|
|
|
|
### Shell
|
|
|
|
| Method | Path | Description |
|
|
|---|---|---|
|
|
| `POST` | `/api/workspaces/:id/shell/start` | Start interactive shell for workspace |
|
|
| `POST` | `/api/workspaces/:id/shell/stop` | Stop interactive shell |
|
|
| `POST` | `/api/workspaces/:id/shell/restart` | Restart interactive shell |
|
|
| `GET` | `/api/workspaces/:id/shell/status` | Get shell running status and PID |
|
|
| `GET` | `/api/workspaces/:id/shell/ws` | WebSocket: stream shell stdout/stderr, send stdin (text frames, raw bytes) |
|
|
|
|
### ACP (Agent Client Protocol)
|
|
|
|
Sits on top of the OpenCode process started above. The ACP service starts the process on the first prompt if it isn't already running, performs the `initialize` + `session/new` handshake, and accumulates the agent's responses until the `session/prompt` reply arrives.
|
|
|
|
| Method | Path | Description |
|
|
|---|---|---|
|
|
| `GET` | `/api/workspaces/:id/acp/status` | Observational status: `{workspaceId, ready, sessionId, running, pid, error}`. Does **not** start the process. |
|
|
| `GET` | `/api/workspaces/:id/acp/history` | Accumulated conversation: `{sessionId, messages: [{role, text, time}]}`, oldest first. In-memory only — cleared when the process restarts. |
|
|
| `POST` | `/api/workspaces/:id/acp/prompt` | Body `{"content": "..."}`. Response `{"sessionId, stopReason, text}`. Starts the process + session on demand. Synchronous (waits up to 5 min). |
|
|
| `POST` | `/api/workspaces/:id/acp/cancel` | Best-effort interrupt of an in-flight prompt. |
|
|
|
|
`terminal/*` and other unsupported agent-initiated requests return JSON-RPC `Method not found` (`-32601`).
|
|
|
|
### Health
|
|
|
|
| Method | Path | Description |
|
|
|---|---|---|
|
|
| `GET` | `/healthz` | Server health check |
|
|
|
|
## Examples
|
|
|
|
### Health check
|
|
|
|
```sh
|
|
curl -s http://localhost:8080/healthz
|
|
```
|
|
|
|
### Create a workspace
|
|
|
|
```sh
|
|
curl -s -X POST http://localhost:8080/api/workspaces \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"id":"user1"}'
|
|
```
|
|
|
|
### List workspaces
|
|
|
|
```sh
|
|
curl -s http://localhost:8080/api/workspaces
|
|
```
|
|
|
|
### Write a file
|
|
|
|
```sh
|
|
curl -s -X PUT http://localhost:8080/api/workspaces/user1/files/write \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"path":"main.go","content":"package main\n"}'
|
|
```
|
|
|
|
### Read a file
|
|
|
|
```sh
|
|
curl -s 'http://localhost:8080/api/workspaces/user1/files/read?path=main.go'
|
|
```
|
|
|
|
### Get file stat
|
|
|
|
```sh
|
|
curl -s 'http://localhost:8080/api/workspaces/user1/files/stat?path=main.go'
|
|
```
|
|
|
|
### Send a prompt to the agent
|
|
|
|
```sh
|
|
curl -s -X POST http://localhost:8080/api/workspaces/user1/acp/prompt \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"content":"list the files in this workspace"}'
|
|
```
|
|
|
|
## Test
|
|
|
|
```sh
|
|
make test
|
|
```
|