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 · dcb3832. 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.
21 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.
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 and the secret scrubber.
Editing, removing and reordering records are each detected by name; a store path survives scrubbing untouched.
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.
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, 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.
tacid-factsProprioception: what this machine is, read from /proc, /sys and /run.
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.
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.
tacid-memoryWhat the machine remembers: 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; 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-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.
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.
The thirteen end-to-end tests, including the Wireshark scenario; and an eleven-case injection corpus in which the model obeys the attacker every time and the machine still refuses.
tacid-cliThe tacid command, the terminal interface, and a development server.
It is what the session above is driving; the interface is rendered into a buffer and asserted at 60, 80, 100 and 132 columns.
tacid-designOne token file, every surface.
The palette's own contrast requirements are tests, and they failed until the palette was fixed.
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 |
| Shell | tacid-shell | the user | renderer | Wayland shell for the Niri session. | planned |
| 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 automatically, so a new tool cannot quietly 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 finds a quirk on its own body can file it as a proposal for everyone else’s.
A token file, and only a token file. There is nowhere else to put a colour, which is the point.
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.