The stable technical reference
TacidOS Architecture
The stable technical reference: components, protocol, genome, transactions, authority.
The stable technical reference. PLAN.md tells the story and carries the phases;
this document carries the contracts. When they disagree, an ADR in decisions/
settles it, and SECURITY.md outranks both.
These are contracts, not a status report: some of what follows is running and tested and some is not. The README’s crate table says which is which, and ADR-0031 records where building the thing moved a line this document had drawn somewhere else.
1. Components
| Component | Binary | User | Trust | One line |
|---|---|---|---|---|
| Stem | tacid-stem | root | privileged | Typed operations only — activate, boot default, reboot, udev, nftables, principals, garbage, and in genesis mode the three that make a machine: lay out a disk from a typed plan, write the first genome, install a built system; keeper-only socket; no network. |
| Keeper | tacid-keeper | tacid-keeper | authority | Policy, classifier, taint, approvals, secrets, audit, health, snapshots; IPAddressDeny=any. |
| Egress | tacid-egress | tacid-egress | gate | The only network path for agent processes; allowlist, credential injection, ledger, airgap. |
| Daemon | tacidd | tacid | agent | Sessions, agent loop, facts, memory, transaction rendering and building, model clients; on a live image, and only there, genesis.build — a first system built from a genome it is handed, with every input pinned to the sources the image carries (tacid.core.inputs). Holds no journal, no token and no approval: everything it may do it asks the keeper for, over the keeper’s socket. |
| Inference | tacid-infer | tacid-infer | hostile input | Supervises sandboxed inference servers; local OpenAI-compatible socket. |
| Update | tacid-update | tacid | gate | Timer-driven; verifies signed manifests through the proxy; classifies and decides when; proposes the advance to the daemon as the system, and never builds or activates itself. |
| Session | tacid-session | the person | user | One per person, started by the system as them on /run/tacid/sessions/<name>.sock; reads their home as them, read-only, and opens what is not text in a sandbox; answers the daemon’s uid and their own, nobody else; everything it returns is untrusted data (ADR-0035). Voice and desktop interop are a later, session-bound component. |
| Shell | tacid-shell | the user | renderer | Wayland shell for the Niri session. |
| CLI/TUI | tacid | the user | renderer | Command line; TUI with no arguments. |
Only the stem runs as root. Only the egress proxy has outbound network. Only the keeper holds
secrets and decides. Only tacidd talks to models. Clients render protocol state and never decide.
tacid / tacid-shell ──── tacidd.sock ────► tacidd ─── infer.sock ──► tacid-infer ─► llama-server …
│ │ ▲
│ approvals, audit view, policy │ │ facts, memory, nix eval/build
└──────────── keeper.sock ───────► tacid-keeper ◄── tacid-session (per user)
│
stem.sock (0600, keeper uid + boot token)
▼
tacid-stem ──► nix-env · nix-store · switch-to-configuration-ng · systemd-bless-boot · sgdisk/mkfs/mount · nixos-enter · nft
tacidd, tacid-session, sandboxes ── egress.sock ──► tacid-egress ──► the network (allowlist)
2. Principals and provenance
A principal is an authenticated actor. Established at connect time from SO_PEERCRED, the
per-boot token, and, for the owner, a signature with the owner key.
| Principal | Established by | Default authority |
|---|---|---|
| owner | owner key signature | writes policy; grants approvals; may set any level |
| user | uid on the local socket | user-scoped tools; approvals within own scope |
| agent | tacidd itself | proposes; acts within the autonomy level |
| remote agent | its own key over a remote transport | its own policy, never the owner’s |
| voice | audio input | untrusted; capped at level 2 unless confirmed physically |
Provenance labels every context item: owner, user, system, tool, file, web,
mcp:<name>, voice. Tool output derived from untrusted input inherits the untrusted label. A
turn is tainted if any item in its context is untrusted; a tainted turn is capped at autonomy
level 1 for side effects. Provenance is stored with memories and with journal entries.
3. Protocol
- JSON-RPC 2.0, newline-delimited, over Unix domain sockets;
SCM_RIGHTSfor file descriptors (audio, pseudo-terminals, blobs); notifications for streams. - Crate
tacid-protocoldefines all messages; JSON Schema is generated and versioned with the crate.tacid-stemuses a separate minimal crate. - Method namespaces:
session.*(ask, cancel, history, events),tx.*(propose, preview, apply, undo, list),facts.*,memory.*,models.*,providers.*,policy.*(keeper),approval.*(keeper),journal.*(keeper),health.*(keeper),keeper.*(the daemon’s side of the keeper: submit, classify, apply, pending, state — the first three answer only the agent’s uid),update.*,device.*,project.*,voice.*,stem.*(keeper → stem only). - UI state travels in the protocol: plans, live tool state, diffs, pending approvals, badges. A client that renders these has feature parity by construction.
- Transport-agnostic: the same messages over SSH or WireGuard with the remote peer as its own principal.
- D-Bus (
zbus) is used only for desktop interop; polkit is not used on the Tacid path.
4. The genome
/etc/tacid is a git repository and the complete declaration of the machine.
| Path | Signed by | Written by | Content |
|---|---|---|---|
flake.nix, flake.lock | owner or agent (lock bumps by the updater) | installer, updater | Pins tacidos; composes body, mind, owner. |
body/hardware.nix, body/disks.nix, body/gpu.nix | agent | installer, tacid body probe | This machine’s hardware; regenerated, not portable. |
mind/agent.nix, mind/preferences.nix | agent | the agent | tacid.* options only. |
mind/policy.nix, mind/personality.md | owner | the owner | The constitution and the system prompt. |
owner.nix | owner | the owner | Free Nix; may import Home Manager. |
pkgs/ | agent | the agent | Derivations built in the Nix sandbox; policy-tagged untrusted. |
secrets/ | owner | the owner, tacid secrets | sops-nix, age-encrypted. |
Rules: the keeper loads mind/policy.nix only if its head commit is owner-signed; the agent’s
file tool cannot write owner-signed paths; tacidd stages the tree before every evaluation; the
journal, memory and models never live here.
State: /persist (impermanence root), /var/lib/tacid/{models,memory/<uid>,journal,cache,snapshots},
/run/tacid/*.sock.
5. Transactions
Transaction { id, kind: action | change, intent, principal, provenance[], items[], classification, verification[] }
ChangeItem { op: package.ensure | package.remove | service.enable | service.disable | service.set
| udev.profile | option.set | option.unset | hardware.profile | user.pref | raw.nix
, … }
ActionItem { op: exec.sandbox | exec.host | git.* | serial.open | probe.* | firmware.flash | net.fetch | screenshot | … }
Classification { reversible, privilege: user | system, persistent, network, destructive, activation: switch | session-restart | boot, data_touching, taint }
Lifecycle for change: classify → policy verdict → render → stage → eval → build → preview →
approval (client → keeper, bound to id, then tx.apply from the client to the daemon, which
asks the keeper to apply and cannot have invented what the keeper holds) → activate (stem, by
class: the generation is set as the system profile first, then switched to, so every activation
is a numbered generation with a boot entry) → health gate → rollback on failure → journal →
agent-signed commit. A genome without a lock file is locked before its first build, to the
sources the machine carries (tacid.core.inputs), and built from the lock ever after; the tools
a NixOS system is assembled by, which no running system references, are kept in every closure
(system.extraDependencies) so a change is built from the disk with no network
(ADR-0037).
Lifecycle for action: classify → verdict → approval if required → execute in the sandbox or
through a typed tool → journal.
Reversibility is computed by the keeper’s classifier over the rendered diff; unknown is
irreversible; the explicit irreversible list covers exposure, principals, secrets, boot and
encryption, external devices, data deletion, git push, and any egress of file content. Rollback
claims are three-way: system, data (snapshot), external (never).
6. Authority
Autonomy levels 0–4 (observe, consult, reversible, trusted, sovereign); default
reversible; level 4 expires and shows a persistent indicator. Capability policy per category
(auto | ask | deny, sandbox for exec). Budgets per session. Tool visibility scoped per
session. Approvals are bound to a transaction id and delivered client-to-keeper, so tacidd
cannot forge them. The hard invariants in SECURITY.md are enforced in the keeper and the stem.
7. Secrets
Handles secret://<provider>/<name>. Storage: sops-nix (age) in the genome for declared secrets;
systemd-creds (TPM2-sealed where available) for machine and service secrets; a Secret Service
façade for applications. Resolution only by the keeper for a named destination; injection by the
egress proxy (HTTP credentials) or into a single tool invocation’s environment. A scrubber runs on
every tool result, memory write and journal line. Protected paths are denied by default.
8. Egress
nftables rules installed by the stem pin the tacid, tacid-session and sandbox uids to the
loopback egress socket. The proxy enforces the domain allowlist, injects credentials, writes the
ledger (destination, data classes, bytes, model, estimated cost), raises the NET badge, and
implements the airgap profile.
9. Facts
system.* is a typed model derived from sysfs, udev, journald, systemd’s D-Bus, the Nix store and
DRM/GPU tools. Paths: cpu, memory, gpu, disks, usb, serial, probes, network,
services, processes, logs, generations, packages, kernel, session, drift. Doctor
checks and the health gate are functions over facts.
10. Memory
SQLite + FTS5 + sqlite-vec per user. Layers session, project, owner, machine;
never-memory for secrets. Every row carries provenance; untrusted-derived rows are never loaded
as instructions; owner-layer writes require confirmation. The vector index is versioned by
embedding-model id. Migrations are forward-only; a snapshot is taken at every activation so a
generation rollback restores the matching database.
11. Mind
Provider trait (rich: tool use, streaming, caching, structured output, multimodal); adapters for
the Anthropic Messages API and the OpenAI-compatible family first. Backend, Stt, Tts, Embed
traits for local engines run as sandboxed subprocesses. Routing by task class, privacy class
(derived from provenance and path policy), policy, context size, latency, offline state, hardware
and budget. Constrained decoding for local tool calls. Registry models/registry.toml, signed.
12. Updates and the immune system
Channel manifest { channel, revision, tacidos_rev, nixpkgs_rev, artifacts{system, kernel, closure_bytes}, cache, inputs{name → locked}, created, expires } signed with TUF roles (offline
root; online channel keys with short expiry), written by tacid-channel publish; the cache it
names is signed by tacid-channel cache-sign in the format Nix reads. tacid-update.timer polls
(hourly edge, daily stable), verifies, classifies, and when the moment is right proposes a
ChannelAdvance to the daemon over tx.propose as the system principal: the genome’s lock moves
to the inputs the channel locked (original untouched, locked replaced), the daemon builds
from the cache, and the stem registers the generation for the next boot. A channel advance is
boot-class: it carries the keeper’s own binary, so boot counting judges it — every entry gets
three tries, boot-complete.target requires the health gate, a blessed boot clears the counter,
and three unblessed boots hand the machine to the generation before
(ADR-0038).
The switch-time health gate: units reached target, daemons up, declared services healthy,
network if declared, inference answers a stub prompt, agent tool-use smoke test. Failure
re-activates the previous generation, restores Tacid’s own state snapshot, journals, and
notifies. Retention: the owner declares it under tacid.core.retention — the newest ten, thirty
days, pinned by number, collected weekly — and the keeper applies it from its own policy, keeping
what runs, what booted and what the profile points at whatever the numbers say; the stem forgets
by number, collects the store and rewrites the bootloader’s entries from the profile’s own
generation; tacid-collect asks on the owner’s timer when nobody is at the machine
(ADR-0040).
13. Self-improvement
Core crates (tacid-stem, tacid-keeper, tacid-protocol, the health gate) are channel-signed;
the keeper verifies their store signatures before activation and only the owner may replace them.
Agent-mutable crates may be rebuilt with --override-input tacidos ./checkout; such a machine is
marked self-built. Agents file proposals (branch, CI, evaluation, rationale) into the owner’s queue;
humans sign and push. Boards use a workstation body as a remote builder.
14. Storage and boot
GPT, UEFI, systemd-boot, systemd stage-1. LUKS2 with TPM2 + PIN bound to PCR 7, sealed at
installation as the stem’s second guarded operation
(ADR-0039); the passphrase chosen
at installation is the way back when the TPM cannot open the root, and the recovery key the
enrolment prints is handed back once through the stem and the keeper and kept nowhere
(ADR-0041).
A person who brings no owner key is given one there: the public half in the genome, the secret
half in their own home, written by the stem. Btrfs subvolumes @root (ephemeral: made again at every boot from the read-only @root-blank the installer kept), @nix, @persist, @log, @home, and @swap when there is swap (ADR-0036).
Impermanence by default; every tacid.services.* option declares its persistent directories.
Lanzaboote integrated, enrolled by tacid secureboot enroll, opt-in until certified.
15. Extension points
tacid.*modules: the agent’s vocabulary. Adding a service means a module with curated options, declared state, and a VM test.- Tools: typed Rust tools registered with a schema, a provenance rule and a policy category.
- Doctor checks: a function over facts with a proposed fix as a transaction.
- Hardware quirks:
nix/hardware/modules with the facts that identify the machine. - Models: registry entries.
- MCP: Tacid exposes an MCP server; third-party servers register as untrusted principals.
- Themes: token files only.