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'son_behalf_ofon the event envelope pinned by its own wire tests (ADR 0005). Every enum Ariel matches on has anUnknownfallback, 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 fromARIEL_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:
| Method | Route | Client call |
|---|---|---|
GET | /api/session | session() — who prosperod thinks Ariel is, and with which scopes |
GET | /api/fleet | fleet() |
POST | /api/workspaces/{workspace}/agents | spawn() |
POST | /api/agents/{id}/kill | kill() |
POST | /api/agents/{id}/respawn | respawn() |
POST | /api/agents/{id}/input | input() |
POST | /api/agents/{id}/end-input | end_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
| Crate | Binary | Role |
|---|---|---|
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-daemon | arield | The 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-cli | ariel | The 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:
| Crate | Feature | Default | Notes |
|---|---|---|---|
ariel-daemon | discord | yes | Compiles in ariel-discord. |
ariel-core | contract-tests | no | Exposes 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.