feat: harden workspace file APIs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c1b8982e3b
commit
04b309d3fc
@@ -1,18 +1,25 @@
|
||||
# codespace
|
||||
|
||||
A runnable Go backend MVP skeleton that manages local workspaces, file operations through an abstract filesystem, and OpenCode processes.
|
||||
A Go backend skeleton for managing local workspaces and file operations with process orchestration.
|
||||
|
||||
## V1 scope
|
||||
|
||||
- Workspace creation, lookup, and deletion backed by local directories.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
### Not implemented in v1
|
||||
### Safety
|
||||
|
||||
Docker, authentication, Git integration, LSP, AI Agent orchestration, web frontend, and full watcher/WebSocket event streaming are out of scope for the first version.
|
||||
- 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
|
||||
|
||||
@@ -20,25 +27,7 @@ Docker, authentication, Git integration, LSP, AI Agent orchestration, web fronte
|
||||
make run
|
||||
```
|
||||
|
||||
The server listens on `:8080` by default. Override via `configs/config.yaml` or environment variables:
|
||||
|
||||
| Variable | Description |
|
||||
|---|---|
|
||||
| `CODESPACE_ADDR` | Listen address |
|
||||
| `CODESPACE_WORKSPACE_ROOT` | Workspace root directory |
|
||||
| `CODESPACE_OPENCODE_COMMAND` | OpenCode binary path |
|
||||
| `CODESPACE_READ_TIMEOUT` | HTTP read timeout (e.g. `15s`) |
|
||||
| `CODESPACE_WRITE_TIMEOUT` | HTTP write timeout (e.g. `15s`) |
|
||||
| `CODESPACE_IDLE_TIMEOUT` | HTTP idle timeout (e.g. `60s`) |
|
||||
| `CODESPACE_MAX_HEADER_BYTES` | Max header bytes (e.g. `1048576`) |
|
||||
| `CODESPACE_LOG_LEVEL` | Log level (`debug`, `info`, `warn`, `error`) |
|
||||
| `CODESPACE_LOG_FORMAT` | Log format (`json`, `text`) |
|
||||
|
||||
## Test
|
||||
|
||||
```sh
|
||||
make test
|
||||
```
|
||||
The server listens on `:8080` by default.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -55,39 +44,80 @@ workspace:
|
||||
root: "./workspaces"
|
||||
process:
|
||||
opencodeCommand: "opencode"
|
||||
file:
|
||||
maxWriteBytes: 1048576
|
||||
log:
|
||||
level: "info"
|
||||
format: "json"
|
||||
```
|
||||
|
||||
The server is started through an explicit `http.Server{Handler: router}` — it does not use `gin.Engine.Run()`.
|
||||
### 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_FILE_MAX_WRITE_BYTES` | Max bytes per file write | `1048576` |
|
||||
| `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. Default output is JSON to stdout.
|
||||
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.
|
||||
|
||||
```yaml
|
||||
log:
|
||||
level: "info"
|
||||
format: "json"
|
||||
```
|
||||
## API
|
||||
|
||||
Environment overrides:
|
||||
### Workspaces
|
||||
|
||||
- `CODESPACE_LOG_LEVEL` (`debug`, `info`, `warn`, `error`)
|
||||
- `CODESPACE_LOG_FORMAT` (`json`, `text`)
|
||||
| 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) |
|
||||
|
||||
All application logs must go through `log/slog`. Do not use `fmt.Print*`, `log.Printf`, `gin.Logger()`, or third-party logging libraries in server runtime code.
|
||||
### Files
|
||||
|
||||
## API examples
|
||||
| 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": "..."}`) |
|
||||
|
||||
Health check:
|
||||
### 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 |
|
||||
|
||||
### Health
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| `GET` | `/healthz` | Server health check |
|
||||
|
||||
## Examples
|
||||
|
||||
### Health check
|
||||
|
||||
```sh
|
||||
curl -s http://localhost:8080/healthz
|
||||
```
|
||||
|
||||
Create a workspace:
|
||||
### Create a workspace
|
||||
|
||||
```sh
|
||||
curl -s -X POST http://localhost:8080/api/workspaces \
|
||||
@@ -95,7 +125,13 @@ curl -s -X POST http://localhost:8080/api/workspaces \
|
||||
-d '{"id":"user1"}'
|
||||
```
|
||||
|
||||
Write a file:
|
||||
### 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 \
|
||||
@@ -103,8 +139,20 @@ curl -s -X PUT http://localhost:8080/api/workspaces/user1/files/write \
|
||||
-d '{"path":"main.go","content":"package main\n"}'
|
||||
```
|
||||
|
||||
Read a file:
|
||||
### 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'
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```sh
|
||||
make test
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user