Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Architecture & Couplings

Ariel is a standalone service, a sibling to prospero and gonzalo, so that chat platform SDKs, credentials, and rate limiting stay behind one boundary (ADR 0002).

The shape

Everything in the diagram exists in code. The session bridge that would make a chat thread an agent session does not, and neither do the Slack and Teams backends; see Status & Roadmap.

Discord                                   (Slack / Teams: not written)
   │  Gateway socket (interactions in), REST (messages out)
   ▼
┌──────────────────────────────────────────────────────────┐
│ arield                                                   │
│  ChatProvider trait · notifier · command router ·        │
│  two-key authorization · renderer · prospero client ·    │
│  fleet watcher · gonzalo records client                  │
└──────────┬───────────────────────────────┬───────────────┘
           │ HTTP + SSE                    │ records
           ▼                               ▼
       prosperod                        gonzalod
  fleet snapshot, per-agent       people, bindings, roles,
  streams, spawn, kill, input     channel config, link
                                  tokens, audit

Couplings

  • Through prospero. All fleet control and fleet events go over prosperod's public HTTP and SSE API. Ariel takes no dependency on prospero crates: it mirrors the wire types it reads in ariel_core::prospero::types, and golden fixtures written from prospero v0.8.1 pin them, with v0.9's on_behalf_of on the event envelope pinned by its own wire tests (ADR 0005). Every enum Ariel matches on has an Unknown fallback, so a newer prosperod does not break decoding, and a field a newer prosperod adds is optional, so an older one still decodes. Ariel authenticates with a bearer token from ARIEL_PROSPERO_TOKEN_FILE, which prosperod requires from v0.8 with API authentication on (ADR 0013).
  • With gonzalo. Ariel stores nothing of its own. Identity, role grants, channel configuration, link tokens, and the audit trail are gonzalo records (ADR 0003), read and written through ariel_core::records. The record kinds are gonzalo's own, defined by its ADR 0022 and ADR 0023 and shipped from gonzalo 0.7.0 (gonzalo compatibility).
  • To caliban only through prospero. Ariel never talks to caliband.

Talking to prosperod

ProsperoClient (in ariel-core) maps prosperod's routes one to one:

MethodRouteClient call
GET/api/sessionsession() — who prosperod thinks Ariel is, and with which scopes
GET/api/fleetfleet()
POST/api/workspaces/{workspace}/agentsspawn()
POST/api/agents/{id}/killkill()
POST/api/agents/{id}/respawnrespawn()
POST/api/agents/{id}/inputinput()
POST/api/agents/{id}/end-inputend_input()
GET/api/agents/{id}/stream?from={seq}stream() (SSE)
GET/api/fleet/stream?from={cursor|now}fleet_stream() (SSE)

A base URL with a path prefix, as behind a reverse proxy, is kept. Requests time out after 30 seconds (the event stream excepted), and connecting after 5. Both http and https work: TLS goes through rustls and verifies against the platform's trust store, which the runtime image populates. In-cluster traffic to a Service stays plain http; https matters for reaching a prosperod through an ingress. arield calls session() at startup whenever ARIEL_PROSPERO_URL is set, so a refused or missing token is an error in the log rather than a surprise at the first command.

on_behalf_of(person) returns a clone of the client that adds prospero v0.9's X-Prospero-On-Behalf-Of header to every request it makes. Ariel holds one operate token for a whole chat workspace, so the token alone cannot tell one person's agents from another's; the person id travels beside it and comes back on the events those requests emit. A value prosperod would answer 400 to is dropped rather than failing the request it rode on — a gonzalo person id never is one, but a rejected header would fail the whole command.

FleetWatcher holds one connection to GET /api/fleet/stream, prospero v0.9's fleet-wide stream, and forwards every agent's events from it (#55). Each event's SSE id: is a fleet cursor — durable insertion order across all streams, which is the only total order a fleet has, since a per-agent seq cannot order two different agents. A dropped connection reconnects with ?from=<cursor>, which resumes strictly after it: no gap, no duplicate.

The first connection asks for ?from=now, so a restarted daemon announces nothing that happened while it was down (ADR 0011). The cursor lives in memory only — Ariel keeps no state of its own (ADR 0003) — so a restart begins at now again rather than resuming. Because the stream is read from prosperod's durable store rather than its live bus, there is no slow-consumer gap signal to handle, unlike the per-agent stream.

Before prospero v0.9 there was no such endpoint, and the watcher built the same feed by polling GET /api/fleet every 5 s and opening one SSE stream per agent. That cost a connection per agent, and it missed any agent that started and finished between two polls — the channel never heard about it at all. fleet() is still how /ariel status reads a snapshot; it is no longer how Ariel learns that something happened. stream() remains on the client, since it mirrors a route prosperod still serves, but nothing in Ariel listens to it.

Crates

CrateBinaryRole
ariel-core—Provider-neutral core: the ChatProvider trait and ConsoleProvider, the prospero client and fleet watcher, the renderer, the notifier, the command handlers, two-key authorization, account linking, channel parsing, and the gonzalo records client. Never depends on a chat platform SDK; CI checks this.
ariel-discord—Discord backend on twilight (ADR 0010).
ariel-daemonarieldThe long-running bridge: configuration, logging, the health endpoint, and the wiring that joins the fleet watcher, the per-channel notifiers and the command router.
ariel-cliarielThe operator CLI: ariel channel show/set and ariel link new (The ariel CLI).
ariel-e2e—Nothing ships from it: it holds the headless end-to-end smoke test, which runs the bridge against prosperod's and gonzalod's own server code. Kept apart so prospero and gonzalo-server stay out of every other crate's dependency tree, and excluded from the main CI gate, which runs it in its own job.

ariel-core's modules map onto those jobs: chat (the provider boundary), prospero, records (gonzalo), render, notify, commands, auth, link and channels.

Chat providers and features

Every platform sits behind one ChatProvider trait (ADR 0004, ADR 0006): a small required core (id, capabilities, register_commands, post, inbound) plus capability-gated methods (edit, direct_message, start_thread) that default to Unsupported. A Capabilities descriptor, including message size limits and a per-channel send budget, tells the core which it may call.

Each backend is its own crate, consumed by ariel-daemon as an optional dependency behind a feature of the same name:

CrateFeatureDefaultNotes
ariel-daemondiscordyesCompiles in ariel-discord.
ariel-corecontract-testsnoExposes the shared provider contract suite for backends' tests.

Features are additive: they decide which providers can exist in a build, and configuration decides which one runs — today, whether the Discord token, guild ID and application ID are all set. arield logs the providers compiled into it at startup, on the arield starting line. cargo build -p ariel-daemon --no-default-features builds with no chat backend, and CI keeps that build green.

Rendering and notifications

Ariel's notification design is one live message per agent, edited in place, with bursts of five or more spawns collapsed into one summary message, and sends paced per channel in the core (ADR 0007). Both halves exist.

AgentView folds agent_spawned, status_changed, agent_finished, and agent_gone events into an agent's current state and reports whether its message changed; render_agent and render_summary turn that state into provider-neutral messages with a severity, fields for the start time, who started it, the end time, outcome, cost and turns, and an optional dashboard link. The attribution belongs to the spawn: a later event carrying someone else — a kill, say — does not rewrite who started the agent.

Notifier then holds one per channel. It decides for itself whether it follows an event's workspace and whether the channel's notify preset wants that event kind (ADR 0009), keeps the live message's id so a change is an edit rather than a new post, collapses a burst, and paces its sends against the provider's advertised SendBudget. A rate limit, a lost permission or a message somebody deleted is handled rather than fatal. arield starts one notifier per configured channel and feeds every fleet event to all of them, so adding a channel is a record, not a code change.

What arrives on an event is the gonzalo person id Ariel asserted, not a display name: ids are stable and unique, names change and collide. So the notifier resolves it before the channel sees it, reading that person's record once and remembering the answer. A failed or unknown lookup is not cached — the person may be created later — and the id is shown as it came rather than dropped, which still tells two people's agents apart.

Commands and authorization

A command arrives as an Inbound::Command from the provider and is answered by ariel_core::commands. Two keys decide what it may do: the role the person holds, from their gonzalo grants, and the ceiling on the channel's configuration record. The command runs at the lower of the two (ADR 0009, ADR 0012), and every command that changes something leaves exactly one audit entry in gonzalo, Denied or Succeeded/Failed. An unlinked chat account gets nothing but instructions for linking, and an unconfigured channel is refused before prosperod is asked anything, so it cannot be used to discover which agents exist. The details, and the table of who may run what, are in Chat Commands.

An allowed decision carries the person it is for, and spawn, kill and respawn run against a client clone that names them to prosperod, so the fleet events a command causes are attributed to the person rather than only to Ariel's token (Who started an agent). Prosperod does not verify the claim — it cannot authenticate someone else's user — so the token stays the authenticated identity and a false claim stays attributable to the credential that made it.

/ariel link is the one command that works in any channel: it redeems a one-time token minted by ariel link new or /ariel invite, binds the chat account to a person and grants the token's role. Only a hash of the token is stored, and it is marked used rather than deleted, so a sync between gonzalo stores cannot bring it back.

Security and deployment

ADR 0008 sets the deployment model: credentials only as mounted Secret files, a single-replica Deployment with no ingress (Discord events arrive over an outbound Gateway connection), and a read-only root filesystem. ADR 0013 revisits its assumption that prosperod is unauthenticated: from prospero v0.8 Ariel presents a scoped API token, so the NetworkPolicy fencing prosperod is defence in depth rather than the only control.

The file-based credentials, health endpoint, logging and image are implemented. The cluster rollout is the ariel chart in caliban-ai/helm-charts, which tracks the published image.