Architecture overview
Status: design draft for review · decided, not built
Open Memory Lab (OML) is an external memory provider for AI agents and crews. Every memory asserts a claim; every claim is held on arrival, scanned, reviewed by an identity that did not write it, and only then visible to others. This page is the map: the system in its context, the containers inside the Kubernetes boundary, the module structure inside every container, the five runtime scenarios that exercise them, and where each constitutional rule is enforced. Contracts live on the pages that own them — this page links, it does not restate. The constitution itself (rules R1–R4 and their one bounded exception) is declared in ADR-0001.
1. System context (C4 level 1)
Three kinds of actor touch the deployment, all through the same versioned HTTP API (api/http-api.md). None of them runs inside the cluster; the provider runs no model itself — review capacity is external crews holding dreamer keys (admission).
flowchart LR
subgraph outside ["Outside the deployment"]
AG["Agent crews<br/>(writer / reader keys)"]
RC["External reviewer crews<br/>(dreamer keys)"]
AD["Tenant admins and the<br/>provider operator"]
end
subgraph k8s ["Kubernetes boundary"]
OML["Open Memory Lab<br/>agentic external memory provider"]
end
AG -- "MCP plugin over streamable HTTP:<br/>remember, recall, learn, attach …" --> OML
RC -- "HTTP API:<br/>review next, verdict, ratify edges" --> OML
AD -- "oml CLI → HTTP API:<br/>keys, workareas, status, simulations,<br/>erasure, evidence export" --> OML
- Agent crews call the agent-safe MCP tool subset, a facade over the JSON HTTP API. The surface they can call never promotes, signs, demotes or deletes (R1). Tool list and transport: api/mcp-plugin.md.
- Reviewer crews are externally managed; they hold dreamer keys and work the review ceremony over HTTP. Who may review what: roles-and-keys.md; how a verdict is bound to what was served: cli/reference.md and admission.md.
- Admins and the operator use the
omlCLI, which speaks the same HTTP API with admin/operator keys. There is no web UI (§6). CLI verbs: cli/reference.md.
Tenancy is derived server-side from the API key, never from the request; everything below is per-tenant (security.md).
2. Containers (C4 level 2)
Four Rust binaries, one database, one object store, one lakehouse. The deciding asymmetry is key custody: only oml-server loads the deployment signing key; lake-writer, reconciler and indexer run under their own credentials and can neither sign journal events nor (for the reconciler) decrypt bodies. Custody and commit-point rules are owned by storage.md; the privileged-service threat model by security.md; workload shapes and the two Helm profiles by operations/deployment.md.
flowchart TB
CL["Agents / reviewer crews / CLI"]
subgraph k8s ["Kubernetes boundary"]
S["oml-server<br/>axum, N stateless replicas<br/>loads the signing key<br/>holds DEKs on the serving path"]
LW["oml-lakewriter<br/>singleton, leader lease<br/>no signing key · keyless, moves ciphertext verbatim"]
RE["oml-reconciler<br/>CronJob, read-only<br/>no signing key · no decrypt — keyless"]
IX["oml-indexer<br/>embeddings per tenant<br/>no signing key"]
PG[("PostgreSQL<br/>signed per-tenant journal chains,<br/>metadata, outbox, serving indexes<br/>— the system of record")]
OS[("Object store (S3/MinIO)<br/>quarantine + content-addressed<br/>ciphertext bodies")]
DL[("Delta lake<br/>bronze / silver / gold<br/>— derived, never serves a read")]
end
CL --> S
S --> PG
S --> OS
LW -- "drains outbox in seq order" --> PG
LW -- "materialize / retract,<br/>exactly-once per txn id" --> DL
RE -- "incremental fold vs journal" --> PG
RE -- "ciphertext-digest compare" --> DL
IX --> PG
| Container | Role | Signing key | Owning page |
|---|---|---|---|
| oml-server | The only API surface; the single transactional commit point is the Postgres transaction it runs | yes — every replica, same deployment key | storage.md |
| oml-lakewriter | Sole outbox drain; lake writes and all lake maintenance | no | storage.md |
| oml-reconciler | Read-only journal-vs-lake comparison; repairs re-emitted via oml-server | no | reconciliation.md |
| oml-indexer | Computes embeddings for the serving tier | no | retrieval.md |
| PostgreSQL | System of record: journal, metadata, outbox, FTS + vector serving tier | — | storage.md, data/postgres-schema.md |
| Object store | Quarantine bucket and ciphertext bodies, content-addressed by plaintext digest | — | storage.md |
| Delta lake | Derived body lakehouse for reconciliation, lineage, analytics, future exports | — | storage.md, data/delta-tables.md |
A compromised replica is treated as key compromise; the signature attests the deployment, not a unique process (storage.md).
3. Module structure
Inside each of the four service binaries the code has one shape, decided in
ADR-0019: a hexagon. A pure domain core
(oml-domain) holds the admission state machine, the workarea algebra, the role/verb ceilings,
the status ladder and the taint predicate — every constitution invariant, and no I/O. Around it,
the application layer (oml-app) declares ports as Rust traits. On the driven side, everything
that touches the world — store, lake, object store, KMS, plugins — is an adapter implementing one
of those traits; on the driving side, the HTTP /v1 transport and the MCP facade are adapters
the other way around: they depend on oml-app and call its use-cases, the application's inbound
port. The four binaries are thin compositions of the same core with the adapter set each job
needs; the oml CLI reaches the same hexagon only through the HTTP transport
(ADR-0015). The crate list is committed in
ADR-0006.
flowchart LR
C["oml CLI<br/>an HTTP client — drives the system<br/>only through /v1 (ADR-0015)"]
subgraph driving ["Driving adapters — depend on oml-app, call its use-cases"]
H["HTTP /v1 transport"]
M["MCP facade"]
end
subgraph hex ["The hexagon — knows no adapter"]
APP["oml-app<br/>use-cases (the driving port) +<br/>driven ports as Rust traits:<br/>store · lake · object store · KMS ·<br/>scanner · embedder · exporter · clock/id"]
DOM["oml-domain<br/>pure core — every<br/>constitution invariant,<br/>no I/O"]
APP -- "depends on" --> DOM
end
subgraph driven ["Driven adapters — depend on oml-app, implement its port traits"]
PGA["Postgres<br/>(sqlx)"]
LKA["Delta lake<br/>(delta-rs)"]
OSA["Object store<br/>(object_store)"]
KMA["KMS"]
PL["Plugins:<br/>scanners · embedders · exporters"]
end
C -. "HTTP /v1" .-> H
H -- "calls use-cases" --> APP
M -- "calls use-cases" --> APP
APP -- "store port" --> PGA
APP -- "lake port" --> LKA
APP -- "object-store port" --> OSA
APP -- "KMS port" --> KMA
APP -- "scanner · embedder ·<br/>exporter ports" --> PL
Arrows out of the hexagon are runtime calls crossing a named port; the compile-time dependency
runs the other way — each driven adapter implements a trait declared in oml-app, so every
dependency in this picture points at the hexagon, and the hexagon knows no adapter.
Plugins exist only at the scanner, embedder and exporter ports; in v1 they are compiled-in Rust implementations selected by journalled configuration. A plugin can narrow, never widen — it cannot mint verbs, can move admission only toward holding, never sits on the signing or erasure path, and its identity and version ride the journal events its work informs (ADR-0019).
4. Runtime scenarios
Five sequences cover the moving parts. Each diagram names the page that owns its contract; the diagrams are illustrations, not specifications.
4.1 A write — remember → held → scan → ack on Postgres commit
Owned by admission.md (pipeline) and storage.md (commit point).
sequenceDiagram
participant A as Agent (writer key)
participant S as oml-server
participant Q as Quarantine (object store)
participant P as Postgres
A->>S: remember (body + attachment digests, idempotency key)
S->>Q: quarantine body (service-only credentials, never the lake)
S->>S: deterministic scan — same call, can veto
S->>P: ONE transaction: signed journal event (held),<br/>metadata rows, outbox row
P-->>S: commit
S-->>A: ack — durable iff this commit#59; state = held,<br/>invisible to everyone but the author
The ack means exactly one thing: the journal event, the metadata and the outbox row committed together. A held body never reaches the lake.
4.2 A review — serve → verdict → ratify → outbox → lake
Owned by admission.md (review policy, promotion checks) and storage.md (outbox protocol). The serve binding and CLI ceremony: cli/reference.md.
sequenceDiagram
participant R as Reviewer crew (dreamer key)
participant S as oml-server
participant P as Postgres
participant LW as oml-lakewriter
participant L as Delta lake
R->>S: review next
S-->>R: one held item + ALL attachments,<br/>under an expiring serve_id
R->>S: verdict under serve_id
S->>S: promotion checks: reviewer absent from<br/>author AND lineage closure (R2)#59; policy snapshot
S->>P: ONE transaction: ratification event,<br/>state → promoted, outbox row
S-->>R: verdict recorded against served digests
LW->>P: drain outbox in strict seq order
LW->>L: materialize promoted claim into silver,<br/>exactly-once per Delta txn id
4.3 A read — served from Postgres, logged
Owned by retrieval.md. Reads never touch the lake (§6).
sequenceDiagram
participant A as Agent (reader key)
participant S as oml-server
participant P as Postgres
A->>S: recall (query, workarea scope)
S->>P: FTS + vector over promoted rows —<br/>RLS, grant scope, simulation predicate
S->>P: read event appended (retention-bounded log,<br/>batch digests anchored into the journal)
S-->>A: steering rows first (never ranked), then hits#59;<br/>bodies decrypt on the serving path only
4.4 A freeze — validating a workarea needs a second ratifier
Owned by workareas.md (status ladder, transition matrix).
sequenceDiagram
participant P1 as Proposer (admin or dreamer)
participant P2 as Ratifier (second dreamer/admin, ≠ proposer)
participant S as oml-server
participant PG as Postgres
P1->>S: propose committed → validated
S->>S: refuse unless every child is already validated
S->>PG: journal the proposal
P2->>S: ratify the transition
S->>PG: journal transition + policy snapshot
Note over S,PG: the workarea now refuses new writes,<br/>promotions into it and reassignment into it#59;<br/>demote, erasure and crew-metadata update remain
Transitions are forward-only; there is no unfreeze (workareas.md).
4.5 A simulation — a fenced provenance channel
Owned by simulations.md.
sequenceDiagram
participant AD as Admin
participant W as Agent in the audience
participant D as Dreamer in the audience
participant S as oml-server
AD->>S: create simulation (audience = journalled list of keys)
W->>S: remember — carries indelible provenance: simulation<br/>+ simulation_id in event, row and lake row
D->>S: verdict — only dreamers in the audience may review#59;<br/>no dreamer in the audience ⇒ nothing promotes
Note over S: served only to audience keys, on every read path#59;<br/>never enters a validated workarea#59;<br/>taint propagates through the lineage closure
AD->>S: end simulation — journalled act demotes its rows,<br/>tainted claims are demoted or re-reviewed
5. The invariant map
Where each constitutional rule is mechanically enforced. The rules are declared in ADR-0001; each enforcement point is specified on its owning page.
| Rule | Statement | Enforced at | Owning pages |
|---|---|---|---|
| R1 | The surface an agent can call never promotes, signs, demotes or deletes — verb ceiling per role | The API surface itself: the MCP tool list omits the verbs; every HTTP endpoint checks the key's role; key issuance carries an issuer ceiling | roles-and-keys.md, api/mcp-plugin.md |
| R2 | The reviewer is never the author | Promotion time, in oml-server: the reviewer's key and principal are checked against the author and the row's lineage closure (pending and ratified edges), not just at key issuance; R2 extends to workarea edges (ratifier ≠ proposer) | admission.md, workareas.md |
| R3 | The gate changes visibility, not ranking — nothing is readable by others until ratified | The serving tier: held rows are excluded from every read path (FTS, vector, fetch, graph); steering rows precede hits but are never ranked | admission.md, retrieval.md |
| R4 | No silent deletion — erasure is a journalled act via crypto-shredding; demotion keeps bytes | The journal table (append-only: REVOKE UPDATE/DELETE plus a guard trigger) and the erasure path (journalled erase event + DEK destruction, with a named copy-class inventory) | storage.md, audit-and-lineage.md |
One bounded exception exists and is named in the constitution, not discovered later: a
development workarea may, by journalled tenant policy, relax R2/R3 for rows marked
self_signed, which are readable only inside that workarea and never promote beyond it
(workareas.md, ADR-0001). One honesty limit sits beside the
invariants: undeclared derivation — a writer omitting derives_from — is not detectable by the
service; lineage exclusion is exactly as good as the declared graph.
6. What is deliberately not here
- No web UI. Administration is the
omlCLI against the same HTTP API; a web console was considered and declined (cli/reference.md, ADR-0015). - No grounded Q&A. Clients are agents that recall and reason themselves; OML returns memories, not answers. Out of scope for v1, recorded in ADR-0013 and retrieval.md.
- Reads are never served from the lake. The Delta lake is derived — reconciliation, lineage, metadata analytics, future journalled exports — and serves no client read; the serving tier is Postgres (retrieval.md, storage.md).
- The provider runs no model. Scanning is deterministic; review is external crews holding dreamer keys (admission.md).
- No cross-tenant federation yet. The "whole world" claim mapping is explicitly future — an RS row with its trigger in the roadmap.
Everything on this page is a design commitment with a tracked exit criterion, not a property of running software; open measurements (review throughput, pgvector recall and latency, the delta-rs exactly-once conformance test) are FR rows in the roadmap.