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, principals, garbage, the journal’s seal, starting the update check (one unit, named by a constant of the stem’s own protocol and never by a caller), 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 (hash-chained and sealed to a TPM counter, ADR-0060), health, snapshots, and what the machine changed without being asked and nobody has been told (ADR-0057); IPAddressDeny=any. |
| Egress | tacid-egress | tacid-egress | gate | The only network path for agent processes; allowlist, credential injection, ledger, the day budget, 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. No network: weights are fetched by the daemon through the proxy and verified here at every start, against the registry (ADR-0079). |
| Update | tacid-update | tacid-update | gate | Timer-driven, or started early by tacid update through the keeper and the stem; verifies signed manifests through the proxy; writes the verified revision where the agent cannot; proposes the advance to the daemon; never builds or activates itself. The verification stays here rather than in the daemon, because the daemon is the agent’s to rewrite (ADR-0077). |
| 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. |
| Scribe | tacid-session --write | the person | writer | One per person, in /run/tacid/scribes/ on a socket only the keeper may open; a writable home, two write methods and nothing that reads, and git on its path. Commits and discards in a repository, and writes a file — the development shell Tacid generates, saved when the person says to — at the keeper’s word (ADR-0055). |
| Shell | tacid-shell | the user | renderer | Wayland shell, on Hyprland — one compositor, no option (ADR-0053). A layer-shell client that knows nothing about it. |
| 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.
Twenty-three operations, and five of them nothing sends: bless_boot, reboot, shutdown,
udev.reload and principal.ensure. Three are done elsewhere and correctly — systemd’s own
systemd-bless-boot blesses when boot-complete.target is reached, which is what tacid-health
gates; activation reloads udev and makes the users the genome declares. Two are surface for work
not yet written: the machine does not reboot itself or shut itself down for a staged boot-class
change (a person does, and RestoreResult.reboot_needed says so). None of it is reachable by
anything but the keeper — the socket takes the keeper’s uid and this boot’s token, so an unsent
operation is dead surface rather than an open door — but the most privileged process in the system
carrying a fifth more executable surface than anything reaches is worth writing down once, rather
than letting a reader assume the list is exercised because it is typed.
There were six, and the sixth is instructive:
ADR-0084 removed
nftables.profile rather than sending it. It was the only operation here that took its
instructions from a file — nft -f <path> — which is run(string) with extra steps on the one
process that is supposed to have neither, and it was carried for §8’s narrowing, which turned out
not to be needed at all.
tacid / tacid-shell ──── tacidd.sock ────► tacidd ─── infer.sock ──► tacid-infer ─► llama-server …
│ │ │
│ approvals, audit view, policy │ ├─ facts, memory, nix eval/build
│ what nobody has been told yet │ └─ sessions/<name>.sock ─► tacid-session (per person, reads)
└──────────── keeper.sock ───────► tacid-keeper
├─ scribes/<name>.sock ────► tacid-session --write (per person, writes)
│
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)
Which way the arrows point is the design. The daemon reads a person’s files through their session and cannot reach their scribe; the keeper reaches the scribe and nothing else reaches it at all. Consent must not travel through the process it authorises, and neither must the act.
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. - Every daemon’s unit is
Type=notifyand every daemon saysREADY=1when its socket is bound, so “started” means “can be reached” rather than “was spawned” and anAfter=on one of them waits for the door rather than for the process.sd_notifyis written out rather than linked: one datagram toNOTIFY_SOCKET, and a C library for that would be a C library on the trust path for fifteen lines. - Crate
tacid-protocoldefines all messages; JSON Schema is generated and versioned with the crate.tacid-stemuses a separate minimal crate. - Method namespaces:
session.*(open, ask, event),tx.*(propose, apply, undo),facts.*,memory.*,models.*,policy.*(keeper),approval.*(keeper),unattended.*(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). - Two names were declared and never answered, which is a shape worth naming because nothing
caught it: the protocol’s own test lists the method names and passes whether or not anything
implements them, so a vocabulary test is not an implementation test.
tx.previewwas redundant —tx.proposereturns the verdict and the preview and applies nothing, so it already was the preview — and is gone.session.cancelwas the one that mattered and is now answered. - Stopping is a mark, not an interruption. A turn is awaited inline on the connection that
asked for it, so
session.cancelleaves a mark that the turn reads between steps: once before asking the mind, and once after the mind answers and before anything is done about it. That second read is what earns the feature — between it and the next line is where a tool the model just asked for stops being a plan and becomes a side effect. What the result promises is therefore the next step and never “stopped”: a request already with the mind cannot be unsent, and a machine that claimed otherwise would be lying about the one thing somebody reached for the stop to be sure of. The mark is taken when read, so a stop does not leak into the next question, andSessionEvent::Stoppedis its own event rather than aFailedwith a kind message — a stop drawn in the colour of a fault teaches people to hesitate over it. crates/tacid-daemon/tests/methods.rsnow holds the two lists against each other, reading the constants out of the protocol and the match arms out of the server, withsession.eventexempted by its own doc comment saying it is never a call.- UI state travels in the protocol: plans, live tool state, diffs, pending approvals, badges. A
client that renders these has feature parity by construction — for what it renders. It says
nothing about what a client can start, and for a while that was a real gap:
:docomposed a typed change with no model between, and the command surface on the screen had no path to it, so a machine with no mind could be changed from the terminal and not from the screen. The verbs aretacid-verbnow and all three surfaces parse with it. The sentence to keep from that: parity of rendering is enforced by the protocol; parity of reach is enforced by the clients sharing the code that composes, and has to be checked when a surface gains a way in. - Transport-agnostic: the same messages over SSH or WireGuard with the remote peer as its own principal.
integration.hellois the one call on the daemon’s socket that only ever subtracts: a connection says it once and isRemoteAgentfrom then on — capped atconsult, treated as an agent by the invariants, refused what is reserved for a person, and named in the record. Nobody has to prove anything to make it, because it cannot widen anything (ADR-0061).tacid mcpis the Model Context Protocol on standard input and output — JSON-RPC over a pipe, so a translation of the above rather than a second implementation. It narrows itself before it reads a byte, relays a tool call to the typed method behind it, and hands back the daemon’s own JSON. Six questions, no verbs.- D-Bus (
zbus) is used only for desktop interop; polkit is not used on the Tacid path — consent goes to the keeper, and a polkit agent needs a graphical session that a tty and a serial console do not have. A machine with a screen does enable polkit, for the other thing ADR-0005 names it for: a desktop session asking logind about the machine.systemctl suspendis a call behind a polkit action, and logind treats a check it cannot answer as a refusal, so without it the owner of a laptop could close its lid and press its power button and not put it to sleep from their own desktop. It needs no agent — logind’s own policy answersyesfor an active seat session — which is what keeps it off the path this line is about.
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/agent.state.json | agent | the agent | tacid.* options only. The module is rendered from the state, so what the agent asked for and what Nix reads cannot disagree. |
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 | sops-nix, age-encrypted. Nothing manages the age identity from the command line yet; tacid secret is about a value already loose on the machine, not about this directory. |
Rules: the four files that are the owner’s — flake.nix, owner.nix, mind/policy.nix,
mind/personality.md — are root-owned, and so is the sticky directory holding them. Both halves
are the rule: in a sticky directory a file may be removed by its owner, by the directory’s
owner, or by root, so a genome belonging to the agent let it delete root’s files however they were
owned. The agent reaches the genome by group, creates and removes its own files there, and neither
writes nor unlinks these; because that leaves it working in a directory it does not own, it names
the genome to git as safe.directory, which is a trust in root rather than a way around this;
the owner’s signature over the four sits beside them, owned by the person whose signature it is; the keeper refuses to activate a generation built under a constitution the owner did not
sign for (ADR-0083);
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.
Not yet true, and worth reading as a plan rather than a rule: the keeper does not verify the
owner’s signature on the genome’s git commits. That is what
ADR-0007 describes and it is the complement for an owner
who edits their own file from another machine and pushes; today a constitution can only be signed
at the machine, with tacid constitution sign. This paragraph said the git rule was in force for
as long as it has existed.
State: /persist (impermanence root), /var/lib/tacid/{models,memory/<uid>,journal,cache,snapshots},
/run/tacid/*.sock.
What a system carries about itself
The genome is what a machine will be. Four files inside every built system say what it is, what it can be asked for, whose rules it was built under, and what it built into its own initrd, so that all four can be read off a store path without evaluating anything.
| Path in a system | Written by | Read by | Content |
|---|---|---|---|
etc/tacid-system/declared.json | tacid.core.declared, from mind/agent.state.json | the keeper, off a store path | What the agent asked this machine to be. |
etc/tacid-system/constitution.json | tacid.core.constitution from the genome, beside the four files hashed by the module at build time | the keeper, off a store path | What the owner signed for, and what this system was actually built from. |
etc/tacid-system/services.json | tacid.services, from the one attribute set that declares it, implements it and describes it | tacid settings, the daemon, the keeper’s classifier | The curated allow-list: each service, its sentence, what it keeps, its settings, and whether enabling it makes this machine answerable from somewhere else (ADR-0078). |
etc/tacid-system/boot.json | tacid.core, from boot.plymouth.enable and the two initrd module lists | system.boot, and through it doctor boot | Whether this system has a boot screen, and what its initrd carries. The one question here that cannot be asked of a running machine: after switch-root the initrd is a compressed archive in the store and nothing of it is on the filesystem, so “which modules were in it” has no reader (ADR-0090). |
The first is the closure binding (ADR-0048): the keeper compares the declaration of a system it is asked to activate against the running system’s plus the change the owner approved, and refuses anything else. Every system carries one, including a machine that has been asked for nothing — the empty declaration is a declaration, and when it was an absent file instead, a fresh machine’s first change had no baseline to be checked against and every change was refused.
The second is the constitution binding (ADR-0083), and it covers what the first cannot: the owner’s four files are in no declaration, so a system built from rewritten ones declares the approved change perfectly. The keeper requires the owner’s signature and the build’s own hashes to agree, with the owner taken from the running system rather than from the candidate. A machine that has never been signed for is not held to one, which is how a machine whose owner kept their own secret half is not bricked by a check it could never pass.
The third answers the question a settings application would have been built for, without being one. It is read rather than served, so a machine whose daemon will not start can still be asked what it is; and the same list narrows the proposal schema the model is shown, so a service this machine does not implement is one the model is never offered — and, for a local engine held to a grammar compiled from that schema, one it cannot emit (ADR-0056).
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). Before a data-touching change
reaches the stem the keeper asks it for a read-only Btrfs snapshot of every declared root, named
for the transaction, and refuses the change if it cannot have one; the classifier is built with
taking_snapshots inside a keeper and answers yes to “is this covered” for a path a declared root
contains. Putting one back is Operation::SnapshotRestore, which puts nothing back: a machine
cannot replace the filesystem it is running on, so the stem checks the snapshot is there and
writes one line that the next boot’s initrd reads and acts on, beside the unit that makes the root
fresh (ADR-0054).
/persist, /var/log and /home may be put back. The gate is the keeper’s rather than the
stem’s guard, because a guard asks whether a person is at the machine and only a live image’s
console can answer that. tacid data list | snapshot | restore is the surface.
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
The proxy is a unix socket in /run/tacid, reachable through its group and by nothing else on the
machine. What confines the processes around it today is systemd’s own address filtering:
IPAddressDeny=any with IPAddressAllow=localhost on the daemon and the session, which is enforced
by the kernel through a cgroup eBPF program rather than by the process being confined.
The narrowing turned out to be a removal. IPAddressAllow = "localhost" was every local port,
so a compromised tacidd could reach any other listener on the machine and use it as a relay. Four
documents said nftables rules pinning the agent uids to the proxy would close that, and nothing
installed them. What closed it is RestrictAddressFamilies = [ "AF_UNIX" ]: tacid-egress is the
only crate in this tree that opens a TCP connection — the model client, the update client, the
keeper, the sessions and the local mind are all Unix sockets — so the daemon needed no narrower
route to the network, it needed none. A seccomp filter on socket(2) is stronger than a rule
filtering packets on a socket the daemon was still allowed to create: there is no port to narrow to
and no relay to reach. IPAddressDeny=any stays beside it as a second mechanism in a different part
of the kernel. The stem’s nftables.profile operation is gone with it — the only operation in its
surface that read its instructions from a file
(ADR-0084). The airgap, which is
often confused with all of this, never depended on it: it is a flag in the keeper’s own directory
that the proxy reads, and it works.
The proxy enforces the domain allowlist, injects credentials, writes the
ledger (destination, data classes, bytes, model, estimated cost), raises the NET badge, keeps the
owner’s egressMiBPerDay budget over that same ledger, and implements the airgap profile. The
budget lives here rather than with the keeper’s other two because a budget belongs in the process
that can still stop what it counts, and the keeper has never seen a byte (ADR-0058).
9. Facts
system.* is a typed model derived from sysfs, udev, journald, the cgroup tree, the Nix store and
DRM/GPU tools. What is running comes from the kernel’s own accounting — a unit’s cgroup with a
process in it — and not from /run/systemd/units, which systemd keeps for a unit that started and
failed and which therefore called a dead daemon healthy. That accounting is the unified cgroup
hierarchy’s, so services also says whether it could be read at all: a machine booted onto the
legacy layout can be asked nothing about which units hold processes, and the doctor and the health
gate skip rather than call every unit dead and roll a good generation back. Paths, as the table in tacid-facts declares them: boot, cpu, devices,
disks, filesystems, generations, graphics, identity, kernel, load, memory,
network, power, services, tpm, usb, wireless. Doctor checks and the health gate are
functions over facts.
Sixteen of the seventeen are read from files the kernel writes, and wireless is the exception
that proves the rule rather than an erosion of it: the list of access points a radio heard is kept
in kernel memory and handed out on nl80211, and there is no file anywhere that holds it. So that
one fact speaks generic netlink, asks only when its root is / — a fixture machine answers not
asked rather than with what the developer’s own laptop can hear — and treats every name it reads
as a stranger’s bytes, because that is what an SSID is (ADR-0085). The bar for the next fact to
open a socket is that sentence: not that a socket is convenient, but that there is no file.
10. Memory
SQLite + FTS5 + sqlite-vec, one database held by the daemon at
/var/lib/tacid/memory/machine.sqlite, with layers rather than one database per person
(ADR-0017, amended). Layers session, project,
owner, machine; never-memory for secrets. Reading is scoped by principal: an integration
sees machine and project and not the owner’s layer or a session it is not in. 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.
A channel is three documents, not one. The manifest says what a revision is; alone it is also
what is current, which makes one key both. snapshot.json names the sha-256 of the exact
manifest bytes; timestamp.json names the sha-256 of the exact snapshot bytes and expires a day
after it was made (--timestamp-hours, for a publication that will not be read at the moment it is
made). A machine fetches the timestamp, checks the snapshot’s bytes against it, checks the
manifest’s bytes against the snapshot, and verifies each against the role its own keyring names —
Role::Timestamp and Role::Snapshot when it holds keys for them, Role::Channel when it does
not, which is every keyring shipped before these files existed. A stolen channel key can still sign
a manifest; it cannot make a machine current on it. A stolen timestamp key can freeze or lie about
freshness and cannot choose the tree.
Whether these documents may be skipped is decided from the machine’s own keyring and never from
what a server answered: a keyring that names either role refuses a channel that will not serve a
timestamp, because a 404 is something anyone serving the channel can produce and treating it as
an upgrade path would hand the downgrade to the leaked key.
tacid-update.timer polls
(hourly edge, daily stable) — and tacid update runs that same check now, by asking the keeper
to ask the stem to start that one unit — 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, @snapshots at /persist/.snapshots, and @swap when there is swap (ADR-0036, ADR-0054).
Impermanence by default; every tacid.services.* option declares its persistent directories.
Secure Boot is Lanzaboote behind tacid.core.boot.secureBoot.enable: a signed unified image per
generation, boot counting carried over from the same option, and enrolment in four steps that
tacid secureboot names one at a time — refused unless the machine already signs what it boots
(ADR-0080). Off by
default until the matrix certifies it.
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. A machine proposes its own withtacid propose body, composed from its firmware identity and thetacid.*lines of itsbody/hardware.nix; the queue writes the file itself, sincenix/is core. - Models: registry entries.
- MCP: Tacid exposes an MCP server; third-party servers register as untrusted principals.
- Themes: token files only.