TacidOS
Developers
Documentation

Decision 0008

The agent writes typed `tacid.*` options, never arbitrary Nix

Architecture decision record 0008: The agent writes typed `tacid.*` options, never arbitrary Nix

Status
Accepted
Date
2026-09-03

Context

Small models are competent when the surface is small and typed; validation errors must be messages a model can act on; the owner’s own configuration must never be overwritten.

The three plans

  • Grok: a TOML tree with a schema; the model is offered only the tools it may use.
  • Fable: the single most important design decision: a small set of well-typed options with descriptions, examples and validation; tacid.core and tacid.agent read-only to the agent; tacid.raw as a loud escape hatch; a curated services allow-list with one VM test each.
  • Codex: typed transactions rendered into a generated area; curated settings, not every option.

Decision

Fable’s surface, rendered from Codex’s transactions. Modules: tacid.core (read), tacid.agent and tacid.policy (read; owner-signed), tacid.packages, tacid.services (allow-listed with curated options and a VM test per service), tacid.hardware (udev profiles curated; raw rules need review), tacid.desktop, tacid.dev, tacid.user, and tacid.raw at level 3 or higher. Every option has description, example and validation; evaluation errors are returned to the model verbatim. Every tacid.services.* option declares its persistent directories (impermanence, review). Tacid does not depend on Home Manager; owner.nix may import it.

Consequences

  • Adding a service is a module, its state declaration and a VM test; nothing merges without them.
  • Beyond the surface: pkgs/ derivations (ADR-0001) and tacid.raw.

What was built, and how the escape hatch differs. The writable modules are as decided. tacid.raw is not an option; the escape hatch is ChangeItem::RawNix, a change item carrying the justification for why the typed surface was not enough, refused for any agent at any autonomy level, and rendered into mind/agent.nix with that justification as the comment above it. Three differences from this record, each of them the better answer: a change item goes through the classifier, the preview and the journal, where an option set directly does not; the owner is not gated on level 3, because the autonomy level says how much the agent may do without asking and not how much authority the owner has — an owner at level 0 has given the agent none, not themselves less; and it shares the agent’s file, because what makes raw Nix visible is the justification beside it and the git log, not a filename. What did need correcting is the shape of the rule rather than its content. It was enforced as a list of roots the agent may not write, and the list said tacid.agent — a module this record describes and nobody built — while the root that decides who the owner is, tacid.owner, was in neither column. So the one option that decides who may approve anything was the one option nothing refused. The rule is the writable list now, read forwards: what is not on it is the owner’s, and a module root invented tomorrow is the owner’s until somebody decides otherwise.

Alternatives rejected

  • Free-form Nix for the agent: unbounded blast radius, unverifiable diffs, unhelpful errors.
  • A GUI over every Nix option: the opposite of curation.