
Daevix is split into two kinds of component. The split exists for **data
sovereignty**: any operation on sovereign data happens inside an enclave and
never transits the controlplane.

```
                    ┌──────────────────────────────┐
                    │         controlplane         │   central management plane
                    │  identity · policy · audit   │   (SaaS or self-hosted)
                    │        · operator API        │   never sees sovereign data
                    └──────────────┬───────────────┘
                                   │  metadata / control only
                                   │  (never sovereign payload)
              ┌────────────────────┴────────────────────┐
              ▼                                          ▼
   ┌───────────────────────┐                 ┌───────────────────────┐
   │   enclave (eu-sov-1)   │                 │   enclave (us-sov-1)   │   sovereign data plane
   │  K8s + Kata + Cilium   │                 │  K8s + Kata + Cilium   │   in the customer boundary
   │  ┌──────────────────┐  │                 │  ┌──────────────────┐  │
   │  │  agent container │  │                 │  │  agent container │  │
   │  │   └─ mounts ─┐   │  │                 │  │        ...        │  │
   │  └────────────┼─────┘  │                 │  └──────────────────┘  │
   │        IMDS unix sock  │                 │                        │
   └───────────────────────┘                 └───────────────────────┘
```

## Components

### controlplane (`dvx serve controlplane`)

Central management: identity, policy, audit aggregation, operator API.
Deployable as SaaS or self-hosted. Owns its **own** database, which **never**
holds sovereign data.

Package: `internal/controlplane`. It must not import `internal/enclave`.

### enclave (deployment unit)

The sovereign data plane — a *deployment unit*, not a single process: a
collection of Daevix-owned processes co-located inside the customer's
sovereignty boundary, a Kubernetes cluster using **Kata containers** (VM-isolated
pods) and **Cilium** (identity-aware network policy). Every operation on
sovereign data happens here, and each enclave owns its **own** database holding
only its tenant's data. Its members are the **broker** (the control node and
sole controlplane egress), the **imds**, the **network gateway**, and the
**llmproxy** — described below and in [`enclave-broker.md`](enclave-broker.md).

Package namespace: `internal/enclave/**`. Nothing under it may import
`internal/controlplane` (lint-enforced).

**Deployment shapes = placement drivers.** The K8s cluster (Kata + Cilium) is
the first shape; the **local micro-VM shape** (mission bd_2_0-cwnz M3,
`internal/enclave/vmdriver`) runs the SAME satellites as bare host processes
on the operator's own machine, with each AGENT as its own cloud-hypervisor
micro-VM: rootfs derived from the agent's container image (per-agent qcow2
overlay), identity delivered over a per-agent vsock channel bridged to
localhost by the injected static init (`cmd/dvx-init`), a file-backed
AgentMapping registry under the enclave state dir, and a `vm`-only isolation
class. Agent authoring is driver-agnostic (same image + command on both
shapes); one enclave runs exactly one driver; the K8s and VM drivers are
verified against one conformance suite (`internal/enclave/cpbroker/driver/drivertest`).

### broker (`dvx serve broker`)

The enclave's central control node and its **sole controlplane egress**
([`enclave-broker.md`](enclave-broker.md)). The broker core
(`internal/enclave/cpbroker`) owns the level-triggered **desired-vs-actual
loop** that turns the operator's desired agent set into running workloads and
garbage-collects withdrawn ones; the platform touchpoints it drives sit behind
the **driver seam** (`internal/enclave/cpbroker/driver`): small interfaces — a
**Provisioner** (Ensure/Delete/State/List verbs), an **ArtifactPublisher**
(enclave-scoped credential + policy delivery), an **IsolationGate**
(fail-closed capability check, ADR 046), and an **Attributor** (runtime → agent
identity) — assembled into one driver value and built **per reconcile pass** by
a factory (so rotated platform credentials are picked up). **Kubernetes is the
first driver implementation** (`k8sDriver` over the stdlib-only `k8sWriter`); a
local micro-VM driver is planned on the same seam. The seam crosses no K8s
types: the driver receives a workload-neutral struct and builds its own
platform objects. The Provisioner's **State** verb makes runtime drift visible
(a CrashLoopBackOff pod or an externally-deleted one is reported in broker logs
and the operator status API, never silently invisible). The local-issuer path
stays off the seam (issuance is driver-invariant; only delivery varies). One
driver per enclave; the AgentMapping registry is broker-core-owned, not a seam
surface. **Every driver implementation passes the interface-level conformance
suite** (`internal/enclave/cpbroker/driver/drivertest`, bead decision 8; the
`drivertest.Harness` + `MemDriver` reference fake precedent
`kmstest.MemProvider`): Ensure idempotence, two-phase teardown with terminating
guards, GC List shape, observe/State verb semantics, and per-agent home
scoping, run against every driver (fake + K8s today; the VM driver joins in M3)
so these semantics cannot drift per driver.

**Driver-neutral placement + one driver per enclave, enforced** (`bd_2_0-cwnz`,
cwnz.5): every AgentMapping carries a typed `Placement{Driver, ID}` plus
per-driver metadata instead of K8s-specific node/namespace fields. The broker
never authors a mapping whose `Placement.Driver` differs from the enclave's
configured driver - a mismatched Put is refused loudly (fail-closed, logged as
an error, never applied), and the K8s driver likewise refuses `EnsureRuntime`
for a foreign-driver placement. The mapping's `Placement.ID` is the node the
broker pinned at creation (RuntimeClass-constraint-filtered; see
`internal/enclave/registry/provider.go` and the `drivertest` conformance suite).

### imds (`dvx serve imds`)

The node-local IMDS daemon: an enclave satellite that hosts the only interface an
in-enclave agent talks to. Modeled on a cloud Instance Metadata Service, it
listens on a **unix domain socket** mounted into the agent's container and serves
the agent its identity, short-lived tokens, and credentials. Agents never reach
the controlplane and never reach a remote enclave.

**Placement matching is fail-closed** (`bd_2_0-cwnz`, cwnz.5): the daemon
serves only mappings whose `Placement` names the driver it implements AND whose
`Placement.ID` positively matches this instance (K8s: the node name). Empty or
mismatched placement fields serve **nothing** - the old
serve-everywhere-on-empty semantics are gone. A mapping matching no local
placement is surfaced by a **rate-limited** warn (once per 10m per mapping), so
broker-authored vs node-served drift is observable without spamming every
reconcile poll; the signal never widens the match.

Package: `internal/enclave/imdsnode` (the daemon), which serves the metadata
service implemented in `internal/enclave/imds`. It must not import
`internal/controlplane`.

### relay (`dvx serve relay`)

The enclave's **edge ingress** and single publicly-exposable surface
(`bd_2_0-ib4.1`). The broker is dial-out-only and in-cluster-private, so an
external operator cannot reach its operator API (secret authoring, audit query)
directly. The relay terminates the external operator connection and
reverse-proxies an **allow-listed** subset of the broker's operator API to it —
so the broker never has to be exposed. The proxy is default-deny: only the
explicitly listed method+path routes are forwarded; everything else is refused at
the relay and never touches the broker.

Two trust boundaries meet here. The **operator-facing side is server-TLS only**:
the operator presents a Zitadel bearer over TLS (no client cert). The relay
**independently validates that bearer at the edge** (defence in depth — it rejects
unauthenticated/expired requests before they reach the private broker) and then
forwards it untouched; the broker **re-validates and enforces org scope**, so it
stays the authorization authority. The **relay→broker hop is mTLS**: the relay
presents a broker-minted client identity (`dvx-relay-broker-client`) and the
broker's operator listener *requires* it, so the relay is the only caller that can
reach it — the broker stays private (mutual-TLS gate, plus a `CiliumNetworkPolicy`
that admits port 8451 only from the relay). The reverse-proxy core is deliberately
generic: the future WS agent-terminal forward and WireGuard relay to agents
(`bd_2_0-kzb`) slot in as further allow-listed routes.

Package: `internal/enclave/relay`. It must not import `internal/controlplane`.

### LLM inference gateway (llmproxy) (`dvx serve llmproxy`)

A node-local **enclave satellite** that governs and forwards agents' LLM
inference calls (epic `bd_2_0-b06`). It is a sovereign-plane process: it sees the
agent's **plaintext prompt bytes** (the customer's most sensitive data) but, like
every enclave component, **never dials the controlplane** (invariants 1/2). Its
only ingress is the network gateway, over the agent's attested SVID.

The datapath has two stages:

1. **Classification + force-route (network gateway).** The gateway already
   MITM-terminates every redirected egress. When a decrypted call is recognized as
   an LLM inference request — a known provider host (`api.anthropic.com`,
   `api.openai.com`, `api.factory.ai`, `inference.hetzner.com`; the host→provider
   set is the shared `internal/enclave/llmreg` registry) *and* an exact inference
   method+path — the gateway **force-routes** it to llmproxy instead of the real
   upstream (`bd_2_0-b06.1`).
   Classification runs **only after destination policy has already allowed the
   flow**, and is **fail-closed** (`bd_2_0-b06`, decision B): an unattributed
   agent, an agent with no held SVID, or an unreachable llmproxy is **denied**
   (`mediation:llm-route`), never sent direct. Non-inference calls to the same host
   (token counting, model list) pass through unchanged.
2. **Verify + forward (llmproxy).** The gateway dials llmproxy over **mTLS
   presenting the agent's own attested SVID** (reusing the assert-identity tunnel;
   the signing key stays in the gateway, *outside* the agent VM — agent-identity
   invariant 6). llmproxy **verifies** that peer cert into agent/org/enclave
   (`agentid.Verifier`, the echoverify pattern) and trusts **nothing** in the HTTP
   bytes. The gateway conveys the observed original destination **and its verified
   SNI** out-of-band as a **PROXY-v2 preamble** (`proxyproto.EncodeV2` → `Parse`;
   the SNI rides a custom TLV, `bd_2_0-izh`), so a lying `Host` header cannot
   redirect egress. llmproxy resolves the **provider from the attested SNI**
   (cross-checked against the agent's `Host` — a mismatch fails closed), maps
   provider→canonical upstream via the shared `internal/enclave/llmreg` registry
   (optional governance override, operator startup default; `https`-only), and uses
   the request path only to detect the wire **format** (never the provider — a path
   cannot tell apart two providers that share a wire, e.g. Hetzner serves OpenAI's
   `/v1/...` paths). It then applies governance (credential mode, model
   allow/blocklist) and forwards the request byte-faithfully — including SSE
   responses, streamed per-chunk (`bd_2_0-b06.3/.4/.5/.6`). This byte-faithful
   passthrough is the **default** mode only: when the cross-format translation
   flag (`--translate` / `DVX_TRANSLATE`) is enabled, a request whose client
   wire format differs from the upstream's expected format is translated
   (request, response, and SSE stream) instead of forwarded byte-faithfully —
   see `docs/threat-model.md` ("Cross-format translation") for the documented
   shift this makes to the passthrough property.

**Direct egress breaks the classify→route→classify loop (decision C).** llmproxy
egresses **directly** to the real provider, *not* back through the gateway. This
is deliberate: llmproxy's own egress attributes to **llmproxy**, not to an agent,
so the gateway would have nothing to attribute and no reason to re-classify it —
routing it back would loop. The trade-off is a **second enclave egress path**
distinct from the agent gateway, governed by llmproxy's own upstream-resolution +
model policy rather than the gateway's destination policy; the threat model tracks
this as a residual (`docs/threat-model.md` §4.1).

Package: `internal/enclave/llmproxy`. It must not import `internal/controlplane`
(the enclave import fence in `.golangci.yml` covers all of
`internal/enclave/**`, so no llmproxy-specific fence is needed).

### Agent identity

Agents are arbitrary, untrusted code, so they never hold a credential and never
authenticate. The platform **attributes** every action to an agent from signals
below the agent (the per-agent socket mount, Cilium identity, the Kata TEE
measurement) and forces all egress through a TLS-terminating gateway. See
[`agent-identity.md`](agent-identity.md) for the full model.

## Invariants

1. **Sovereign data never transits the controlplane.** Operations on sovereign
   data go directly to an enclave.
2. **Sovereign data never goes to the controlplane; the enclave never depends
   on it to serve the data plane.** This is a rule about *data*, not about who
   dials whom. An enclave is free to *initiate* a connection to the controlplane
   — hold an outbound SSE channel, poll, pull — and that is the normal, expected
   shape (it lets a NAT'd customer-cluster enclave receive nudges without any
   inbound reachability). What must never happen is sovereign data — anything not
   meant to be stored in the controlplane — flowing to or through it. Only
   platform-owned metadata (attestation policy, governance policy) moves
   CP→enclave, and it may be delivered out-of-band (file at rest) *or pulled by
   the enclave*. The sovereign payload path is always enclave-local, and a
   controlplane outage never stalls it — the enclave serves from last-known
   metadata. The pull path
   is one reusable primitive, the CP→enclave **metadata channel** (SSE nudge +
   authenticated per-enclave pull, keep-cache-on-error): `internal/cpchannel`
   (server) + `internal/enclave/cpsync` (client), with governance policy and
   attestation reference values as its two consumers. It works across the
   CP/enclave cluster boundary, where a shared ConfigMap cannot reach a
   customer-cluster enclave.
3. **Agents talk only to their local enclave**, via the IMDS socket. Agents
   **hold no controlplane credential, ever**: egress toward the controlplane is
   terminated at the enclave **gateway** (total mediation, like all agent
   egress) and carries enclave-asserted agent identity — the gateway relays the
   agent's SVID through the broker and presents the controlplane-minted 60s
   agent-operator *token* itself on the re-originated hop (bd_2_0-bvw); the
   token never enters the agent's view. The agent's own TCP to a
   controlplane-bound host is redirected into the gateway listener like any
   other egress, never delivered direct.
4. **controlplane and enclave are separate packages.** They share the `dvx`
   binary for packaging convenience only; a `depguard` lint rule
   (`.golangci.yml`) forbids cross-imports so the boundary cannot erode.
5. **Identity minting is a controlplane-only authority; enclaves never mint
   controlplane-trusted identities.** The controlplane is the only party that
   mints identities the controlplane trusts (Zitadel service accounts / keys). It
   mints exactly **one** such identity per enclave — the enclave's **broker** — so
   the controlplane's per-enclave trust surface is one identity, not many. The
   broker may issue only *local* credentials to the other enclave services (it is
   a local issuer, never an issuer of controlplane-trusted identities), and those
   services reach controlplane-owned data only *through* the broker, never with a
   credential the controlplane trusts. Handing an enclave the power to mint
   controlplane-trusted identities would push IdP authority across the sovereignty
   boundary and multiply that trust surface — so it never happens. The `depguard`
   fence enforces the structural half: an `internal/enclave` package cannot import
   the Zitadel management client, which lives under `internal/controlplane`. See
   [`enclave-broker.md`](enclave-broker.md).
6. **An agent authenticates to trust-domain services with exactly one identity:
   a single attestation-bound mTLS credential the agent never holds.** There is
   one auth method for an agent reaching any service inside (or explicitly
   federated with) the dvx trust domain — mutual TLS presenting a SPIFFE-style
   agent-identity SVID whose subject is the *attested* agent (UID / org /
   image-digest). No second scheme (bearer tokens, signed headers as their own
   trust root, per-service API keys, WireGuard peer keys) is introduced for this
   purpose; alternatives were evaluated and rejected (see `bd_2_0-bwc`). The
   credential is **originated by the gateway**, which runs *outside* the agent's
   Kata VM: the agent makes a plain connection, the egress redirect sends it to
   the gateway, and the gateway holds the key and presents the SVID. The agent
   image is never reconfigured and the signing key never enters the untrusted
   guest — a compromised agent can only have traffic signed *as itself*, never
   forge or steal the identity. This is the inward-facing half of a two-sided
   rule: **inside/federated → assert the agent's attested identity; genuine
   third-party → inject the destination's credential** (`creds.Source`), because
   an outside party does not trust the enclave CA. Both halves keep the BYO agent
   holding no credentials. This invariant is the design law; it is *realized* by
   the attestation-bound credential (`bd_2_0-hbk`, minting on attest) and the
   identity-asserting tunnel (`bd_2_0-bwc`, the gateway wrapping agent traffic),
   which build onto the vsock IMDS seam and the real-attestation gate
   (`bd_2_0-che`). **Containment is enforced in two layers** (the same strategy as
   invariants 1-5). *Deterministic (compiler):* `internal/enclave/agentid` splits
   the mint *input* (`Identity`, openly constructable - a signature request proves
   nothing until the enclave CA signs it) from the verify *output*
   (`VerifiedAgentIdentity`, unexported fields, produced only by `agentid.Verifier`),
   so holding a verified identity is itself proof a credential was checked, and it
   is a distinct type from the gateway's enclosure attribution
   (`registry.AgentIdentity`) - the two cannot be interchanged. *Judgment
   (drift-review skill):* flags any agent-facing service that trusts a different
   root (bearer token, header, API key, or attribution-as-auth).
7. **A resource is addressed on two orthogonal axes, and placement never confers
   authority.** Every dvx resource has a *tenancy* coordinate — who owns it / who
   may act on it: `platform → org → group → agent` — and a *placement* coordinate
   — where it physically lives: `enclave` (sovereign region) + namespace +
   resource. **Authorization keys on tenancy only.** The authz target is
   `policy.Target{OrgID, GroupID, AgentID}` (`internal/policy`); enclave is a
   *placement target* the operator API takes as a publish destination, never a
   scope that grants authority (`internal/controlplane/iam/agents.go`: "the
   authz coordinate comes from the route path, the enclave is placement"). This is
   load-bearing security, not taxonomy: if placement conferred authority, *"move
   an agent into enclave X to inherit X's power"* would be an escalation. The same
   agent is nameable on both axes — one says where it runs, the other who it is —
   and they must never be conflated. dvx is **not** AWS: policy is scope-based, not
   resource-ARN-based, so a unified coordinate is wanted for audit / targeting /
   cross-reference, **not** for policy authoring. *Realization is deliberately
   deferred* (`bd_2_0-ihz`): today the coordinate primitives are ad-hoc per
   subsystem (grant principals `user:`/`group::`, the `urn:dvx:org` token claim,
   value-crypt AAD, `--enclave-id` audit/secret targeting). A single `urn:dvx:`
   grammar (`urn:dvx:enclave:…` placement, `urn:dvx:org:…` tenancy) would subsume
   all of them — too large to smuggle into any one change — so it is not built
   yet; the enclave id was kept **URN-embeddable** (an opaque random token, not a
   human coordinate; invariant realized by `bd_2_0-49z`) so the scheme can be
   adopted later with zero rework, homed in `internal/naming`.

## Data at rest & key custody

Encryption at rest and key ownership follow the same sovereignty logic as
everything else: **key custody never crosses a boundary the data itself may not
cross.** A regulated customer who asks for "BYOK" almost always means "we do not
trust you to hold it" — so Daevix answers the hard tiers by moving the *data and
the deployment* into the customer's boundary, not by holding an encrypted blob
under a key we must still reach to use.

### The BYOK spectrum

There are three answers, escalating with how little the customer will let the
SaaS operator hold:

| Customer stance | Answer |
|---|---|
| "operator must never possess our sovereign data" | **BYO-enclave** — run your own enclave. Nothing sovereign ever reaches the controlplane; there is no ciphertext to compel and no key to compel. Stronger than BYOK. |
| "operator may hold our control-plane metadata, under a managed key" | Controlplane metadata **encrypted at rest under the operator's KMS** (operator CMK). Honest disclosure: the operator holds that key. |
| "operator must not hold the key to *anything* of ours" | **BYO-controlplane** — self-host the whole controlplane, its operator-CMK pointing at your own KMS. The symmetric top answer; the controlplane is deployable SaaS *or self-hosted* (see [`self-hosted-controlplane.md`](self-hosted-controlplane.md), `bd_2_0-k5t.10`). |

The shape to keep in mind: **BYO-*key* → BYO-*enclave* → BYO-*controlplane***.
The two hard tiers are answered structurally (the data/deployment lives in the
customer boundary), not by per-tenant key escrow on our side.

### Two KMS trust domains, never crossing

- **Enclave KMS** — the customer/enclave-operator's (Vault in-cluster first; their
  own AWS/GCP KMS later). Guards sovereign rows *and* the attestation signing
  key. **Only the enclave dials it.** Its binding (endpoint, auth, key
  ring/transit key) is **local deployment config the operator sets when standing
  the enclave up**, exactly like the DB DSN — the controlplane never learns it,
  never holds a credential to it, never performs a crypto op against it. This is
  the direct consequence of invariants 1/2/5: a controlplane that could unwrap an
  enclave's DEKs would hold key-custody authority over sovereign data, which is
  forbidden the same way identity-minting authority is (invariant 5). If a feature
  seems to need controlplane→customer-KMS access, that feature belongs in the
  enclave, driven by policy the enclave *pulls*.
- **Controlplane KMS** — the operator's own. Guards controlplane metadata at rest.
  Only the controlplane dials it. A cloud enclave KMS must be the *customer's own*
  account/region, or key material crosses the sovereignty boundary.

### Envelope encryption (both planes)

The mechanism is standard envelope encryption, ported from the prior daevix
`internal/kms`: a random per-value **DEK** encrypts the plaintext locally with
AES-256-GCM; the **KEK** (in the KMS) only *wraps the DEK* — the KMS never sees
plaintext. A `KeyRef` stored beside the ciphertext (`provider:orgID:name:version`)
is the read-routing handle, and an **AAD** binds each ciphertext to its logical
identity `(org, agent, secret)` so a stolen `(ciphertext, key_ref)` pair cannot be
replayed onto another row or org. Decrypt is **fail-closed**.

One provider backs **two operations**: envelope (rows) and **sign** (the SVID CA,
via a `crypto.Signer` through the `agentid.SVIDIssuer` seam, `bd_2_0-d5n`). So the
attestation signing key and the data-at-rest KEK share a provider and its
auth/config, differing only in primitive. The KMS-backed SVID CA (`bd_2_0-k5t.6`,
`agentid.KMSMinter`) **self-signs its CA cert over the provider's key and mints
every leaf through the provider** — the CA private key never leaves the KMS and is
never written to disk; only the public `ca.crt` trust bundle is persisted. When no
KMS is configured the dev-start node-local CA holds the key on tmpfs; both plug in
behind the same seam, so `agentid.Verifier` is unchanged.

### Per-org keys without per-org custody

Within a single enclave (or the controlplane), keys are partitioned **per org by
key *name*** (`org-{id}-secrets`) inside the *one* provider that plane owns — not
per-org providers reaching into per-org KMSs. That deliberately drops the prior
daevix central `org_kms_config` registry, which had the operator reaching into
each org's KMS — precisely the controlplane→customer-KMS coupling this design
forbids. An enclave serves exactly one org (see "One enclave, one org" below), so
the per-org key name is effectively the enclave's own key; the naming is retained
(forward-compat and free) and still buys:

- **Crypto-shredding** — destroy an org's key and its rows become unreadable
  (GDPR erasure / clean offboarding), a concrete compliance win the operator can
  offer with no customer-KMS coupling.

Isolation *between* orgs is topological, not a shared-plane key partition: two
orgs never share an enclave, so their sovereign data lives in separate
clusters/KMSs — operator-proof isolation, stronger than the blast-radius framing
a shared plane could offer.

### One enclave, one org

**Invariant (`bd_2_0-j9r`, decision `bd_2_0-qyk`): an enclave serves exactly one
org.** `enclave.owning_org_id` is required (a NOT NULL FK to the org coordinate);
there is no shared / multi-tenant / platform-only enclave, and no runtime
served-org set the controlplane scopes snapshots to. An org **may own many
enclaves** (org 1→N enclaves; e.g. one per region or per sensitivity tier), but no
enclave is ever shared across orgs. The controlplane fans a per-enclave policy /
config snapshot out scoped to that enclave's single owning org plus the platform
floor (`Snapshot.ForOrg`, `internal/policy`); the enclave needs no request-time
cross-org gate, because a foreign-org agent's rows are simply absent from its
snapshot and default-deny downstream. This removes the shared-enclave co-tenant
adversary class entirely (`docs/threat-model.md` §4.6/§6). Hardware density, when
needed, comes from multi-**enclave**-per-node (`bd_2_0-6l2`), which is orthogonal
and unaffected.

### The rule that keeps controlplane BYOK from breaking the boundary

Classic per-tenant BYOK on the controlplane is deliberately **not** built: the
controlplane must *read* most of its metadata to function (evaluate policy,
resolve identity), so a customer-only key would force it to dial the customer's
KMS on every read — a controlplane→customer-boundary dependency (breaks invariant
2) and an availability inversion (customer KMS down ⇒ their control plane down).
Hence the design rule:

> **The controlplane may apply a customer-held key only to fields it stores and
> returns but never interprets. Anything it computes on stays under operator
> custody (or the customer self-hosts the controlplane).**

The strongest lever remains **data minimization**: the less org-sensitive
metadata the controlplane holds authoritatively (push sensitive policy *content*
down to the enclave; keep only references/hashes centrally), the smaller this
whole question gets — invariant-2 thinking applied to *sensitivity*, not just
sovereignty. The column-by-column classification that operationalizes this — which
controlplane fields are public / operational / org-sensitive, and per sensitive
column whether to encrypt-at-rest, keep a reference only, or relocate — is in
[`controlplane-data-minimization.md`](controlplane-data-minimization.md)
(`bd_2_0-k5t.9`); it feeds tier-2 controlplane at-rest encryption (`bd_2_0-k5t.7`).

## Client interaction

How clients (the CLI, a web console, operator tools) talk to the controlplane vs.
directly to an enclave — the endpoint split, enclave discovery, control-intent
delivery, the direct sovereign-data path, and enclave identity — has one governing
model: [`client-interaction.md`](client-interaction.md). In one line: metadata and
control go to the controlplane and reach an enclave only by the enclave pulling +
reconciling; sovereign-data operations go client→enclave directly and never
through the controlplane.

## Two log streams

Operational logging and audit logging are **separate first-class streams**, not
two uses of one logger. Application logs go to **stdout**; audit logs are a
**store**, not a stdout line — when an audit store sink is registered
(`audit.AddSink`) the record goes to the store instead of stdout. Both are told
apart by the top-level `stream` key:

| | application log | audit log |
|---|---|---|
| package | `internal/applog` | `internal/audit` |
| sink | stdout | ClickHouse audit store (`internal/auditstore`); stdout only when no store is wired |
| top-level key | `stream=app` | `stream=audit` (+ `audit_schema`) |
| purpose | diagnostics for operators | durable security-relevant record |
| may be dropped/reworded | yes | no |

Audit is stored in ClickHouse, per plane: each **enclave** has its own sovereign
audit store (agent signals — network flows, etc.), and the **controlplane** has
its own platform-audit store (operator actions; no sovereign data). The two never
share a store (invariants 1/2). A component with no `--audit-dsn` configured falls
back to stdout. Split streams downstream with one field match, e.g.
`jq 'select(.stream=="audit")'`.

Record classes are classified into **evidence tiers** (authority /
completion / telemetry), each with a named durability behavior at the store's
WAL bound — the classification bd_2_0-4p8.3's capture-or-deny extension scopes
against. See [`audit-evidence-tiers.md`](audit-evidence-tiers.md).

In an enclave the audit stream is the primary evidence that sovereign-data
operations stayed within the enclave. Treat it as an API, not convenience
logging.

**Durability.** The store buffers writes through a per-table batcher whose
back-pressure *drops* rather than blocks — audit must never stall a component's
hot path. By default the buffer is in-memory (lost on a ClickHouse outage or a
restart). Setting `--audit-dsn`'s companion `--audit-wal-dir` upgrades it to a
crash-safe **on-disk WAL** (`bd_2_0-8kl.10`): each batch is fsync'd + atomically
renamed before flushing, removed only once it lands in ClickHouse, and replayed
on start, so a ClickHouse outage or a component restart loses nothing. The
spool is bounded (`MaxWALBytes`, default 512 MiB); past the bound it drops the
newest batch and raises producer back-pressure (`Store.Stats().WALDepth`).

**Why ClickHouse, hand-written.** The audit/telemetry store speaks the
`clickhouse-go` driver directly rather than through sqlc, because sqlc has no
ClickHouse codegen ([sqlc-dev#1628](https://github.com/sqlc-dev/sqlc/issues/1628));
the rest of the tree is sqlc+goose over Postgres. This split is deliberate and
fenced (see *Preventing drift*): `internal/auditstore` is the one-stop-shop home
for ClickHouse access, and a `depguard` rule confines the raw driver to it so it
cannot leak into Postgres-backed code.

## Preventing drift

Cross-cutting concerns each have exactly one home package, so a change happens
in one place. We keep it that way with two complementary layers:

**Deterministic (CI, every commit)** - `.golangci.yml`:

- `depguard` enforces the controlplane <-> enclave import boundary, keeps
  `internal/kms` boundary-safe (it may import neither plane, so both can use it),
  and confines the raw `clickhouse-go` driver to `internal/auditstore` (the
  ClickHouse-vs-sqlc split, `bd_2_0-8kl.8`).
- `forbidigo` fences each raw primitive to its owning package: build a logger
  anywhere but `internal/applog`/`internal/audit`, write a JSON response outside
  `internal/httpx`/`internal/problem`, stand up an HTTP server outside
  `internal/server`, or encrypt at rest with raw AES-GCM (or construct a KMS
  backend directly) outside `internal/kms`, and `make lint` fails.
- `make drift-dupl` is an advisory (non-blocking) token-level clone scan.

The one-stop-shop packages: `internal/server`, `internal/httpx`,
`internal/problem`, `internal/applog`, `internal/audit`, `internal/auditstore`
(ClickHouse access), `internal/obs`, `internal/authn`, `internal/kms`,
`internal/config`.

**Judgment (periodic / on review)** - the `drift-review` skill
(`.claude/skills/drift-review`). It finds what fences cannot: a *new*
cross-cutting concern with no home yet, and semantic duplication that clone
detection misses. Every finding it confirms proposes the `forbidigo`/`depguard`
fence to add, so that class of drift becomes deterministic from then on. The
skill is the discovery mechanism; the fences are the ratchet.

A third surface has its own uniformity contract: every component's **HTTP API**
follows one shape — `/api/v1/*` paths, `apispec.Route` tables that drive both the
mux and the OpenAPI spec, RFC 9457 errors, bearer + path-derived capability, and
a self-describing origin. See [`http-api-conventions.md`](http-api-conventions.md):
*this is what a daevix HTTP API looks like; do not diverge from it.*

## The threat-model lens (change discipline)

The same judgment philosophy applies to a second kind of drift: the gap between
the code and its **threat model** ([`threat-model.md`](threat-model.md)). That
document is a Daevix-wide, boundary-by-boundary security model, and it is only
useful if it stays *true* - a fail-closed default that quietly goes fail-open, a
new trust dependency nobody wrote down, or a "designed-but-unbuilt" note that is
now stale because the mechanism shipped, all silently invalidate it.

So it is a standing discipline - **not** just a doc to read - that every
security-relevant change is viewed through one question: *how does this affect the
threat model?* The rule:

> A change that materially affects the threat model must (a) be recognized as
> such, (b) get a careful review or an alternative approach when the effect is
> **negative** (it weakens a boundary), and (c) leave `threat-model.md` still
> true - reconciled in the same change, or with a bead filed for any gap it
> knowingly opens.

"Material" means it alters a trust boundary (§4.1-4.7 of the doc), an invariant
(1-6 above; agent-identity invariants 1-6), or one of the doc's §5 assumptions /
§6 out-of-scope items - typically by weakening a fail-closed default, expanding
the TCB / adding a trust dependency, crossing a sovereignty invariant (data,
identity-minting, or dial direction), introducing a standing agent secret, adding
a second agent-auth trust root, or opening a new reachable surface. A behavior-
preserving refactor in a security package is *not* material.

This is the security sibling of `drift-review`, with the same two layers:

- **Judgment** - the `threat-model-review` skill
  (`.claude/skills/threat-model-review`) applies the lens to a diff / PR or a
  proposed design: it locates the affected boundary, computes the concrete threat
  delta, flags negative deltas for careful review or an alternative, and says
  exactly how the doc must be reconciled.
- **The ratchet** - here the ratchet is not a lint fence but the doc itself: a
  material change that lands without reconciling `threat-model.md` (or filing a
  bead for a known gap, as `bd_2_0-a9e` was filed for the last-admin race) is
  incomplete. The model is kept honest by the same adversarial-verification
  discipline that built it - a mitigation is real only if the code enforces it.
