This guide explains where behavior belongs and how to follow it through the repository. Use development.md for setup and checks, and AGENTS.md for service and security boundaries.
apps/
backend/
cloud_agent/ Control plane and its HTTP APIs
cloud_agent_host/ Host execution and retained sandbox management
cloud_agent_shared/ Contracts and algorithms shipped in both images
migrations/ Agent-owned database schema, managed by Alembic
openapi.json The committed snapshot of this service's OpenAPI
frontend/ Next.js browser UI and backend-for-frontend routes
deploy/ Image builds and deployment entry points
scripts/ Development, qualification, and release tooling
tests/ Backend behavior, integration, and boundary checks
docs/ Maintained guides and dated design evidence
The control plane and host are independently deployed implementations. Neither imports the other. Shared code provides the facts and contracts they agree on; it imports neither implementation. Accounts, admission and usage live in the agent database; model calls go from the sandbox to the owner's provider.
The code and the wire keep their historical names; documentation and new code use the terms in the first column. A job is one turn of a conversation, not the user's whole task.
| Term | In code and on the wire | Meaning |
|---|---|---|
| Conversation | thread, thread_id |
One user session, made of turns; a chat or task in the UI |
| Turn | job, job_id, turn_no |
One user message and the agent's work on it; every turn after the first names its parent_job_id |
| Attempt | attempt_id, attempt_no, lease_generation |
One execution of a turn on one host; it carries the lease fence |
| Sandbox group | agent_sandboxes, sandbox_id, sandbox_generation |
One retained container and its workspace |
| Member | membership, member | One conversation in a group, with its own native session and CLI process |
| Activation, residency, process | activation_generation, residency_generation, process_generation |
A member's three layers: its activation in the container, the residency that lets one live CLI process serve later turns, and that process. A stale generation is refused at its layer |
| Placeholder credential | amr. (MCP), agh. (GitHub) |
A bearer that names one residency. Only its relay accepts it, and the relay resolves the fence again on every request |
Never name a new field a bare generation: say what it counts, as in
sandbox_generation or residency_generation.
flowchart TD
App[app.py: listener and route assembly] --> API[api/: HTTP adapters]
API --> Flows[application/ and services: orchestration]
API --> Ports[ports/: capability contracts]
Flows --> Ports
Flows --> Domain[domain/: rules and vocabulary]
Bootstrap[bootstrap/: resource and lifecycle assembly] --> Workers[workers/: background loops]
Workers --> Flows
Bootstrap --> Storage[storage/: Postgres implementations]
Storage --> Ports
Storage --> Domain
app.py mounts the owner/admin API and the separate MCP-only listener. It
installs shared HTTP response and exception policies, and on both listeners a
dependency that runs ahead of every route's own (api/_parameters.py): a path
or query parameter holding NUL, or a route's integer parameter too wide for a
bigint, is refused with a 422 before any query reads it. So is a JSON body
holding NUL or half a surrogate pair, on a route an owner, an administrator or
the on-call relay calls; host and sandbox reports keep their own models, which
replace those characters instead. bootstrap/lifespan.py
constructs the resources and starts and stops background work; route modules
do not own service startup or shutdown.
| Directory or module | Owns | Put a change here when |
|---|---|---|
api/ |
HTTP parsing, dependencies, authorization entry points, response mapping | The request or response behavior changes |
schemas/ |
Pydantic request, response, and host/worker wire models | A transport field or validation rule changes |
application/ |
Admission, follow-ups, Fork, delivery, reconciliation, and other flows | A use case coordinates several capabilities |
domain/ |
Execution state, request identity, and business vocabulary | A rule is independent of persistence and HTTP |
ports/ |
Narrow typed capabilities | A caller needs a new operation from an adapter |
storage/assembly.py |
Construction of Postgres repository capabilities | A storage capability is wired into the service |
storage/repositories/ |
SQL and transaction/fencing behavior | A durable query or mutation changes |
workers/ |
Scheduled work and recovery loops | Work continues independently of a request; queue and goal recovery cannot hold up attempt lease recovery |
bootstrap/ |
Resources and lifecycle | A component must be constructed, started, or closed |
mcp/ |
MCP hosting, transport, policy, and process-local runtime | Protocol or upstream session behavior changes |
goals/ |
Goal orchestration and retry policy | Goal scheduling or progress policy changes |
providers/ |
External provider implementations | A provider's integration behavior changes |
The storage assembly exposes capabilities such as store.host_pool and
store.pending_messages. Callers type against the capabilities they use. They
do not import Postgres repositories or reach through the assembly to its pool.
Outside storage/, the assembly is the only storage module a caller imports.
A port imports only domain/, schemas/, cloud_agent_shared, other ports and
libraries, because its caller and its implementation both depend on whatever it
names: a type a new port needs moves to domain/ or schemas/ first.
Do not add a port that only repeats its repository's methods: the assembly then
names the repository class, and each caller declares the narrow Protocol it
uses, as api/config.py does for model preferences.
Application flows do not raise HTTP errors: they raise the domain errors
Refused and Unavailable (domain/errors.py), and api/errors.py maps them
to the same status, headers and body. A test refuses HTTPException in
application/.
MCP session ownership lives in mcp/runtime.py: lazy host construction,
fenced session reaping, maintenance, and closing. Both the relay's handlers
and bootstrap call that runtime directly. Eager send-now handoff task retention
and cancellation live in application/send_now.py; the durable delivery rows
and recovery worker still own crash recovery.
Import public models from cloud_agent.schemas. Its __init__.py explicitly
exports them; implementations use direct imports between domain modules rather
than importing the facade back into themselves.
| Modules | Models for |
|---|---|
jobs, conversations, messages, goals |
Tasks, sessions, queues, questions, and goals |
events, resources, workspace |
Event streams, measurements, files, terminals, and Git views |
workers, hosts, admin_hosts |
Worker requests, enrollment, heartbeats, and admin host controls |
archive_settings, audit, config |
Archive settings, admin audit, and advertised configuration |
runtime_upgrades |
Agent CLI updates started from the admin Settings page |
integrations, mcp, session |
Source control, MCP selection, identity, and user preferences |
teams |
Teams, members, invitations, and join requests |
_host_reports, _prompts |
Private helpers: bounds for host and worker reports, and the prompt text rule |
Keep model names and serialized behavior stable during structural refactors. Cross-domain annotations must resolve immediately after import. Intentional wire changes require the contract update described in the developer guide.
The host package groups its modules by concern. Paths below are relative to
cloud_agent_host/; a module named without a directory sits at its top level.
| Package | Owns |
|---|---|
service/ |
The long-lived host service: enrollment, heartbeats, the claim loop, draining, what the host advertises, and starting each turn's child |
turn/ |
One claimed turn in its child process: reservation, source preparation, the attempt's stages, and the stream it relays |
container/ |
The Docker/Kata boundary: retained chat containers, their models and markers, and the exact-container identity gate |
network/ |
The sandbox network: this deployment's host rules, per-group bridges, and the workloads that depend on them |
supervisor/ |
The shared-session supervisor: one group's lifecycle, registration and reconciliation, its members and their native sessions |
archive/ |
Sandbox archives, restorable checkpoints, and verified deletion of stopped sandboxes |
observe/ |
The guest observer, member execution windows, tool windows, and Docker resource samples |
member_setup/ |
What the trusted host installs into a member before its CLI starts: plugins, MCP helpers, runtimes, trusted files |
launch/, runtimes/, manager_api/ |
Launch facets, the Agent CLI adapters, and the Sandbox Manager's HTTP layer |
The top level keeps the process entry points (runner.py, sandbox_manager.py,
candidate_check.py, and the host_capacity and storage_attestation
commands deployment runs inside a runner) and the modules several packages
share, such as claimed_job.py, control_plane_client.py, drivers.py,
resident_transport.py and session_lifecycle.py.
runner.py is the command line. The host service in service/host_service.py
enrolls the host, heartbeats, claims turns up to its limit, and drains on
SIGTERM; service/host_capabilities.py holds what it advertises and the proof
each claim carries. Each claimed turn runs in a child process
(turn/turn_child.py) that preflights, answers the handover, and then runs the
attempt under the lease service/attempt_lease.py renews:
turn/group_reservation.py checks and reserves its sandbox group,
turn/shared_live_attempt.py runs it as a sequence of stages, and
turn/turn_stream.py relays the admitted turn. turn/source_preparation.py is
the trusted Git work on a chat's source outside the sandbox. claimed_job.py
defines claim payloads and lease errors; control_plane_client.py owns
attempt-scoped worker HTTP calls, request deadlines, and event replay. The
client queues a turn's events on the ordered sender in event_sender.py, which
uploads them in batches.
container/chat_sandbox_models.py holds retained-container specs,
inspection/status values, ownership labels, and identity bounds. Group bridges,
relay membership, Kata guest route synchronization, and network proof/release
live in the composed network/chat_sandbox_network.py component. It consumes a
narrow container protocol; exact container identity, bounded exec, and
fail-stop operations stay with the manager. Existing manager network entry
points delegate to that owner. Network snapshots remain scoped to one
operation, and operational log names are stable.
container/chat_sandbox.py manages retained groups and their members. Its
ChatSandboxManager is assembled from mixins, one module per concern:
container creation (chat_sandbox_create.py), member execs and processes
(chat_sandbox_members.py), workspace and home files
(chat_sandbox_workspace.py), the guest observer (chat_sandbox_observation.py)
and archive markers (chat_sandbox_markers.py); terminals and the retained
backend are in chat_sandbox_terminal.py and retained_backend.py. A test pins
the manager's method set. The in-guest network check is
network/guest_network_check.py, and the retained-container inventory the
capacity report reads is observe/sandbox_inventory.py.
container/sandbox.py owns the container/process boundary. The Sandbox Manager
and session supervisor coordinate native sessions and workspace operations. The
supervisor's metadata lock protects in-memory ownership only. Stop, orphan
recovery, and archive deletion reserve one group's lifecycle before Docker or
marker I/O, then revalidate its group, member, container, and lifecycle
identities before accepting the result. Other groups remain available; failed
checks release their temporary reservation, while durable archive and
native-flush barriers remain in force. Public sandbox model imports through
chat_sandbox continue to work.
A group's lifecycle is one value, GroupLifecycleState
(supervisor/group_lifecycle_state.py): which start, stop, exclusive workspace
operation and lifecycle check hold it, each with its holder, the barriers
still owed (a final archive, an orphan finalization), and the run's fence
label. Only that module's transitions change it, and a test refuses any other
assignment. supervisor/group_lifecycle.py is the admission table that decides
what may run now. The design
records which combinations are reachable and the invariants the tests check.
The runtime adapters are a package under cloud_agent_host/runtimes/, with one
package per CLI:
| Module | Responsibility |
|---|---|
base.py |
Adapter contracts, capabilities, MCP configuration, and runtime errors |
events.py |
Normalized event vocabulary and bounded payload helpers |
preview.py |
Partial-reply preview batching |
member_launch.py |
The steps one CLI adds to a member's launch, with a CLI-free default |
claude/ |
Claude Code: its launch and event parsing (adapter), live session (session), background tasks (background), plugins (plugins), and member config, launch steps and native Fork (member) |
codex/ |
Codex: its app-server launch (adapter), notification readings (app_server), live session (session), MCP relay policy (mcp), plugins (plugins), and member home, launch steps and Fork recovery (member) |
pi/ |
pi: its launch and event parsing (adapter), live session with its native Fork (session), and launch steps (member) |
http_bridge/ |
OpenCode and Kilo: the launch they share (adapter), each CLI's name, wrapper binary and catalog entry (opencode, kilo), the bridge's records and live session (parts, session), Kilo's MCP runtime (kilo_mcp), and member home, launch steps and server locks (member) |
registry.py |
Adapter lookup and construction |
live.py |
Each CLI's live session class, the driver that opens it, and its launch steps |
drivers.py keeps the generic Driver, Session and Turn contract and the base
classes the sessions share. resident_transport.py opens member processes and
journals native Forks. It does not branch on the CLI to launch one: each
CLI's member module defines that CLI's launch steps (its member config or
private home, trusted helpers, plugin installer, where platform instructions
go, and Claude's commit attribution), and the factory calls them. Native Fork
still branches on the CLI there. member_setup/member_homes.py holds the private provider homes that Codex, OpenCode
and Kilo members use.
Existing callers use cloud_agent_host.runtimes; the package explicitly
exports its public API, which is the dialect layer. Sessions, live and the
member modules build on drivers.py, which imports that API, so callers
import them by module name. Runtime discovery facts live in the immutable
cloud_agent_shared/runtime_catalog.py, so the control plane advertises runtime
IDs, MCP support, and reasoning efforts without loading host adapters. Runtime
version pins remain in cloud_agent_shared/shared_protocol.py and image files.
Producers continue to import cloud_agent_shared.operation_profiling. The
implementation is organized under cloud_agent_shared/profiling/ by schema,
models, run parsing, policy, measurement collection, stage observers, process
context, export, and clock access. See the package's
responsibility and contract guide.
The facade keeps the existing producer API and operational diagnostics. Clock
access respects its replaceable time object, keeping deterministic tests and
the default-off path intact. New profiling logic belongs in its concern module
instead of expanding the facade.
Next.js route files live under apps/frontend/src/app/. Shared feature UI lives
under components/features/ (agents for tasks and conversations, settings
for the settings pages, teams, profile and auth), reusable presentation
components under components/, and browser utilities and API clients under
lib/. Server-only
integrations belong in the existing server modules; a browser component must
not pull server credentials into its import graph.
lib/api/agents.ts is the explicit public API facade. It delegates to focused
modules under lib/api/agents/: jobs, threads, sessions, goals, configuration,
integrations, resource and usage reads, workspace files, terminals, and event
streams. Shared create/Fork idempotency response handling stays private to that
package. Keep endpoint paths, retries, credentials, cursor handling, and
notifications with their owning operation.
Inside components/features/agents/, each concern has its own folder:
| Folder | Responsibility |
|---|---|
adapt/ |
The Job and Thread API-shape adapters, and the event, diff, resource and job-state readers they share |
conversation/ |
Transcript, turns, tool activity, turn navigation and the side chat |
composer/ |
Message input, the new-task composer, model and repository pickers, queued messages |
sidebar/ |
Conversation list, folders, search, archive and getting started |
workspace/ |
Workspace files and tabs, terminals, recorded diffs, temporary workspaces |
trajectory/ |
Trajectory view, step inspector, and the protocol and step projections |
Feature areas such as context/, environment/, goal/, mcp/ and skills/
follow the same rule. The top level keeps what several folders share:
types.ts, the job and conversation records, the read and push providers, and
the job page. Tests sit next to the module they test.
components/features/agents/JobDetail.tsx composes the job page. Its extracted
implementation lives under job-detail/:
| Module | Responsibility |
|---|---|
JobDetailHeader |
Page header and conversation actions |
JobTranscript |
Transcript projection and navigation |
JobComposer, useJobComposerSettings |
Message input and composer settings |
RunOutcome, GitPanel |
Outcome, pull request, and Git interaction |
JobDetailSidePanels, LazyPanels |
Workspace/inspector rendering and lazy loading |
useJobDetailPanels |
Panel selection, focus, resizing, and retained panel state |
useJobConversation |
Turn navigation, transcript scrolling, and reading position |
useJobRunActions |
Stop, Fork, and restore actions with request guards |
Keep state with the concern that owns it. Retained panels preserve terminals, file previews, and drafts while hidden; scope changes must reset the relevant state. Keep hooks' dependencies explicit and preserve lazy loading when extracting components.
- Identify the service and concern that owns the behavior using the maps above.
- Add or update its domain rule, wire model, and port only where the behavior requires them. Implement durable operations inside storage.
- Put orchestration in an application flow or owning service, then call it from the HTTP adapter or worker. Wire new resources through bootstrap.
- Add public facade exports deliberately. Import implementation modules directly within a package to keep its dependency graph clear.
- Exercise observable behavior and affected boundaries. For structural refactors, compare contracts and reuse existing behavioral tests.
- Run the relevant complete gates and update the maintained guide for changed ownership, commands, or behavior.
Backend architecture checks enforce service-image independence, API import
direction, domain/port independence, and repository isolation. Backend imports
must use the installed package names (cloud_agent, cloud_agent_host, and
cloud_agent_shared), rather than the checkout namespace apps.backend.
The host image flattens these packages, so checkout-prefixed imports can work
locally and fail after deployment; the guards reject them in all three packages.
The import scanners include relative imports, imports inside functions, and
TYPE_CHECKING blocks. Backend behavior tests remain under tests/; frontend
tests remain alongside the feature or utility they exercise.