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.
Developers
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.
Source snapshot · 90fa069. Test totals are reported by the upstream project; they are not a hardware certification. Source access is currently limited to collaborators.
Components, principals, the protocol, the genome, transactions, authority, secrets, egress, facts, memory, updates.
ContractTwelve hard invariants enforced in the keeper and the stem, the threat model they answer, and how to report a finding.
HistoryThe reasoning behind each decision and the alternatives considered. When a record and the plan disagree, the record wins.
PlanPhases with acceptance tests, the repository layout, release engineering, and the backlog by epic.
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.
| Namespace | What 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. |
→ 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.
24 Rust crates form the current implementation. Open a component for its responsibilities and the test coverage described in the source snapshot.
tacid-protocolThe vocabulary and the wire: principals, provenance, transactions, verdicts, events, JSON-RPC with newline framing.
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.
tacid-policyThe deterministic authority: path safety, the reversibility classifier, taint, budgets, the engine and its hard invariants.
A sovereign machine still refuses ~/.ssh; a README cannot install software; a lookalike host does not pass the allow-list.
tacid-txThe genome renderer and a unified diff.
Rendering is deterministic and idempotent; a package name cannot inject Nix; every diff reconstructs its target.
tacid-auditThe 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.
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.
tacid-stem-protocol · tacid-stemThe 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.
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 {:?}.
tacid-listenHow a service receives its socket: from systemd when it was started for one, otherwise by making it.
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.
tacid-keeperPolicy, approvals, the journal, the health gate and automatic rollback, in their own process.
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.
tacid-modelThe provider trait, tool schemas, two models that need no weights, and two that reach a real one.
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.
tacid-egressThe 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.
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.
tacid-factsProprioception: 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.
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.
tacid-doctorWhat is wrong with the machine, in the one place both the report and the keeper's gate read from.
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.
tacid-memoryWhat the machine remembers: one database with four layers, full-text search, and a provenance label on every note.
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.
tacid-updateChannels, signed manifests, and the rules deciding when a machine may change itself.
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.
tacid-inferLocal minds: a registry of weights, profiles sized to the hardware, and a supervisor that treats an inference server as hostile.
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.
tacid-model::grammarWhat 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.
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.
tacid-sessionA 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.
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.
tacid-installGenesis: a device and a few answers become the stem's plan for a disk and the machine's first declaration, from one layout.
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.
tacid-proposeHow the machine changes its own source: a partition it cannot cross, and a queue only a person empties.
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.
tacid-daemonThe agent loop with real tools, the genome as a git repository, and the service every client shares.
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.
tacid-cliThe tacid command, the terminal interface, a development server, and the Model Context Protocol bridge that offers this machine to another agent.
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.
tacid-verbThe 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.
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.
tacid-designOne token file, every surface — down to the screen a machine wakes up on.
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.
tacid-shellThe 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.
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.
tacid-voicePush-to-talk: the stages a spoken turn passes through, their budgets, and the bounds on an utterance, with capture and playback behind traits.
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.
| Component | Binary | User | Trust | Constraint | State |
|---|---|---|---|---|---|
| Stem | tacid-stem | root | privileged | Typed operations only; keeper-only socket; no network. | built |
| Keeper | tacid-keeper | tacid-keeper | authority | Policy, classifier, taint, approvals, secrets, audit, health, snapshots. Denied all IP addresses. | built |
| Egress | tacid-egress | tacid-egress | gate | The only network path for agent processes; allowlist, credential injection, ledger, airgap. | built |
| Daemon | tacidd | tacid | agent | Sessions, the agent loop, facts, memory, transaction rendering and building, model clients. | built |
| Inference | tacid-infer | tacid-infer | hostile input | Supervises sandboxed inference servers behind a local OpenAI-compatible socket. | built |
| Session | tacid-session | the user | user | Per-user executor; untrusted parsers in sandboxes; voice; desktop interop. | built |
| Scribe | tacid-session --write | the user | writer | Keeper-only writer in a person's home: git, files, firmware flash, serial capture. | built |
| Shell | tacid-shell | the user | renderer | Wayland shell on Hyprland: bar, launcher, calendar, calculator, timer, notepad. | built |
| CLI and TUI | tacid | the user | renderer | Command line; the terminal interface with no arguments. | built |
The architecture defines these extension points. Check the current source and decision records before building an integration; interfaces are still evolving during development.
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.
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.
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.
A registry entry, signed. Providers implement one trait covering tool use, streaming, caching, structured output and multimodal input; local engines run as sandboxed subprocesses.
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 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.
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.
# 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
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.