Conversation
Gastown is an autonomous software engineering platform built on Cloudflare Workers, Durable Objects, and Containers. This branch implements the full stack from scratch. Architecture: - Cloudflare Worker (Hono) as the API gateway with CF Access auth - TownDO as the central control-plane Durable Object, consolidating all rig, agent, bead, and config state in SQLite - AgentDO for per-agent event storage with 10GB isolation - TownContainerDO running Docker containers with the Kilo SDK, process manager, PTY proxy, and git merge infrastructure - GastownUserDO and AgentIdentityDO for user/agent identity management Data model: - Beads-centric universal schema: all domain objects (issues, messages, molecules, agents, merge requests, convoys, escalations) are beads with type-specific satellite metadata tables - Bead dependencies graph for convoy/molecule relationships - Bead events log for full audit trail Agent system: - Mayor agent as the orchestrating AI that triages, plans, and delegates - Polecat agents as parallel workers dispatched to individual tasks - Refinery agent for code review before merge - Witness agent for periodic health monitoring - Convoy system for coordinated multi-agent execution - Molecule system for multi-step task decomposition - Mail system for inter-agent messaging - Escalation system with configurable thresholds and auto-bump Terminal and UI: - xterm.js PTY terminals connected to container SDK sessions via WebSocket passthrough (browser → worker → container) - Dashboard with drawer stack navigation for deep drill-down into beads, agents, and events - Persistent mayor terminal bar across all town pages - Bead kanban board, activity feed, agent grid, and observability views - Town-specific sidebar with live rig list Infrastructure: - Git credential resolution from platform integrations with on-demand refresh via Next.js API - Detached worktree merge strategy to avoid branch lock conflicts - All Gastown UI and tRPC routes gated to admin-only users - Next.js tRPC router with 22 procedures for the frontend
Code Review SummaryStatus: 8 Issues Found | Recommendation: Address before merge Fix these issues in Kilo Cloud Overview
Issue Details (click to expand)CRITICAL
WARNING
SUGGESTION
Other Observations (not in diff)No additional issues found outside the diff. Files Reviewed (96 files)
|
…t container, as casts - Add CF Access JWT validation to WebSocket upgrade handler (was bypassing Hono middleware). Extracted validateCfAccessRequest() from the Hono middleware for reuse outside the middleware chain. - Remove X-Town-Id header fallback from getTownId() — all callers already have :townId in the route path, and the header was a client-controllable cross-town vector. - Add non-root "agent" user to both Dockerfiles with proper ownership of /workspace, /app, and ~/.config/kilo directories. - Remove as casts in getEnforcedAgentId() and getTownId(), using flow-sensitive typing instead.
|
|
||
| // ── Town Configuration ────────────────────────────────────────────────── | ||
|
|
||
| app.get('/api/towns/:townId/config', c => handleGetTownConfig(c, c.req.param())); |
There was a problem hiding this comment.
CRITICAL: Town-level routes (/api/towns/:townId/config, /api/towns/:townId/convoys, /api/towns/:townId/escalations, /api/towns/:townId/container/*, /api/towns/:townId/mayor/*) have no authorization middleware beyond Cloudflare Access.
Any CF Access-authenticated user can:
- Read/write any town's configuration (including secrets like
kilocode_token,git_authtokens viaPATCH /api/towns/:townId/config) - Start/stop agents in any town's container
- Send messages as the mayor of any town
- Create convoys and acknowledge escalations in any town
The rig routes are protected by authMiddleware (line 142), but these town-level routes are not. Consider adding an ownership check middleware for /api/towns/:townId/* routes that verifies the authenticated user owns the town.
|
|
||
| // Verify the rigId in the JWT matches the route param | ||
| const rigId = c.req.param('rigId'); | ||
| if (rigId && result.payload.rigId !== rigId) { |
There was a problem hiding this comment.
WARNING: authMiddleware validates rigId matches the JWT but does not validate townId. An agent with a valid JWT for rig R in town A could call /api/towns/<townB>/rigs/<R>/... and the middleware would pass because the rigId matches. The handler then resolves townId from the route param (via getTownId), allowing the agent to operate on town B's data.
Consider also checking:
| if (rigId && result.payload.rigId !== rigId) { | |
| // Verify the rigId in the JWT matches the route param | |
| const rigId = c.req.param('rigId'); | |
| if (rigId && result.payload.rigId !== rigId) { | |
| return c.json(resError('Token rigId does not match route'), 403); | |
| } | |
| // Verify the townId in the JWT matches the route param | |
| const townId = c.req.param('townId'); | |
| if (townId && result.payload.townId !== townId) { | |
| return c.json(resError('Token townId does not match route'), 403); | |
| } |
| ); | ||
|
|
||
| const town = getTownDOStub(c.env, params.townId); | ||
| const beads = await town.listBeads({ |
There was a problem hiding this comment.
WARNING: handleMayorListBeads verifies the rig belongs to the town but then calls town.listBeads() without passing rig_id: params.rigId. This returns beads from ALL rigs in the town, not just the requested rig. The route is /api/mayor/:townId/tools/rigs/:rigId/beads which implies rig-scoped results.
| const beads = await town.listBeads({ | |
| const beads = await town.listBeads({ | |
| status: status?.data, | |
| type: type?.data, | |
| rig_id: params.rigId, | |
| assignee_agent_bead_id: | |
| c.req.query('assignee_agent_bead_id') ?? c.req.query('assignee_agent_id'), | |
| limit: limit?.data, | |
| offset: offset?.data, | |
| }); |
| ); | ||
|
|
||
| const town = getTownDOStub(c.env, params.townId); | ||
| const agents = await town.listAgents({}); |
There was a problem hiding this comment.
WARNING: handleMayorListAgents verifies the rig belongs to the town but then calls town.listAgents({}) with an empty filter. The route is /api/mayor/:townId/tools/rigs/:rigId/agents which implies rig-scoped results, but this returns agents from ALL rigs.
| const agents = await town.listAgents({}); | |
| const agents = await town.listAgents({ rig_id: params.rigId }); |
What is Gastown?
Gastown is an agent orchestration system for managing multiple AI coding agents working concurrently across git repositories. It was originally built as a local CLI tool — two Go binaries (
gtfor orchestration andbdfor the Beads work-tracking database) coordinated with tmux on the user's machine.The core primitive is beads — a universal data model where every object (issues, mail, merge requests, escalations, convoys, agents) is a bead. Agents are organized into roles: a Mayor that coordinates, Polecats that do the work, a Refinery that reviews and merges, and a Witness that monitors health.
What is this PR?
Gastown Cloud — a cloud-native implementation of Gastown as a Kilo platform feature, built on Cloudflare Workers, Durable Objects, and Containers. Instead of tmux on a laptop, agents run in Cloudflare Containers. Instead of databases on disk, state lives in DO SQLite. Users interact through a web dashboard with live agent terminals via xterm.js.
There's still a lot of work to do being tracked in #204
Architecture
A diagram for ants

Data model
Agent system
Terminal and UI
Infrastructure