This repository has been archived on 2026-07-17 . You can view files and clone it. You cannot open issues or pull requests or push a commit.
568423751f816f5e19498ddd9a382bbfcf067e41
The race: os/exec writes cmd.ProcessState inside cmd.Wait(), which
runs in the waitExit goroutine. Start, Status, Subscribe, and
Restart all read cmd.ProcessState from other goroutines to decide
'still running?'. That's a textbook Go data race — fixed by
TestStartRejectsDuplicateRunningSession under -race.
Fix: port the same pattern the shell package uses for its multi-shell
refactor. The manager now owns an exited map[string]struct{} guarded
by exitedMu. waitExit is the ONLY place that calls cmd.Wait(); after
Wait() returns it records exited[workspaceID]. Everyone else checks
the exited map instead of reading cmd.ProcessState.
- internal/process/manager.go: add exited + exitedMu to LocalManager;
Start conflict check, Status, Subscribe, Restart, Stop all consult
the exited map; Start clears the entry on new process; Stop and
Restart delete the entry on stop; waitExit sets it after Wait().
- go.mod / go.sum: tidied (uuid v1.6.0 was already direct dep from
the shell refactor).
go test -race -count=3 ./... clean across the whole repo.
go test -race -count=5 ./internal/process/... clean.
…
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
FileSysteminterface 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 acpby 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. Servesfs/read_text_fileandfs/write_text_filefrom the workspace filesystem. The web UI surfaces this as a chat panel alongside the terminal. - HTTP API using Gin for routing with JSON responses.
- Explicit
http.Servercomposition with configurable timeouts and max header bytes. - Structured logging using Go standard library
log/slogwith 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
make run
The server listens on :8080 by default.
Configuration
configs/config.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
curl -s http://localhost:8080/healthz
Create a workspace
curl -s -X POST http://localhost:8080/api/workspaces \
-H 'Content-Type: application/json' \
-d '{"id":"user1"}'
List workspaces
curl -s http://localhost:8080/api/workspaces
Write a file
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
curl -s 'http://localhost:8080/api/workspaces/user1/files/read?path=main.go'
Get file stat
curl -s 'http://localhost:8080/api/workspaces/user1/files/stat?path=main.go'
Send a prompt to the agent
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
make test
Languages
Go
52.7%
TypeScript
46%
CSS
1%
JavaScript
0.2%