docs/architecture.md

main at 58e6347eeb72 · 17 KB

OpenAgents architecture

Date: 2026-08-22

Status: Current production architecture

Purpose

This document is the source of truth for the integrated openagents.com application. Use it to understand product ownership, durable authority, trust boundaries, and deployment strategies. Dated migration and implementation plans remain useful as historical records, but they do not override this document.

OpenAgents is one public AGPL-3.0 application. It includes the source-code forge, issues and projects, the Sarah conversational agent, provider orchestration, memory, voice, delegated work, connected computers, and operator surfaces. The application does not divide these features between a public shell and a private product service.

For shared vocabulary — forge versus GitHub, push versus deploy, computer versus machine, which receipt, which module — read docs/taxonomy.md.

System map

browser
  |
  | HTTPS, LiveView, WebSocket, or authenticated JSON
  v
OpenAgents Phoenix application
  |-- public and authenticated web surfaces
  |-- Sarah conversation, voice, memory, and work orchestration
  |-- issues, projects, computers, and data rights
  |-- Git HTTP and forge deployment control
  |-- server-side provider adapters
  |
  +--> PostgreSQL: durable product and deployment authority
  +--> object/artifact storage: immutable build artifacts
  +--> provider APIs: replaceable server-side dependencies
  +--> GitHub: identity and user-authorized repository access
  `--> BEAM cluster: ephemeral execution and fleet coordination

The OpenAgents namespace owns generic application infrastructure. Sarah is a persona and behavior package within that application, not a second application or service boundary.

Product ownership

The repository owns these capabilities:

  • Browser presentation and Phoenix LiveView state.
  • Sarah persona artifacts, behavior, voice, and evaluation material.
  • Conversation, message, turn, provider-step, and tool-step lifecycles.
  • Provider selection, request construction, response handling, and usage accounting.
  • Tools, memory systems, delegated work, and connected-computer orchestration.
  • Accounts, data export, deletion, administrative policy, and incidents.
  • Repositories, Git HTTP, issues, labels, milestones, projects, and public code browsing.
  • Build, promotion, deployment, rollback, convergence, and deployment receipts.

Do not add a private Sarah service as an architectural dependency. A separate provider or infrastructure service can exist behind a documented adapter, but the public application remains responsible for its product contracts and data rights.

Durable authority

PostgreSQL is the durable authority for product state, authorization state, repository metadata, work state, fleet targets, and receipts. A transaction that does not commit cannot become product truth.

Treat these systems as projections or execution aids:

  • LiveView socket assigns and browser state.
  • Phoenix PubSub messages.
  • BEAM registries, supervisors, tasks, and process mailboxes.
  • In-memory caches and :persistent_term values.
  • Network and deployment status pages.
  • Local build and artifact caches.

Immutable artifacts can live in durable object storage, but PostgreSQL records their identity, digest, lifecycle, and authorized target. Reconcile an ephemeral projection from durable state after restarts or missed events.

Trust boundaries

Every route belongs to one authority class. Gate 6 of the hardening plan owns the exhaustive route ledger and enforcement tests.

Class Principal Examples Required enforcement
Public Anonymous visitor Home, docs, status, changelog, allowed public code Read-only behavior, bounded output, and visibility policy
Authenticated Active OpenAgents user Chat, data rights, computers, issues, projects Signed session, CSRF protection for browser mutations, and owner or repository scope
Operator Configured active administrator Administration, promotion, deployment, recording review Authenticated session plus server-side operator authorization on every action
Computer Paired computer credential Controller socket, presence, and agent jobs Hashed or encrypted scoped token, explicit computer ownership, rotation, and revocation
Internal service Configured service identity or signed grant Inference proxy and build/deployment adapters Narrow audience and scope, expiry, replay defense, and no browser-held service secret
Git Computer or operator Git credential /git fetch and push HTTP Basic transport with server-side token verification and repository authorization

Route placement does not prove authorization. A controller, LiveView mount, or socket must enforce the class and resource scope at the server boundary. Until the Gate 6 route audit passes, treat the current router as implementation evidence rather than a complete authorization policy.

Provider boundary

Provider adapters implement replaceable server-side behavior. Direct OpenAI integration is the current default adapter choice for text, embeddings, shadow programs, and voice. It is not an application-wide contract.

The browser can receive bounded provider-derived events and media negotiation results, but it must never receive an OpenAI API key, GitHub access token, forge operator token, computer token, recording key, or internal-service signing key. Adapters own transport details, timeouts, bounded retries, response validation, error normalization, and secret redaction. Product contexts own durable lifecycle and policy.

Tests replace network providers with explicit fakes. A provider outage must produce a bounded durable failure outcome instead of abandoning an in-flight turn, work item, or voice session.

Durable account chat

OpenAgents.Chat.AccountTurns is the application entry point for account chat submissions from both /chat and POST /api/v1/chat/turns. It creates the run and first event in one transaction before it starts provider work. The browser and GET /api/v1/chat/events then project the same account-scoped journal. LiveView messages and PubSub notifications remain replaceable projections of that durable state.

Each run has a monotonically increasing event sequence. The journal preserves the order of user input, reasoning deltas, tool-call starts and outcomes, text deltas, and the terminal provider response or failure. This ordering lets an API client observe the same reasoning, tool, and response lifecycle that the browser renders. Conversation ownership comes from the authenticated account; client-supplied resource identifiers do not establish authority.

The terminal response_completed event and run record retain the provider's Responses output list. When the next turn constructs provider history, it replays that list in its original order instead of reducing it to assistant text or reconstructed function calls. This preserves provider item IDs, reasoning items, encrypted reasoning state, and the function-call continuity required by the stateless Responses API.

OpenAgents applies OpenAgents.Tools.Redaction before it stores provider completions or event payloads and before a tool outcome reaches a provider or client. The replay boundary therefore preserves the redacted provider output, not an unfiltered provider payload. Never log the journal or use telemetry as a second copy of message, reasoning, or tool content.

Shared text and voice tool admission

Text and voice are transports over one conversation authority boundary. OpenAgents.Tools.ConversationExecutionContext derives the owner, conversation scope, authorities, approval receipts, workspace, and registry snapshot for both surfaces. OpenAgents.Tools.AdmittedCatalog applies the shared selector, scope check, authority check, and module surface policy before it produces a provider catalog.

Text captures the registry and execution context for each Responses turn. Voice captures the same admitted catalog when the voice session starts and keeps that snapshot for the session. A surface can change presentation and the catalog format, but it cannot select a separate tool implementation or bypass the shared runner and policy checks. Add a conversation tool to the registry and its declared surface policy rather than wiring independent text and voice backends.

The shared catalog exposes read, write, and edit only when the host binds the conversation to an explicit, noncanonical repository or computer workspace. read limits each result to 2,000 lines or 50 KiB. write replaces a complete file, and edit applies an exact, nonoverlapping batch against an expected digest. Mutation tools require repository.write and an exact approval receipt. They serialize changes to each path, store restorable snapshots outside the workspace, and reject absolute paths, traversal, symbolic links, canonical Forge repositories, and application checkouts. The model and API request can select a relative file path, but they cannot supply the workspace root or mint mutation authority.

publish_changes is the only chat tool that turns those workspace mutations into a Forge ref. The host derives the repository, remote, and opaque openagents/chat/<run-id> branch from the authenticated workspace. The caller supplies only a commit message and, optionally, the workspace digest it observed. Publication requires repository.write, current repository membership, and an exact approval receipt. It never writes the default branch.

Each admitted tool call creates one repository_publications ledger row before Git or network effects begin. The row records the argument and workspace digests, expected previous branch object ID, published object ID, state, WAL sequence, and exact result. A retry with the same tool call ID returns the stored result. A retry with different arguments fails, and a branch that no longer matches the recorded lease fails without overwriting the new tip. Forge acceptance is complete only after the publication resolves the matching push receipt from the durable WAL.

Bearer clients use the same account chat entry point through a personal API token with chat:account scope. Forge mutations continue to require forge:write; one scope does not imply the other. The authenticated account, not the token name or request body, determines conversation ownership.

Untrusted Markdown boundary

Assistant and repository Markdown enters HTML through OpenAgents.Markdown.to_html/2 only. MDEx parses CommonMark with dangerous rendering disabled, Ammonia applies exact tag, attribute, and URL-scheme allowlists, and the application normalizes links before Phoenix marks the result safe. Independent input, syntax-tree nesting, and output limits produce a bounded escaped fallback instead of partial markup. The same function handles streaming completion and persisted text, so a completed message does not change when its durable projection replaces the stream.

Forge planes

The forge contains two separate planes within the OpenAgents.Forge application namespace:

  • The Git plane accepts authenticated Git traffic, stores repositories and push receipts, controls visibility, serves public browsing, and mirrors refs.
  • The deployment plane promotes an exact pushed SHA, builds immutable artifacts, selects a deployment strategy, changes fleet state, rolls back, converges restarted nodes, and records receipts.

A push never promotes itself. Git repository availability does not imply that a build is safe, and a successful build does not imply that a candidate is live. PostgreSQL transitions connect the planes through an operator-authorized target.

Deployment strategies

Direct BEAM load, relup, and rolling replacement solve different problems. The classifier must select one strategy for the complete candidate and fail closed when it cannot prove eligibility.

Strategy Eligible changes Required safety proof
Direct BEAM load Allowlisted module additions or changes without structural effects Immutable manifest and digest, fleet prepare/apply/verify transaction, exact binary rollback, and readiness verification
Relup Compatible versioned application changes and explicit process-state migrations Forward and reverse appup/relup chain, state migration tests, staged install, permanent-release verification, and rollback drill
Rolling replacement ERTS, OTP, NIF, dependency graph, assets, configuration, migrations, module deletion, or unclassified changes Immutable image digest, node drain, readiness-gated replacement, capacity limits, and last-known-good rollback

Development code reloading is not a production deployment strategy. Keep every deployment capability disabled by default until its local proof and isolated staging drill pass.

SCV boundary

An SCV is the durable coding-execution and supervision contract. It is not a container, model, OpenCode session, or tool catalog. The internal agent runtime deploys SCV runs and each run binds these parts:

  • A driver adapts one coding implementation, such as OpenCode or a native Elixir tool loop.
  • An environment supplies a digest-addressed image or owned host with declared language and system capabilities.
  • A runner starts and supervises the driver in that environment.
  • The SCV owns policy, lifecycle, budgets, events, cancellation, artifacts, receipts, and the handoff to Forge.

This boundary lets the runtime deploy several SCVs with different drivers and environments. Native coding tools belong to a native driver. OpenCode keeps its own protocol behind the same SCV policy and event boundary; OpenCode does not become the SCV or the environment.

The first qualification image combines the Elixir SCV worker release with the OpenCode driver and the opencode-core polyglot environment. Its staging process role accepts only read-only runs and starts no Phoenix endpoint, Repo, Forge service, or deployment coordinator. Forge remains the only deployment authority.

Runtime and staging topology

Production runs one OpenAgents release on a three-node BEAM fleet. PostgreSQL holds durable product and deployment authority. The Forge owns Git refs, promotion targets, build receipts, and deployment receipts. Boot convergence keeps a restarted node out of service until it matches the live target.

Qualification keeps two isolated staging lanes:

  • A web lane proves Phoenix, LiveView, PostgreSQL, authentication, chat, voice, data rights, and provider behavior without distributed deployment enabled.
  • A three-node distributed lane proves Ra quorum, Git, immutable builds, direct loading, relups, rolling replacement, rollback, and boot convergence.

Both lanes use staging-only hosts, credentials, buckets, repositories, service accounts, and a PostgreSQL instance that does not share a production failure domain. Use staging for browser, migration, configuration, distributed-cluster, and failure-injection evidence before a production promotion requires it.

OpenAgents.RuntimeConfig validates the complete behavior-changing settings boundary before migrations or traffic. The runtime configuration contract defines the safe feature profile, durable storage requirements, staging-gate admission, and content-free readiness report.

Source control authority

The self-hosted Forge is canonical. Contributors and deployment automation push accepted work to the owned Forge. MirrorWatch exports the accepted branch to GitHub and reports mirror lag separately from deployment state. A GitHub mirror update cannot promote a deployment, and a direct GitHub push does not become production authority.

Read ADR 0007 and the Forge hot loop runbook for the cutover decision and current operator procedure.

Decision records

Superseded narratives

The following documents record earlier plans and measurements. They do not define the current architecture:

  • docs/chat-inference-plan.md proposed a private Sarah service boundary that the integrated application does not use.
  • docs/sarah-integration-plan.md records the migration into this repository.
  • docs/2026-08-19-gap-implementation-plan.md records earlier gap work.
  • docs/beam-hot-deployment-plan.md contains detailed deployment design, but its phase status does not authorize production use.

The integration hardening and staging readiness plan tracks the work required to make this architecture safe and verifiable.