Decision 0008
The agent writes typed `tacid.*` options, never arbitrary Nix
Architecture decision record 0008: The agent writes typed `tacid.*` options, never arbitrary Nix
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.coreandtacid.agentread-only to the agent;tacid.rawas 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) andtacid.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.