Design Document
Dev Console — Product Design Document#
1. Overview#
Dev Console is an AI-chat-first development environment designed for software engineers who want to develop software from mobile devices or thin web clients while the heavy lifting runs on self-managed Linux infrastructure. The system organises work around three nested resources: a project (a repository), one or more workspaces (branch-scoped instances of that project), and agent sessions that act inside a workspace through chat. Developers can review and accept the changes agents propose, browse files, and interact with a terminal, all from a lightweight browser or mobile client.
2. Goals#
| # | Goal |
|---|---|
| G1 | Run entirely on self-managed Linux servers; no dependency on third-party cloud execution environments. |
| G2 | Provide a copilot-style AI chat interface as the primary way to make code changes. |
| G3 | Expose a terminal that is scoped to the active workspace. |
| G4 | Let users view, browse, and manually edit files in the workspace. |
| G5 | Present diffs of agent-proposed changes and let the user accept or reject them. |
| G6 | Authenticate users exclusively through GitHub OAuth (no separate credential store). |
| G7 | Keep the client thin enough to run well on mobile browsers. |
Non-Goals#
- A full IDE with language-server features (autocomplete, inline diagnostics, etc.) is out of scope for v1.
- The server does not manage its own AI model weights; it calls an external LLM API (e.g. OpenAI or Anthropic) configured by the operator.
- Multi-tenant / cloud-hosted SaaS deployment is not a design goal; each deployment serves a single user or a small trusted team.
3. Architecture Overview#
┌──────────────────────────────────────────────────────────────┐
│ Client Layer │
│ ┌────────────────────┐ ┌────────────────────────────┐ │
│ │ Web App (SPA) │ │ Mobile Browser / PWA │ │
│ └────────┬───────────┘ └────────────┬───────────────┘ │
└───────────┼────────────────────────────┼────────────────────┘
│ HTTPS / WebSocket │
┌───────────▼────────────────────────────▼────────────────────┐
│ Dev Console Server │
│ ┌──────────────┐ ┌───────────────┐ ┌──────────────────┐ │
│ │ Auth Module │ │ REST/WS API │ │ Static Asset │ │
│ │ (GitHub OAuth│ │ (HTTPS + WS) │ │ Serving │ │
│ └──────────────┘ └───────┬───────┘ └──────────────────┘ │
│ │ │
│ ┌─────────────────────────▼─────────────────────────────┐ │
│ │ Core Services │ │
│ │ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │ │
│ │ │ Project / │ │ Agent Chat │ │ Terminal │ │ │
│ │ │ Workspace │ │ Manager │ │ Manager │ │ │
│ │ │ Manager │ └──────┬───────┘ └──────┬──────┘ │ │
│ │ └──────┬──────┘ │ │ │ │
│ └─────────┼────────────────┼─────────────────┼──────────┘ │
│ │ │ │ │
│ ┌─────────▼────────────────▼─────────────────▼──────────┐ │
│ │ Infrastructure Layer │ │
│ │ ┌───────────┐ ┌──────────────┐ ┌───────────────┐ │ │
│ │ │ File I/O │ │ LLM Client │ │ PTY / Shell │ │ │
│ │ │ (fs, git) │ │ (HTTP client)│ │ (pty/exec) │ │ │
│ │ └───────────┘ └──────────────┘ └───────────────┘ │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘The server is a single long-running process written in Go (see §9). It hosts the REST + WebSocket API and serves the compiled client SPA as static assets from the same origin, removing CORS complexity.
4. Authentication#
4.1 GitHub OAuth Flow#
Client Server GitHub
│ │ │
│── GET /auth/login ──▶│ │
│◀─ 302 → github.com ─│ │
│ │ │
│── GET /auth/callback?code=… ────────────────▶│
│ │◀── access_token ───────│
│ │── GET /user ───────────▶│
│ │◀── { login, id } ───────│
│ │ │
│◀─ Set-Cookie: session ─│ │- The server is configured with a GitHub OAuth App
client_id/client_secret. /auth/loginredirects to GitHub’s authorization page withscope=read:user.- GitHub redirects back to
/auth/callback; the server exchanges the code for an access token, fetches the user’s GitHub login and numeric ID, then creates a signed, HTTP-only session cookie (JWT or opaque token backed by an in-memory or on-disk store). - All subsequent API calls must present this session cookie. The server validates it on every request.
- An allowlist of GitHub user IDs (configured by the operator) controls who may access the server.
4.2 Session Management#
- Sessions expire after a configurable idle timeout (default: 24 hours).
- Session tokens are signed with an operator-supplied secret key; no database is required for stateless JWT sessions.
- Logout invalidates the session cookie on the client and, if using opaque tokens, removes the server-side record.
5. Projects#
A project is the top-level context resource. All work happens within a project, and each project corresponds 1:1 with a single Git repository. Projects are created dynamically by authenticated users through the UI — users select a GitHub repository and the server registers and clones it into a configurable local storage directory.
5.1 Data Model#
Project {
id: string // URL-safe slug derived from repo name (lowercased,
// non-alphanumeric characters replaced with hyphens);
// a numeric suffix is appended on collision
name: string // Display name (defaults to repo name)
repoURL: string // GitHub repository URL, e.g. "https://github.com/owner/repo"
rootPath: string // Absolute path on the server where the repo is cloned
createdAt: timestamp
}5.2 Operations#
| Operation | Description |
|---|---|
GET /api/projects | List all projects. |
POST /api/projects | Create a new project by providing a GitHub repository URL. Server clones the repo into storage.projectsDir. |
GET /api/projects/:pid | Get project metadata. |
DELETE /api/projects/:pid | Delete a project and its on-disk clone. |
To support repository selection in the UI, the following GitHub-proxy endpoint is also provided:
| Operation | Description |
|---|---|
GET /api/github/repos | List GitHub repositories accessible to the authenticated user (proxied from the GitHub API using the user’s OAuth token). |
6. Workspaces#
A workspace is an instance of a project. Workspaces are 1:1 with a Git branch and (optionally) a pull request. A project may have many workspaces (e.g. one per feature branch or PR under review).
6.1 Data Model#
Workspace {
id: string // URL-safe slug, e.g. "feature-auth"
projectId: string // Parent project
name: string // Display name (defaults to branch name)
branch: string // Git branch name
prNumber: int? // GitHub PR number (null until a PR is opened)
createdAt: timestamp
}6.2 Operations#
| Operation | Description |
|---|---|
GET /api/projects/:pid/workspaces | List workspaces for a project. |
POST /api/projects/:pid/workspaces | Create a new workspace (checks out the branch). |
GET /api/projects/:pid/workspaces/:wid | Get workspace metadata. |
DELETE /api/projects/:pid/workspaces/:wid | Delete a workspace. |
GET /api/projects/:pid/workspaces/:wid/files?path= | List directory contents at path (relative to workspace root). |
GET /api/projects/:pid/workspaces/:wid/file?path= | Read file contents. |
PUT /api/projects/:pid/workspaces/:wid/file?path= | Write file contents (manual edit). |
GET /api/projects/:pid/workspaces/:wid/git/status | Return git status for the workspace. |
GET /api/projects/:pid/workspaces/:wid/git/diff?path= | Return unified diff for a path. |
6.3 File Browsing#
The client displays a file tree for the workspace root. Directories are
expanded on demand via GET /api/projects/:pid/workspaces/:wid/files?path=<dir>.
File contents are fetched and rendered in a read-only viewer with syntax
highlighting. A separate “edit” mode switches to a minimal text editor.
7. Agent Chat Sessions#
An agent session is a conversation between the user and an AI copilot that can read and write files inside the workspace. Agent sessions live within a workspace; a workspace may have any number of agent sessions.
7.1 Data Model#
AgentSession {
id: string
workspaceId: string
projectId: string
title: string // Auto-generated from first user message
createdAt: timestamp
updatedAt: timestamp
status: "active" | "idle" | "error"
}
Message {
id: string
sessionId: string
role: "user" | "assistant" | "tool"
content: string // Markdown text or tool-call JSON
createdAt: timestamp
}
PendingChange {
id: string
sessionId: string
filePath: string // Relative to workspace root
diff: string // Unified diff
status: "pending" | "accepted" | "rejected"
createdAt: timestamp
}7.2 Operations#
| Operation | Description |
|---|---|
GET /api/projects/:pid/workspaces/:wid/sessions | List agent sessions for a workspace. |
POST /api/projects/:pid/workspaces/:wid/sessions | Create a new agent session. |
DELETE /api/projects/:pid/workspaces/:wid/sessions/:sid | Close/delete a session. |
GET /api/projects/:pid/workspaces/:wid/sessions/:sid/messages | Fetch message history. |
WS /api/projects/:pid/workspaces/:wid/sessions/:sid/chat | Bidirectional stream for chat messages and agent events. |
GET /api/projects/:pid/workspaces/:wid/sessions/:sid/changes | List pending changes proposed by the agent. |
POST /api/projects/:pid/workspaces/:wid/sessions/:sid/changes/:cid/accept | Apply a pending change to the workspace. |
POST /api/projects/:pid/workspaces/:wid/sessions/:sid/changes/:cid/reject | Discard a pending change. |
7.3 WebSocket Chat Protocol#
Messages over the WebSocket are JSON-encoded and follow this envelope:
// Client → Server
{ "type": "user_message", "content": "Refactor the auth module to use JWT" }
{ "type": "cancel" } // Interrupt the in-flight agent turn
// Server → Client
{ "type": "assistant_chunk", "content": "Sure, I'll start by…" }
{ "type": "assistant_done" }
{ "type": "tool_call", "name": "read_file", "args": { "path": "auth.go" } }
{ "type": "tool_result", "name": "read_file", "content": "…file contents…" }
{ "type": "change_proposed", "changeId": "c1", "filePath": "auth.go", "diff": "…" }
{ "type": "error", "message": "LLM API quota exceeded" }7.4 Agent Tool Set#
The agent is given the following tools by the server (function-calling style):
| Tool | Description |
|---|---|
read_file(path) | Read a file from the workspace. |
write_file(path, content) | Propose a file change (creates a PendingChange; does not write to disk until accepted). |
list_files(path) | List directory contents. |
run_command(cmd, args) | Execute a shell command inside the workspace directory and return stdout/stderr. Restricted to a configurable allowlist (e.g. go test, npm test, make). |
git_diff(path?) | Return the current git diff. |
8. Terminal#
The terminal provides a full PTY session inside the workspace directory.
8.1 Operations#
| Operation | Description |
|---|---|
POST /api/projects/:pid/workspaces/:wid/terminals | Create a new terminal session; returns { terminalId }. |
DELETE /api/projects/:pid/workspaces/:wid/terminals/:tid | Kill the terminal. |
WS /api/projects/:pid/workspaces/:wid/terminals/:tid | Attach stdin/stdout/stderr over WebSocket. |
8.2 WebSocket Terminal Protocol#
Raw PTY bytes are forwarded over the WebSocket. The client sends resize events as a JSON message before switching to binary mode:
// Client → Server (JSON control message, UTF-8 text frame)
{ "type": "resize", "cols": 220, "rows": 50 }
// After the handshake, all frames are binary (raw PTY bytes).
// Client → Server: stdin bytes
// Server → Client: stdout/stderr bytes8.3 Security#
- Each terminal is bound to a single workspace root; the shell is started with
cwdset to the workspace root. - The operator may restrict which shells are allowed (default:
bash). - Terminal sessions are subject to the same session authentication as all other API endpoints.
9. Change Review#
When an agent proposes a file change it is stored as a PendingChange. The
user reviews proposed diffs in the client before they are written to disk.
9.1 Review UI Flow#
- After an agent turn completes, the client shows a badge on each changed file in the file tree.
- Selecting a changed file opens a side-by-side or unified diff view.
- The user clicks Accept or Reject per file.
- Accepting calls
POST …/changes/:cid/accept; the server atomically writes the new file content and records the change as accepted. - Rejecting calls
POST …/changes/:cid/reject; the pending change is discarded and the file is left unmodified. - After all changes in a session are resolved the agent session returns to
idlestatus.
10. Technology Stack#
10.1 Server#
| Concern | Choice | Rationale |
|---|---|---|
| Language | Go | Single static binary; good concurrency; easy deployment. |
| HTTP framework | net/http + gorilla/mux or chi | Lightweight; no magic. |
| WebSocket | gorilla/websocket | De facto standard in the Go ecosystem. |
| PTY | creack/pty | Mature Go PTY library. |
| LLM client | OpenAI-compatible HTTP client | Works with OpenAI, Anthropic (via compatibility layer), or local models (Ollama). |
| Session store | JWT (golang-jwt/jwt) signed with operator secret | Stateless; no database required. |
| Configuration | YAML file + env variable overrides | Simple operator experience. |
10.2 Client#
| Concern | Choice | Rationale |
|---|---|---|
| Language | TypeScript | Strong typing; large ecosystem. |
| Framework | React (Vite build) | Widely known; fast iteration; good mobile support. |
| Terminal emulator | xterm.js | Widely used; handles full VT100/VT220 escapes. |
| Diff viewer | react-diff-viewer or similar | Renders unified/side-by-side diffs. |
| Syntax highlighting | highlight.js or prism.js | Client-side, no server dependency. |
| Styling | Tailwind CSS | Responsive by default; small bundle. |
| State management | React Query + Zustand | Server state + local UI state. |
10.3 Deployment#
The operator runs a single dev-console binary and a short configuration file:
# dev-console.yaml
server:
listenAddr: ":8080"
tls:
certFile: "/etc/dev-console/tls.crt"
keyFile: "/etc/dev-console/tls.key"
auth:
github:
clientId: "Ov23liABCDEFGH"
clientSecret: "${GITHUB_CLIENT_SECRET}" # env variable
callbackUrl: "https://console.example.com/auth/callback"
allowedGithubUsers:
- "alice"
- "bob"
sessionSecret: "${SESSION_SECRET}"
sessionTtl: "24h"
llm:
provider: "openai" # or "anthropic", "ollama"
apiKey: "${OPENAI_API_KEY}"
model: "gpt-4o"
allowedCommands:
- "go"
- "npm"
- "make"
- "git"
storage:
projectsDir: "/srv/projects" # root directory where project repos are clonedThe compiled client SPA is embedded in the server binary using Go’s embed
package so no separate static file server is needed.
11. Security Considerations#
| Threat | Mitigation |
|---|---|
| Unauthorized access | GitHub OAuth + allowlist of GitHub user IDs. All API routes require a valid session. |
| Session hijacking | HTTP-only, Secure, SameSite=Strict cookies; short TTL with refresh. |
| Path traversal in file API | All file paths are resolved against the workspace root and rejected if the resolved path escapes it. |
| Arbitrary command execution via terminal | Terminal access is behind authentication; the operator controls which projects, workspaces, and shells are allowed. |
| Agent-triggered command execution | run_command tool is restricted to an operator-configured allowlist; commands run in a sandboxed subprocess with the workspace as the working directory. |
| LLM prompt injection | Agent tool outputs are escaped before being re-injected into the context; file content passed to the LLM is clearly delimited. |
| TLS | The server is expected to terminate TLS directly (cert/key configurable) or be placed behind a TLS-terminating reverse proxy. |
| Secret management | Secrets (OAuth client secret, session secret, LLM API key) are read from environment variables; they are never stored in the config file or logged. |
12. Open Questions / Future Work#
- Multi-user collaboration: Should multiple users be able to share a workspace and see each other’s agent sessions? (Deferred to v2.)
- Agent sandboxing: Should the agent’s
run_commandcalls run inside a container or VM for additional isolation? (Recommended for untrusted code.) - Persistent sessions: Agent session history could be persisted to an embedded SQLite database so it survives server restarts.
- Notification / async events: A Server-Sent Events (SSE) or WebSocket “event bus” endpoint could push workspace change notifications to all connected clients in real time.
- Mobile-native app: A React Native app could provide a richer mobile experience than the PWA, particularly for the terminal.