forked from wrenn/wrenn
v0.1.0 (#17)
This commit is contained in:
153
CLAUDE.md
153
CLAUDE.md
@ -12,10 +12,10 @@ All commands go through the Makefile. Never use raw `go build` or `go run`.
|
||||
|
||||
```bash
|
||||
make build # Build all binaries → builds/
|
||||
make build-cp # Control plane only (builds frontend first)
|
||||
make build-cp # Control plane only
|
||||
make build-agent # Host agent only
|
||||
make build-envd # envd static binary (verified statically linked)
|
||||
make build-frontend # SvelteKit dashboard → internal/dashboard/static/
|
||||
make build-frontend # SvelteKit dashboard → frontend/build/ (served by Caddy)
|
||||
|
||||
make dev # Full local dev: infra + migrate + control plane
|
||||
make dev-infra # Start PostgreSQL + Prometheus + Grafana (Docker)
|
||||
@ -55,7 +55,7 @@ User SDK → HTTPS/WS → Control Plane → Connect RPC → Host Agent → HTTP/
|
||||
| Binary | Module | Entry point | Runs as |
|
||||
|--------|--------|-------------|---------|
|
||||
| wrenn-cp | `git.omukk.dev/wrenn/wrenn` | `cmd/control-plane/main.go` | Unprivileged |
|
||||
| wrenn-agent | `git.omukk.dev/wrenn/wrenn` | `cmd/host-agent/main.go` | Root (NET_ADMIN + /dev/kvm) |
|
||||
| wrenn-agent | `git.omukk.dev/wrenn/wrenn` | `cmd/host-agent/main.go` | `wrenn` user with capabilities (SYS_ADMIN, NET_ADMIN, NET_RAW, SYS_PTRACE, KILL, DAC_OVERRIDE, MKNOD) via setcap; also accepts root |
|
||||
| envd | `git.omukk.dev/wrenn/wrenn/envd` (standalone `envd/go.mod`) | `envd/main.go` | PID 1 inside guest VM |
|
||||
|
||||
envd is a **completely independent Go module**. It is never imported by the main module. The only connection is the protobuf contract. It compiles to a static binary baked into rootfs images.
|
||||
@ -64,21 +64,31 @@ envd is a **completely independent Go module**. It is never imported by the main
|
||||
|
||||
### Control Plane
|
||||
|
||||
**Packages:** `internal/api/`, `internal/dashboard/`, `internal/auth/`, `internal/scheduler/`, `internal/lifecycle/`, `internal/config/`, `internal/db/`
|
||||
**Internal packages:** `internal/api/`, `internal/email/`
|
||||
|
||||
Startup (`cmd/control-plane/main.go`) wires: config (env vars) → pgxpool → `db.Queries` (sqlc-generated) → Connect RPC client to host agent → `api.Server`. Everything flows through constructor injection.
|
||||
**Public packages (importable by cloud repo):** `pkg/config/`, `pkg/db/`, `pkg/auth/`, `pkg/auth/oauth/`, `pkg/scheduler/`, `pkg/lifecycle/`, `pkg/channels/`, `pkg/audit/`, `pkg/service/`, `pkg/events/`, `pkg/id/`, `pkg/validate/`
|
||||
|
||||
- **API Server** (`internal/api/server.go`): chi router with middleware. Creates handler structs (`sandboxHandler`, `execHandler`, `filesHandler`, etc.) injected with `db.Queries` and the host agent Connect RPC client. Routes under `/v1/sandboxes/*`.
|
||||
**Extension framework:** `pkg/cpextension/` (shared `Extension` interface + `ServerContext`), `pkg/cpserver/` (exported `Run()` entrypoint with functional options for cloud `main.go`)
|
||||
|
||||
The cloud repo imports this module as a Go dependency and calls `cpserver.Run(cpserver.WithExtensions(myExt))`. Each extension implements two methods: `RegisterRoutes(r chi.Router, sctx ServerContext)` to add HTTP routes, and `BackgroundWorkers(sctx ServerContext) []func(context.Context)` to add long-running goroutines. `ServerContext` carries all OSS services (DB, scheduler, auth, etc.) so extensions can use them without reimplementing anything. To expose a new OSS service to extensions, add it to `ServerContext` in `pkg/cpextension/extension.go` and populate it in `pkg/cpserver/run.go`.
|
||||
|
||||
**pkg/ vs internal/ decision rule:** A package belongs in `pkg/` only if the cloud repo needs to import it directly. Everything else stays in `internal/`. New OSS services (e.g. email, notifications) go in `internal/` — the cloud repo accesses them through `ServerContext`, not by importing the package. Do not put a service in `pkg/` just because the cloud repo uses it.
|
||||
|
||||
Startup (`cmd/control-plane/main.go`) is a thin wrapper: `cpserver.Run(cpserver.WithVersion(...))`. All 20 initialization steps live in `pkg/cpserver/run.go`: config → pgxpool → `db.Queries` → Redis → mTLS CA → host client pool → scheduler → OAuth → channels → audit logger → `api.New()` → background workers → HTTP server. Everything flows through constructor injection.
|
||||
|
||||
- **API Server** (`internal/api/server.go`): chi router with middleware. Creates handler structs (`sandboxHandler`, `execHandler`, `filesHandler`, etc.) injected with `db.Queries` and the host agent Connect RPC client. Routes under `/v1/capsules/*`. Accepts `[]cpextension.Extension` — each extension's `RegisterRoutes()` is called after all core routes are registered.
|
||||
- **Reconciler** (`internal/api/reconciler.go`): background goroutine (every 30s) that compares DB records against `agent.ListSandboxes()` RPC. Marks orphaned DB entries as "stopped".
|
||||
- **Dashboard** (SvelteKit + Tailwind + Bits UI, statically built and embedded via `go:embed`, served as catch-all at root)
|
||||
- **Database**: PostgreSQL via pgx/v5. Queries generated by sqlc from `db/queries/sandboxes.sql`. Migrations in `db/migrations/` (goose, plain SQL).
|
||||
- **Config** (`internal/config/config.go`): purely environment variables (`DATABASE_URL`, `CP_LISTEN_ADDR`, `CP_HOST_AGENT_ADDR`), no YAML/file config.
|
||||
- **Dashboard** (SvelteKit + Tailwind + Bits UI, built to static files in `frontend/build/`, served by Caddy as a reverse proxy)
|
||||
- **Database**: PostgreSQL via pgx/v5. Queries generated by sqlc from `db/queries/*.sql` → `pkg/db/`. Migrations in `db/migrations/` (goose, plain SQL). `db/migrations/embed.go` exposes `migrations.FS` so the cloud repo can run OSS migrations via `go:embed`.
|
||||
- **Config** (`pkg/config/config.go`): purely environment variables (`DATABASE_URL`, `CP_LISTEN_ADDR`, `CP_HOST_AGENT_ADDR`), no YAML/file config.
|
||||
|
||||
### Host Agent
|
||||
|
||||
**Packages:** `internal/hostagent/`, `internal/sandbox/`, `internal/vm/`, `internal/network/`, `internal/devicemapper/`, `internal/envdclient/`, `internal/snapshot/`
|
||||
|
||||
Startup (`cmd/host-agent/main.go`) wires: root check → enable IP forwarding → clean up stale dm devices → `sandbox.Manager` (containing `vm.Manager` + `network.SlotAllocator` + `devicemapper.LoopRegistry`) → `hostagent.Server` (Connect RPC handler) → HTTP server.
|
||||
**Production deployment:** `scripts/prepare-wrenn-user.sh` creates the `wrenn` system user, sets Linux capabilities (setcap) on wrenn-agent and all child binaries (iptables, losetup, dmsetup, etc.), installs an apt hook to restore capabilities after package updates, configures udev rules for `/dev/net/tun`, loads required kernel modules, and writes systemd unit files for both services. No sudo grants — all privilege is via capabilities.
|
||||
|
||||
Startup (`cmd/host-agent/main.go`) wires: root/capabilities check → enable IP forwarding → clean up stale dm devices → `sandbox.Manager` (containing `vm.Manager` + `network.SlotAllocator` + `devicemapper.LoopRegistry`) → `hostagent.Server` (Connect RPC handler) → HTTP server.
|
||||
|
||||
- **RPC Server** (`internal/hostagent/server.go`): implements `hostagentv1connect.HostAgentServiceHandler`. Thin wrapper — every method delegates to `sandbox.Manager`. Maps Connect error codes on return.
|
||||
- **Sandbox Manager** (`internal/sandbox/manager.go`): the core orchestration layer. Maintains in-memory state in `boxes map[string]*sandboxState` (protected by `sync.RWMutex`). Each `sandboxState` holds a `models.Sandbox`, a `*network.Slot`, and an `*envdclient.Client`. Runs a TTL reaper (every 10s) that auto-destroys timed-out sandboxes.
|
||||
@ -105,8 +115,8 @@ Runs as PID 1 inside the microVM via `wrenn-init.sh` (mounts procfs/sysfs/dev, s
|
||||
- **Package manager**: pnpm
|
||||
- **Routing**: SvelteKit file-based routing under `frontend/src/routes/`
|
||||
- **Routing layout**: `/login` and `/signup` at root, authenticated pages under `/dashboard/*` (e.g. `/dashboard/capsules`, `/dashboard/keys`)
|
||||
- **Build output**: `frontend/build/` → copied to `internal/dashboard/static/` → embedded via `go:embed` into the control plane binary
|
||||
- **Serving**: `internal/dashboard/dashboard.go` registers a `NotFound` catch-all SPA handler with fallback to `index.html`. API routes (`/v1/*`, `/openapi.yaml`, `/docs`) are registered first and take priority
|
||||
- **Build output**: `frontend/build/` — static files served by Caddy
|
||||
- **Serving**: Caddy reverse-proxies API requests to the control plane and serves the SvelteKit SPA directly. The control plane does not serve frontend assets.
|
||||
- **Dev workflow**: `make dev-frontend` runs Vite dev server on port 5173 with HMR. API calls proxy to `http://localhost:8000`
|
||||
- **Fonts**: Manrope (UI), Instrument Serif (headings), JetBrains Mono (code), Alice (brand wordmark) — all self-hosted via `@fontsource`
|
||||
- **Dark mode**: class-based (`.dark` on `<html>`) with system preference detection + localStorage persistence
|
||||
@ -147,19 +157,19 @@ HIBERNATED → RUNNING (cold snapshot resume, slower)
|
||||
|
||||
### Key Request Flows
|
||||
|
||||
**Sandbox creation** (`POST /v1/sandboxes`):
|
||||
**Sandbox creation** (`POST /v1/capsules`):
|
||||
1. API handler generates sandbox ID, inserts into DB as "pending"
|
||||
2. RPC `CreateSandbox` → host agent → `sandbox.Manager.Create()`
|
||||
3. Manager: resolve base rootfs → acquire shared loop device → create dm-snapshot (sparse CoW file) → allocate network slot → `CreateNetwork()` (netns + veth + tap + NAT) → `vm.Create()` (start Firecracker with `/dev/mapper/wrenn-{id}`, configure via HTTP API, boot) → `envdclient.WaitUntilReady()` (poll /health) → store in-memory state
|
||||
4. API handler updates DB to "running" with host_ip
|
||||
|
||||
**Command execution** (`POST /v1/sandboxes/{id}/exec`):
|
||||
**Command execution** (`POST /v1/capsules/{id}/exec`):
|
||||
1. API handler verifies sandbox is "running" in DB
|
||||
2. RPC `Exec` → host agent → `sandbox.Manager.Exec()` → `envdclient.Exec()`
|
||||
3. envd client opens bidirectional Connect RPC stream (`process.Start`), collects stdout/stderr/exit_code
|
||||
4. API handler checks UTF-8 validity (base64-encodes if binary), updates last_active_at, returns result
|
||||
|
||||
**Streaming exec** (`WS /v1/sandboxes/{id}/exec/stream`):
|
||||
**Streaming exec** (`WS /v1/capsules/{id}/exec/stream`):
|
||||
1. WebSocket upgrade, read first message for cmd/args
|
||||
2. RPC `ExecStream` → host agent → `sandbox.Manager.ExecStream()` → `envdclient.ExecStream()`
|
||||
3. envd client returns a channel of events; host agent forwards events through the RPC stream
|
||||
@ -189,7 +199,7 @@ To add a new RPC method: edit the `.proto` file → `make proto` → implement t
|
||||
|
||||
### sqlc
|
||||
|
||||
Config: `sqlc.yaml` (project root). Reads queries from `db/queries/*.sql`, reads schema from `db/migrations/`, outputs to `internal/db/`.
|
||||
Config: `sqlc.yaml` (project root). Reads queries from `db/queries/*.sql`, reads schema from `db/migrations/`, outputs to `pkg/db/`.
|
||||
|
||||
To add a new query: add it to the appropriate `.sql` file in `db/queries/` → `make generate` → use the new method on `*db.Queries`.
|
||||
|
||||
@ -201,7 +211,7 @@ To add a new query: add it to the appropriate `.sql` file in `db/queries/` → `
|
||||
- **TAP networking** (not vsock) for host-to-envd communication
|
||||
- **Device-mapper snapshots** for rootfs CoW — shared read-only loop device per base template, per-sandbox sparse CoW file, Firecracker gets `/dev/mapper/wrenn-{id}`
|
||||
- **PostgreSQL** via pgx/v5 + sqlc (type-safe query generation). Goose for migrations (plain SQL, up/down)
|
||||
- **Dashboard**: SvelteKit (Svelte 5, adapter-static) + Tailwind CSS v4 + Bits UI. Built to static files, embedded into the Go binary via `go:embed`, served as catch-all at root
|
||||
- **Dashboard**: SvelteKit (Svelte 5, adapter-static) + Tailwind CSS v4 + Bits UI. Built to static files in `frontend/build/`, served by Caddy (not embedded in the Go binary)
|
||||
- **Lago** for billing (external service, not in this codebase)
|
||||
|
||||
## Coding Conventions
|
||||
@ -233,7 +243,9 @@ The main module (`go.mod`) and envd (`envd/go.mod`) are fully independent. `make
|
||||
## Design Context
|
||||
|
||||
### Users
|
||||
Developers across the full spectrum — solo engineers building side projects, startup teams integrating sandboxed execution into products, and platform/infra engineers at larger organizations. The interface must feel at home for all three: approachable enough not to intimidate a hacker, precise enough to earn the trust of a production ops team. Never condescend, never oversimplify. Trust the user to understand what they're looking at.
|
||||
Developers across the full spectrum — solo engineers building side projects, startup teams integrating sandboxed execution into products, and platform/infra engineers at larger organizations running production workloads on Firecracker microVMs. They arrive with context: they know what a process is, what a rootfs is, what a TTY means. The interface must feel at home for all three: approachable enough not to intimidate a hacker, precise enough to earn the trust of a production ops team. Never condescend, never oversimplify. Trust the user to understand what they're looking at.
|
||||
|
||||
**Primary job to be done:** Understand what's running, act on it confidently, and get back to code.
|
||||
|
||||
### Brand Personality
|
||||
**Precise. Warm. Uncompromising.**
|
||||
@ -243,9 +255,9 @@ Wrenn is an engineer's favorite tool — built with visible care, not assembled
|
||||
Emotional goal: **in control.** Users leave a session with full confidence in what's running, what happened, and what comes next. Nothing is hidden, nothing is ambiguous.
|
||||
|
||||
### Aesthetic Direction
|
||||
**Dark-first, industrial-warm, data-forward.**
|
||||
**Dark-only (permanently), industrial-warm, data-forward.**
|
||||
|
||||
The near-black-green background palette (`#0a0c0b` through `#2a302d`) reads as "black with intention" — not pitch black (cold) and not charcoal (dated). The sage green accent (`#5e8c58`) is muted and organic, a meaningful departure from the startup-green neon that saturates the developer tool space.
|
||||
No light mode planned. All design decisions should optimize for dark. The near-black-green background palette (`#0a0c0b` through `#2a302d`) reads as "black with intention" — not pitch black (cold) and not charcoal (dated). The sage green accent (`#5e8c58`) is muted and organic, a meaningful departure from the startup-green neon that saturates the developer tool space.
|
||||
|
||||
**Anti-references:**
|
||||
- **Supabase**: avoid the friendly, approachable startup-green energy — too generic, too eager to please
|
||||
@ -259,30 +271,95 @@ The near-black-green background palette (`#0a0c0b` through `#2a302d`) reads as "
|
||||
### Type System
|
||||
Four fonts with strict roles — this is the design system's strongest personality trait and must be respected:
|
||||
|
||||
| Font | Role | When to use |
|
||||
|------|------|-------------|
|
||||
| **Manrope** (variable, sans) | UI workhorse | All body copy, nav, labels, buttons, form text |
|
||||
| **Instrument Serif** | Display / editorial | Page titles (h1), dialog headings, metric values, hero moments |
|
||||
| **JetBrains Mono** (variable) | Data / code | IDs, timestamps, key prefixes, file paths, terminal output, metrics |
|
||||
| **Alice** | Brand wordmark | "Wrenn" in sidebar and login only — nowhere else |
|
||||
| Font | CSS Class | Role | When to use |
|
||||
|------|-----------|------|-------------|
|
||||
| **Manrope** (variable, sans) | `font-sans` | UI workhorse | All body copy, nav, labels, buttons, form text |
|
||||
| **Instrument Serif** | `font-serif` | Display / editorial | Page titles (h1), dialog headings, metric values, hero moments |
|
||||
| **JetBrains Mono** (variable) | `font-mono` | Data / code | IDs, timestamps, key prefixes, file paths, terminal output, metrics |
|
||||
| **Alice** | brand wordmark only | Brand wordmark | "Wrenn" in sidebar and login only — nowhere else |
|
||||
|
||||
Instrument Serif at scale creates the signature editorial moments. Mono provides the precision signal for technical data. Never swap these roles.
|
||||
|
||||
**Tracking overrides (app.css):**
|
||||
- `.font-serif` — `letter-spacing: 0.015em` (positive tracking; Instrument Serif reads less condensed at display sizes)
|
||||
- `.font-mono` — `font-variant-numeric: tabular-nums` (numbers align in tables and metric displays)
|
||||
|
||||
**Type scale (root: 87.5% = 14px base):**
|
||||
| Token | Value | Use |
|
||||
|---|---|---|
|
||||
| `--text-display` | 2.571rem (~36px) | Auth section headings |
|
||||
| `--text-page` | 2rem (~28px) | Page h1 titles |
|
||||
| `--text-heading` | 1.429rem (~20px) | Dialog headings, empty states |
|
||||
| `--text-body` | 1rem (~14px) | Primary body, buttons, inputs |
|
||||
| `--text-ui` | 0.929rem (~13px) | Nav labels, table cells |
|
||||
| `--text-meta` | 0.857rem (~12px) | Key prefixes, minor info |
|
||||
| `--text-label` | 0.786rem (~11px) | Uppercase section labels |
|
||||
| `--text-badge` | 0.714rem (~10px) | Live badges, tiny indicators |
|
||||
|
||||
### Color System
|
||||
```
|
||||
Backgrounds: bg-0 (#0a0c0b) through bg-5 (#2a302d) — 6 steps
|
||||
Text: bright > primary > secondary > tertiary > muted — 5 levels
|
||||
Accent: accent (#5e8c58) / accent-mid / accent-bright / glow / glow-mid
|
||||
Status: amber (#d4a73c) / red (#cf8172) / blue (#5a9fd4)
|
||||
```
|
||||
|
||||
Use accent sparingly. It should feel earned — reserved for live/active state indicators, primary CTAs, focus rings, and active nav. When accent appears, it should register.
|
||||
All values are CSS custom properties in `frontend/src/app.css`.
|
||||
|
||||
### Upcoming Surfaces (design must accommodate)
|
||||
- **Terminal / shell output**: streaming exec output, TTY sessions. Needs strong mono treatment, high contrast for long sessions.
|
||||
- **File browser**: filesystem tree inside capsule. Density matters — breadcrumbs, file icons, permission bits.
|
||||
- **SDK / docs embedding**: code samples, quickstart flows inline in dashboard. Code blocks must feel premium, not afterthought.
|
||||
- **Billing / usage charts**: pool consumption, cost curves, usage over time. Instrument Serif at large scale for metrics; chart containers should feel like instruments, not dashboards.
|
||||
**Backgrounds (6-step near-black-green scale):**
|
||||
| Token | Value | Use |
|
||||
|---|---|---|
|
||||
| `--color-bg-0` | `#0a0c0b` | Page base, sidebar deepest layer |
|
||||
| `--color-bg-1` | `#0f1211` | Sidebar surface |
|
||||
| `--color-bg-2` | `#141817` | Card backgrounds |
|
||||
| `--color-bg-3` | `#1a1e1c` | Table headers, elevated surfaces |
|
||||
| `--color-bg-4` | `#212624` | Hover states, inputs |
|
||||
| `--color-bg-5` | `#2a302d` | Highlighted items, selected rows |
|
||||
|
||||
**Text (5-level hierarchy):**
|
||||
| Token | Value | Use |
|
||||
|---|---|---|
|
||||
| `--color-text-bright` | `#eae7e2` | H1s, dialog headings |
|
||||
| `--color-text-primary` | `#d0cdc6` | Body copy, primary labels |
|
||||
| `--color-text-secondary` | `#9b9790` | Secondary labels, descriptions |
|
||||
| `--color-text-tertiary` | `#6b6862` | Hints, placeholders |
|
||||
| `--color-text-muted` | `#454340` | Dividers as text, ultra-subtle |
|
||||
|
||||
**Accent (sage green — use sparingly, must feel earned):**
|
||||
| Token | Value | Use |
|
||||
|---|---|---|
|
||||
| `--color-accent` | `#5e8c58` | Primary CTA, live indicators, focus rings, active nav |
|
||||
| `--color-accent-mid` | `#89a785` | Hover accent text |
|
||||
| `--color-accent-bright` | `#a4c89f` | Accent on dark backgrounds |
|
||||
| `--color-accent-glow` | `rgba(94,140,88,0.07)` | Subtle tinted backgrounds |
|
||||
| `--color-accent-glow-mid` | `rgba(94,140,88,0.14)` | Hover tint on accent items |
|
||||
|
||||
**Status semantics:**
|
||||
| Token | Value | Use |
|
||||
|---|---|---|
|
||||
| `--color-amber` | `#d4a73c` | Warning, paused state |
|
||||
| `--color-red` | `#cf8172` | Error, destructive actions |
|
||||
| `--color-blue` | `#5a9fd4` | Info, neutral system states |
|
||||
|
||||
**Borders:** `--color-border` (`#1f2321`) default; `--color-border-mid` (`#2a2f2c`) for inputs/hover.
|
||||
|
||||
### Component Patterns
|
||||
|
||||
**Buttons:**
|
||||
- Primary: solid sage green (`--color-accent`), hover brightness boost + micro-lift (`-translate-y-px`)
|
||||
- Secondary: bordered (`--color-border-mid`), text transitions to accent on hover
|
||||
- Danger: red text + subtle red background on hover
|
||||
- All: `transition-all duration-150`
|
||||
|
||||
**Inputs:**
|
||||
- Border `--color-border`, background `--color-bg-2`; focus transitions border and icon to accent
|
||||
- Group focus pattern: `group` wrapper + `group-focus-within:text-[var(--color-accent)]` on icon
|
||||
|
||||
**Tables / data lists:**
|
||||
- Grid layout; header `bg-3` + uppercase `--text-label`; row hover `hover:bg-[var(--color-bg-3)]`
|
||||
- Status stripe: left border color matches sandbox state
|
||||
|
||||
**Status indicators:** Running = animated ping + sage green dot; Paused = amber dot; Stopped = muted gray. Color is never the sole differentiator.
|
||||
|
||||
**Modals & dialogs:** Border + shadow only — no accent gradient bars/strips. `fadeUp` 0.35s entrance.
|
||||
|
||||
**Empty states:** Large icon with glow, Instrument Serif heading, secondary body text, CTA below, `iconFloat` 4s animation.
|
||||
|
||||
**Animations (always respect `prefers-reduced-motion`):** `fadeUp` (entrance), `status-ping` (live indicator), `iconFloat` (empty states), `spin-once` (refresh), staggered `animation-delay` on lists.
|
||||
|
||||
### Design Principles
|
||||
|
||||
|
||||
Reference in New Issue
Block a user