Architecture Overview
x1agent runs LLM agents in Kubernetes pods with a sidecar-based security model. Each agent session is a short-lived Job. Long-running infrastructure (API, NATS, Postgres, provider services) runs as standard Deployments.
Session pod
Section titled “Session pod”Every agent session runs as a 2-container Kubernetes Job:
graph TB
subgraph pod["K8s Job Pod"]
agent["Agent Container<br/>:3100 SSE stream<br/>:8788 message injection"]
sidecar["Sidecar Container (Rust)<br/>:9090 internal API"]
vol[("/workspace")]
end
agent -- "localhost" --- sidecar
agent -.- vol
sidecar -.- vol
Agent container — Runs the LLM runtime (Claude Agent SDK or a custom runtime). Exposes an SSE stream on :3100 for event output and an inject endpoint on :8788 for user message input. Receives zero secrets. Talks only to the sidecar on localhost.
Sidecar container — Rust (Axum + async-nats). Bridges the agent to NATS, enforces permissions, manages the workspace volume, proxies credential-bearing API calls, and logs all operations. This is the trust boundary.
Shared resources — Localhost network within the pod. A /workspace volume for agent file I/O.
Pod security context: runAsNonRoot, seccompProfile: RuntimeDefault, all capabilities dropped, resource limits enforced, activeDeadlineSeconds for hard session timeout.
Communication paths
Section titled “Communication paths”Three paths handle all data flow between agents, clients, and the platform:
sequenceDiagram
participant B as Browser
participant Api as api / ws-bridge
participant N as NATS
participant S as Sidecar
participant A as Agent
Note over A,S: Path 1: Passive observation
A->>S: SSE stream (:3100)
S->>N: publish x1.session.{id}.events
N->>Api: subscription
Api->>B: relay (WebSocket /api/ws)
Note over B,A: Path 2: User input
B->>Api: WebSocket pub_input
Api->>N: JetStream publish x1.session.{id}.input
N->>S: subscription
S->>A: POST :8788/inject
Note over A,B: Path 3: Proactive emission
A->>S: MCP tool call (emit_status, emit_artifact, etc.)
S->>N: publish x1.session.{id}.events
N->>Api: subscription
Api->>B: relay (WebSocket /api/ws)
The browser never talks to NATS directly. The api hosts a WebSocket bridge at wss://api.<base-domain>/api/ws that authenticates the upgrade, authorizes each subscribe against resolveSessionVisibility, and relays a whitelisted subset of NATS subjects in both directions. NATS is ClusterIP-only and reachable only by the api, providers, and session pods (enforced by nats-networkpolicy.yaml). See NATS mTLS for the bridge’s wire protocol + the field-level whitelist.
Path 1 (passive observation) — The agent runtime produces a stream of typed events (thinking, text, tool calls, results). The sidecar consumes this SSE stream, wraps each event in the X1Message envelope, and publishes to NATS. The bridge subscribes server-side and relays each filtered event to the browser over its authenticated WebSocket.
Path 2 (user input) — The browser sends pub_input over its WebSocket. The bridge validates ownership of the session, then JetStream-publishes to the session’s input subject. The sidecar subscribes, validates the message, and POSTs to the agent’s inject endpoint. The agent runtime feeds this into the conversation as a new user turn.
Path 3 (proactive emission) — The agent calls MCP tools (emit_status, emit_artifact, request_input, request_permission) that produce structured events. The sidecar publishes these to NATS. This gives the LLM deliberate control over what it communicates, with typed payloads rather than parsed stdout.
System components
Section titled “System components”| Component | Type | Purpose |
|---|---|---|
| API server | Deployment | REST API. Session orchestration, auth, workspace management. |
| NATS | Deployment | Event bus. Session events, user input, provider communication. |
| PostgreSQL | StatefulSet | Relational state. Users, agents, sessions, workspaces. |
| Frontend | Deployment | Astro + React SPA. Agent management, session viewer, admin. |
| Provider services | Deployments | Pluggable NATS-subscribed integrations. Today: graph-surrealdb (graph + vector), google-workspace (files, docs, sheets, calendar, email), messaging-slack, preview. |
| Session pods | Jobs (dynamic) | One per active session. Agent + sidecar, short-lived. |
| Job watcher | In-process loop in api | Polls pending sessions and creates Kubernetes Jobs. (No CRDs / operator today; see proposals/operator.md if and when one lands.) |
NATS subject conventions
Section titled “NATS subject conventions”All session messages use a standard subject hierarchy:
x1.session.{session_id}.events -- sidecar publishes, clients subscribe (X1Message envelope)x1.session.{session_id}.input -- clients publish, sidecar subscribes (user input)x1.session.{session_id}.audit -- sidecar publishes, api persists (privileged HTTP audit log)x1.session.{session_id}.presence -- browser publishes, sidecar tracks (keepalive)
x1.provider.{domain}.* -- provider request/reply (graph, vector, files, docs, sheets, calendar, email, preview)
x1.image.build -- image-builder consumes for in-cluster Kaniko buildsAgent runtimes
Section titled “Agent runtimes”The platform treats agent runtimes as pluggable. A runtime must expose two HTTP interfaces from the agent container:
| Endpoint | Purpose |
|---|---|
GET :3100/stream | SSE stream of agent events |
POST :8788/inject | Accept user messages mid-session |
Built-in runtimes:
- claude_code — Claude Agent SDK (TypeScript). Multi-turn via
streamInput(). MCP servers for tools and proactive emission. This is the only runtime accepted byagents.runtime_typetoday (seepackages/domains/agents/src/domain/runtime.ts).
Other runtimes (Codex, Gemini, opencode, Pi) are documented under architecture/runtimes/ as forward-looking shapes; adding one requires extending RUNTIME_TYPES and shipping a runtime image. Custom runtimes will be supported by implementing the two HTTP endpoints in any language.