# 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 ```