TacidOS
Developers

Developers

Build on a system
you can inspect.

The architecture, interfaces, and decisions behind TacidOS. Start with the change lifecycle, understand the trust boundaries, and find the part you want to work on.

Crates
24
Reported tests
1655
Decisions
99
Status
Pre-release

Source snapshot · 90fa069. Test totals are reported by the upstream project; they are not a hardware certification. Source access is currently limited to collaborators.

Start here

The protocol

JSON-RPC 2.0, newline-delimited, over Unix domain sockets, with file descriptors passed for audio, pseudo-terminals and blobs, and notifications for streams. One crate defines every message and publishes a versioned JSON Schema. The same messages travel over SSH or a tunnel, with the remote peer as its own principal.

NamespaceWhat it carries
session.*ask, cancel, history, events — a conversation or a job bound to a principal.
tx.*propose, preview, apply, undo, list. The whole change API.
facts.*The typed live model of the machine, derived from the kernel and systemd rather than parsed shell output.
memory.*Per-user memory with provenance on every row.
models.* · providers.*Which minds are available, and which the owner has configured.
policy.* · approval.* · journal.* · health.*The keeper. Approvals reach it directly from the client.
update.* · device.* · project.* · voice.*Channels, the hardware graph, project environments, speech.
stem.*Keeper to stem only. A closed vocabulary, no command lines, no interpolation.
Illustrative protocol exchange
→ session.ask
  { "session": "s_1",
    "text": "install wireshark and let me
             capture without root" }

← session.event  plan
← session.event  transaction
  { "id": "tx_…", "kind": "change",
    "needs_approval": true }

  (the client connects to keeper.sock)

→ approval.grant
  { "transaction": "tx_…",
    "scope": "once" }

← session.event  activated
  { "generation": 188, "health": "pass" }

← result
  { "summary": "Wireshark is installed;
      your user is in the wireshark group.",
    "journal": "j_…" }

Note where the approval goes. It is granted by the client to the keeper, bound to that transaction id, so the daemon that proposed the change is not on the path that consents to it. That single arrow is most of the security model.

The workspace

24 Rust crates form the current implementation. Open a component for its responsibilities and the test coverage described in the source snapshot.

01tacid-protocol

The vocabulary and the wire: principals, provenance, transactions, verdicts, events, JSON-RPC with newline framing.

Coverage described in the source

A hostile string cannot break out of a Nix literal; an oversized frame is refused rather than buffered, and the stream it arrived on is finished rather than left to be read again for ever.

02tacid-policy

The deterministic authority: path safety, the reversibility classifier, taint, budgets, the engine and its hard invariants.

Coverage described in the source

A sovereign machine still refuses ~/.ssh; a README cannot install software; a lookalike host does not pass the allow-list.

03tacid-tx

The genome renderer and a unified diff.

Coverage described in the source

Rendering is deterministic and idempotent; a package name cannot inject Nix; every diff reconstructs its target.

04tacid-audit

The hash-chained journal, sealed to a counter the machine cannot wind back and witnessed by a line the owner keeps off it, and the secret scrubber: by shape everywhere, and by identity for what a person registered.

Coverage described in the source

Editing, removing and reordering records are each detected by name; a truncated journal still verifies, no longer matches the seal, and no longer matches the line its owner carried away — which is the one check a root that drives the machine's own chip cannot answer; a store path survives scrubbing untouched; a registered value can be named and forgotten without ever being shown.

05tacid-stem-protocol · tacid-stem

The only process that runs as root, and the closed vocabulary it speaks — including the six operations that make a machine: lay out a disk from a typed plan, seal its key to the TPM behind a PIN, enrol the recovery key it hands back once, write the first genome, write the owner's key into their home, install a built system.

Coverage described in the source

No operation carries a command line; a stranger is refused over the real wire; guarded operations need a human; the disk is laid out in the order a machine mounts it, and the passphrase reaches cryptsetup on standard input and appears in no argument, no report and no {:?}.

06tacid-listen

How a service receives its socket: from systemd when it was started for one, otherwise by making it.

Coverage described in the source

The one adoption safe Rust cannot express lives outside every Tacid crate; a stale socket is replaced, not refused; without a manager nothing is inherited.

07tacid-keeper

Policy, approvals, the journal, the health gate and automatic rollback, in their own process.

Coverage described in the source

The agent cannot approve its own work; consent for one transaction cannot apply another; a failed health gate restores the previous generation unasked; and a machine is refused the one act it cannot take back — telling its firmware to trust its keys — unless it already signs what it boots.

08tacid-model

The provider trait, tool schemas, two models that need no weights, and two that reach a real one.

Coverage described in the source

A completion cannot express a shell command; the rules model still installs a package with no network; a proposal the protocol cannot express is refused in the adapter, not downstream.

09tacid-egress

The only way out: allow-list, credential injection, airgap, the owner's day budget, the values they registered as never to send, and a hash-chained ledger of every byte.

Coverage described in the source

A certificate for the wrong name is refused; the ledger holds the handle and never the key; raising a flag stops the machine talking mid-conversation; a day's budget spent survives a restart of the process that keeps it; a registered value is refused in a body, in a header and in a query string, and the refusal names it by fingerprint without quoting it.

10tacid-facts

Proprioception: what this machine is, read from /proc, /sys and /run — and, from the two files a system carries, what it has been asked to be and what it can be asked for.

Coverage described in the source

Loop devices are not disks; an unknown Secure Boot state is not reported as disabled; a machine with no systemd says so rather than reporting nothing running; a unit that started and died is not running, however long systemd keeps its invocation file; a machine with no catalogue says so rather than saying it can do nothing.

11tacid-doctor

What is wrong with the machine, in the one place both the report and the keeper's gate read from.

Coverage described in the source

A skipped check is never counted as a pass; the gate and the report are asserted to agree about the same machine; a machine that cannot be measured is not rolled back; a daemon that started and died fails the gate, which is how a bad generation gets abandoned; and a boot screen with no driver in the initrd is reported, which is a failure with no failed unit anywhere — plymouth starts, finds no device, and exits.

12tacid-memory

What the machine remembers: one database with four layers, full-text search, and a provenance label on every note.

Coverage described in the source

A secret is not stored safely, it is not stored; the agent cannot write the owner's layer; something a person plugged in reads the machine and project layers and not what the owner confided; a forgotten memory leaves the search index too; FTS5 syntax cannot be smuggled through a search.

13tacid-update

Channels, signed manifests, and the rules deciding when a machine may change itself.

Coverage described in the source

Rollback, freeze and cross-channel attacks each have a test named after them; a signature is checked before anything in the document is believed; a reboot is offered, never taken.

14tacid-infer

Local minds: a registry of weights, profiles sized to the hardware, and a supervisor that treats an inference server as hostile.

Coverage described in the source

No hash, no install; a model that does not fit is not offered; the weights' own chat template is never honoured; a server that keeps dying is reported rather than resurrected.

15tacid-model::grammar

What holds a small local model to a tool call: this turn's tool schemas compiled to GBNF, sent on the request, admitting one call or one spoken answer and nothing else.

Coverage described in the source

Every reference defined and every rule reachable, on every catalogue in the tree — the two ways a generated grammar is wrong; a hostile tool name from an untrusted MCP server cannot introduce a rule and round-trips back to itself; a constrained reply becomes the same typed completion the remote path produces from a different field. Not proven: that a real llama.cpp accepts it, because nothing here starts an engine.

16tacid-session

A person's executor: their files, read as them and never as the agent, and untrusted content opened in a sandbox with no network, nothing writable, and one readable file — and, on a second socket only the keeper may open, the scribe that writes.

Coverage described in the source

A caller that is neither the daemon nor the person is refused by its socket credentials before any method is looked at; a link out of the home is followed to where it goes and refused there; the executor refuses a protected file a talked-around daemon would have allowed; a file is what its bytes say, not what its name says; a file called --bind is a file.

17tacid-install

Genesis: a device and a few answers become the stem's plan for a disk and the machine's first declaration, from one layout.

Coverage described in the source

Only a stable whole-disk path is ever written to; the plan and the declaration name the same root; encryption without a passphrase is refused rather than skipped; a hostile answer cannot break out of the Nix it lands in; the rendered configuration balances at every combination of options.

18tacid-propose

How the machine changes its own source: a partition it cannot cross, and a queue only a person empties.

Coverage described in the source

A change to the keeper cannot be *filed*, not merely rejected; an unclassified crate is core; a signature does not survive the diff being swapped.

19tacid-daemon

The agent loop with real tools, the genome as a git repository, and the service every client shares.

Coverage described in the source

Twenty end-to-end tests, the Wireshark scenario among them; a fourteen-case injection corpus in which the model obeys the attacker every time and the machine still refuses; and a session that belongs to whoever opened it, for speaking as well as for hearing.

20tacid-cli

The tacid command, the terminal interface, a development server, and the Model Context Protocol bridge that offers this machine to another agent.

Coverage described in the source

It is what the session above is driving; the interface is rendered into a buffer and asserted at 60, 80, 100 and 132 columns; an integration takes its own authority down before it asks anything, and cannot put it back.

21tacid-verb

The deterministic verbs: one per typed change, and no mind between. Its own crate because three surfaces take these words — tacid do …, :do … in the terminal interface, and the command surface on the screen — and the third is a workspace of its own that could not see a module in the second.

Coverage described in the source

One parser, so install ripgrep cannot mean two things depending on where it was typed, and a verb added here appears on all three at once. On the core side of the self-improvement partition, because a fallback the model may redefine is not a fallback.

22tacid-design

One token file, every surface — down to the screen a machine wakes up on.

Coverage described in the source

The palette's own contrast requirements are tests, and they failed until the palette was fixed; the exemptions from "every surface carries the accent" are named rather than counted, because the boot screen writes colours as fractions and was excused by that rule in silence the day it arrived; and the boot screen itself is drawn by plymouth under an X server on every push and measured — the ground to the byte, which caught plymouth truncating a fraction into a byte and painting #0d1013 one level dark on every channel.

23tacid-shell

The face: a layer-shell client with a bar, a command surface and an agent panel, in its own workspace so a machine with no screen never builds a renderer.

Coverage described in the source

It renders what the daemon says and can change nothing the command line cannot — and, since the verbs moved into tacid-verb, nothing *less* either: :do install ripgrep in the command surface is the same words, the same parser and the same refusals as tacid do install ripgrep at a shell, which is what a machine with no model needs from its screen. A self-built machine admits it on every screen; the bar is drawn into a buffer and asserted; :do is typed at the surface with a real keyboard and the answer read off the pixels; and the compositor's own parser reads the session configuration the module writes.

24tacid-voice

Push-to-talk: the stages a spoken turn passes through, their budgets, and the bounds on an utterance, with capture and playback behind traits.

Coverage described in the source

An utterance is bounded before anything is allocated for it; the measured stages are held against a published budget rather than against each other; nothing spoken is ever trusted, which is why a rewritten surface cannot make the machine act on what it hears; and on a booted machine with a real sound card the microphone is open exactly while the key is held, a key that never comes up does not leave one open, and what was played into the card is what came back out of it. Since ADR-0091 a turn can end in *words*: Transcriber is a trait, Transcript says Said or Unasked rather than an Option that names neither, and the whisper.cpp client — in the executor, because that is the process holding the microphone — is asserted against a real HTTP server on a real Unix socket. **No machine built from this tree has a model**, so every turn here still ends in Unasked and the loudness; one row in models/registry.toml is the remaining distance.

The graphical shell and voice interface remain planned. Some crates provide libraries as well as services; an implemented component does not by itself mean a supported release.

Process model

ComponentBinaryUserTrustConstraintState
Stemtacid-stemrootprivilegedTyped operations only; keeper-only socket; no network.built
Keepertacid-keepertacid-keeperauthorityPolicy, classifier, taint, approvals, secrets, audit, health, snapshots. Denied all IP addresses.built
Egresstacid-egresstacid-egressgateThe only network path for agent processes; allowlist, credential injection, ledger, airgap.built
DaemontaciddtacidagentSessions, the agent loop, facts, memory, transaction rendering and building, model clients.built
Inferencetacid-infertacid-inferhostile inputSupervises sandboxed inference servers behind a local OpenAI-compatible socket.built
Sessiontacid-sessionthe useruserPer-user executor; untrusted parsers in sandboxes; voice; desktop interop.built
Scribetacid-session --writethe userwriterKeeper-only writer in a person's home: git, files, firmware flash, serial capture.built
Shelltacid-shellthe userrendererWayland shell on Hyprland: bar, launcher, calendar, calculator, timer, notepad.built
CLI and TUItacidthe userrendererCommand line; the terminal interface with no arguments.built

Extension points

The architecture defines these extension points. Check the current source and decision records before building an integration; interfaces are still evolving during development.

Extend the option surface

Adding a service means a tacid.* module with curated options, declared persistent directories, and a virtual-machine test. The module surface is the agent’s vocabulary: if it is not an option, the agent cannot ask for it.

Add a tool

A typed Rust tool with a schema, a provenance rule and a policy category. Output derived from untrusted input inherits the untrusted label, so adding a tool does not widen what a tainted turn may do.

Add a doctor check

A function over facts that returns a proposed fix as a transaction. Diagnostics and repairs are the same object, so a check can never suggest something the change API would refuse.

Add a model

A registry entry, signed. Providers implement one trait covering tool use, streaming, caching, structured output and multimodal input; local engines run as sandboxed subprocesses.

Add hardware

A quirk module under nix/hardware/ with the facts that identify the machine. An agent that identifies a hardware quirk can submit it as a proposal.

Add a theme

Add a token file. Authored source does not accept hard-coded colours.

Tacid also exposes an MCP server of its own. Third-party servers register as untrusted principals with their own policy, and cannot escalate through the agent that called them.

Run the loop in a terminal

The development demo does not require Nix or model weights once the source and dependencies are available. Start with the setup guide for requirements and commands.

View the historical CLI recording
tacidthe founding demonstration43da513
# Reversible, so the agent acts. The diff is the genome, not a summary of it.
$ tacid ask --diff install ripgrep
I will install ripgrep.

  + install ripgrep (cli)

  Reversible: system configuration to generation 1.

--- a/mind/agent.nix
+++ b/mind/agent.nix
@@ -5,4 +5,7 @@
 # transaction. Your own configuration belongs in owner.nix, which Tacid never writes.
 { ... }:
 {
+  tacid.packages.cli = [
+    "ripgrep"
+  ];
 }

# Exposure cannot be undone, so the agent stops and says exactly why.
$ tacid ask enable openssh
I will enable openssh.

  + enable the openssh service

  This cannot be fully undone:
  ! enabling openssh would make this machine reachable from the network

  Reversible: not by a generation rollback alone.

  Waiting for you. Approve it with the keeper, or say no and nothing happens.

# The hash-chained journal. The refusal is a record too.
$ tacid history
History:
     6  asked     the owner's policy asks before services.enable
     5  proposed  enable openssh (enable the openssh service)
     4  concluded applied as generation 2
     3  health    passed
     2  activated generation 2 (switch, from 1)
     1  allowed   every effect of this change can be undone
     0  proposed  install ripgrep (install ripgrep (cli))

# One command back to the previous generation.
$ tacid undo
I have undone the last change and switched the machine back.

# Provenance, including whether this machine built its own code.
$ tacid version
TacidOS 0.0.1 (dev)
  protocol   0.1
  autonomy   reversible
  generation 3
  self-built this machine is running code it built itself
Development recording from tacid-cli at revision 43da513on . This historical demo uses a simulated Nix builder and privileged operations. It shows the change flow, not a production installation.