TacidOS
DevelopersGitHub

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

ComponentBinaryUserTrustOne line
Stemtacid-stemrootprivilegedTyped 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.
Keepertacid-keepertacid-keeperauthorityPolicy, classifier, taint, approvals, secrets, audit, health, snapshots; IPAddressDeny=any.
Egresstacid-egresstacid-egressgateThe only network path for agent processes; allowlist, credential injection, ledger, airgap.
DaemontaciddtacidagentSessions, 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.
Inferencetacid-infertacid-inferhostile inputSupervises sandboxed inference servers; local OpenAI-compatible socket.
Updatetacid-updatetacidgateTimer-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.
Sessiontacid-sessionthe personuserOne 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.
Shelltacid-shellthe userrendererWayland shell for the Niri session.
CLI/TUItacidthe userrendererCommand 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.

PrincipalEstablished byDefault authority
ownerowner key signaturewrites policy; grants approvals; may set any level
useruid on the local socketuser-scoped tools; approvals within own scope
agenttacidd itselfproposes; acts within the autonomy level
remote agentits own key over a remote transportits own policy, never the owner’s
voiceaudio inputuntrusted; 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_RIGHTS for file descriptors (audio, pseudo-terminals, blobs); notifications for streams.
  • Crate tacid-protocol defines all messages; JSON Schema is generated and versioned with the crate. tacid-stem uses 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.

PathSigned byWritten byContent
flake.nix, flake.lockowner or agent (lock bumps by the updater)installer, updaterPins tacidos; composes body, mind, owner.
body/hardware.nix, body/disks.nix, body/gpu.nixagentinstaller, tacid body probeThis machine’s hardware; regenerated, not portable.
mind/agent.nix, mind/preferences.nixagentthe agenttacid.* options only.
mind/policy.nix, mind/personality.mdownerthe ownerThe constitution and the system prompt.
owner.nixownerthe ownerFree Nix; may import Home Manager.
pkgs/agentthe agentDerivations built in the Nix sandbox; policy-tagged untrusted.
secrets/ownerthe owner, tacid secretssops-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.