Skip to content

Latest commit

 

History

History
327 lines (283 loc) · 20.5 KB

code-structure.md

File metadata and controls

327 lines (283 loc) · 20.5 KB

Code structure

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.

Start with the deployment boundary

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.

Vocabulary

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.

Follow a control-plane request

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
Loading

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.

API schemas are organized by domain

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.

Follow a host execution

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.

Shared operation profiling

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.

Follow a browser action

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.

Add a feature without losing the structure

  1. Identify the service and concern that owns the behavior using the maps above.
  2. Add or update its domain rule, wire model, and port only where the behavior requires them. Implement durable operations inside storage.
  3. Put orchestration in an application flow or owning service, then call it from the HTTP adapter or worker. Wire new resources through bootstrap.
  4. Add public facade exports deliberately. Import implementation modules directly within a package to keep its dependency graph clear.
  5. Exercise observable behavior and affected boundaries. For structural refactors, compare contracts and reuse existing behavioral tests.
  6. 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.