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.
13b0c4e53f37cf318ea3559057f51d9a0ab3eee2
BREAKING: every shell operation now requires a shellId. The Manager
previously keyed by workspaceID alone (one bash per workspace). It now
keys by (workspaceID, shellID) where shellID is a UUID returned by
Start.
Fixes the long-standing bug where the WS handler's closeAll called
stdin.Close() and killed bash when a WS client disconnected. The
Session owns the pty file; the WS handler no longer closes it. The
pty is only closed by Manager.Stop (explicit) or by captureOutput
when the process naturally exits (EOF).
- internal/shell/manager.go: Manager interface gains List and every
method takes shellID; storage becomes
map[workspaceID]map[shellID]*Session; Start returns (shellID, err)
via uuid.NewString; Resize/Status/ExitStatus/Subscribe/Stdin/Stop
route by shellID; new List(workspaceID) returns ShellInfo[] in
creation order.
- internal/shell/session.go: Session gains ShellID + CreatedAt; Status
type gains ShellID.
- internal/shell/manager_test.go: updated existing tests for new
signatures; added TestShellMultiInstance (two shells in one
workspace, no output cross-talk, independent stop, List behavior).
- internal/service/shell_service.go: wrappers carry shellID; new
List method.
- internal/service/workspace_service.go: auto-start captures/logs
shellID; Delete iterates and stops all workspace shells.
- internal/api/shell_handler.go: WS closeAll drops stdin.Close();
start/restart return 201 with {shellId, pid, ...}; new list handler;
stop/resize take shellId in body.
- internal/api/router.go: GET /api/workspaces/:id/shell (list).
- internal/model/shell.go: new ShellStartResponse, ShellInfo,
ShellListResponse, ShellStopRequest, ShellRestartRequest; updated
ShellStatusResponse + ShellResizeRequest to carry shellId.
- go.mod/go.sum: github.com/google/uuid.
E2E:
- workspace create -> 1 auto shell
- start 2 more -> 3 shells in list
- stop 1 -> 2 shells in list
- WS connect -> send cmd -> disconnect -> WS reconnect -> send cmd ->
response OK, no [process exited] banner
…
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%