TacidOS
Developers
Documentation

Decision 0074

0074. A corpus is made of the things that must not leave

Architecture decision record 0074: 0074. A corpus is made of the things that must not leave

Status
accepted
Date
2026-09-07

Status: accepted. Builds three of the four parts ADR-0017 named, and says plainly which one is not built.

Context

PLAN.md §3.11 has promised since the founding: opt-in, sanitized tool-trace collection with a published sanitizer specification and a shown-before-upload bundle feeds tune/, which fine-tunes a small Tacid tool-use model. The reasoning behind it is sound and is Grok’s: a small model fine-tuned on this machine’s own tools beats a large generic one at running this machine, because tool reliability beats Elo.

The item carried no note, which by PLAN.md’s own preamble means it is not built, and it was not.

The difficulty is not the pipeline. It is that a tool-use trace is made of the things that must not leave a machine: the sentence somebody typed, the path they typed it about, the name of their machine. The obvious rule — remove what is risky — removes the dataset with it. [redacted] teaches nothing.

Decision

Identity is substituted. Everything else is kept, and a person reads it before it goes.

Substituted rather than deleted, because /home/<user>/project/.gitignore still teaches what [redacted] does not. Three rules follow from that one, in the order they run:

  1. Secrets first, through the same scrubber that runs on every tool result: registered values by identity, known credential shapes by pattern. A secret that survived into a placeholder would have been carried past the one filter written to catch it.
  2. Whole shapes before the identifiers inside them. dom@example.com is an email, and substituting the account name first leaves <user>@example.com — which still names somebody’s employer and looks to a reader like the rule worked. MAC addresses go before IP addresses, because 52:54:00:12:01:02 is hex digits and colons and so is an IPv6 address. Both orderings were found by a test that failed, not by reasoning.
  3. Then this machine’s own identifiers, longest first and matched as whole words, because an account called dom is inside domain, random and freedom.

127.0.0.1 stays: it is the same address on every machine there has ever been, it is most of what a local Tacid talks to, and a corpus with it replaced would teach a model that the inference server on this machine lives somewhere unknown.

Nothing here uploads. tacid trace export writes a file; a person hands it over. A machine that could send this on its own would be a machine whose privacy rested on its own good behaviour.

Three conditions on keeping a turn at all, and only two of them are the owner’s:

  • there is a store,
  • tacid.policy.keepTraces is on — off by default, and only the owner may write it,
  • and the turn touched nothing untrusted.

The third is not in the policy and is not the owner’s to waive. A turn that read a hostile page carries that page’s words in its context, and a corpus built out of those is one somebody wrote on purpose. Taint has stopped changes and memory writes since the founding; a dataset is the same question with a longer fuse.

The specification is executed. docs/sanitizer.md is what a person reads before agreeing to any of this, and its examples are a table a test runs: every row is put through the sanitizer, against the identity the document itself states, and must produce exactly what the document promises. A rule changed without the table changing fails; a table changed without the rules changing fails too. Four documents in this tree have already drifted from the code they described, and this is the one where drift would be a person consenting to something else.

Consequences

  • Turns are stored as they were said — the sentence, the paths, the arguments — in /var/lib/tacid/traces/, one JSON document per line. That is deliberate: a person reviewing what their machine kept must read what was actually said, not a rendering of it. Identity is substituted when a bundle is made. The store never leaves the machine, and tacid trace forget empties it.
  • With one exception, added after the fact: a credential is not written down. The two halves of sanitizing are separated for opposite reasons. Substituting identity on the way in would leave a reviewer unable to check the text against what was said. Removing a secret on the way in loses nothing a reviewer needs — nobody reads a token verbatim to decide whether a trace is safe to send — and a corpus is a new place on a disk where everything a person typed sits at rest. Every other such store on this machine filters, and an exception is how a rule stops being one. It is done field by field rather than over the serialised line, because a value with a quote or a backslash in it is escaped differently in JSON than it was registered, and a filter run over the text would miss exactly the awkward secrets.
  • The daemon holds it, beside what the machine remembers rather than beside what it did. A journal and a dataset must never be the same file: one is a record the agent cannot modify, the other is a corpus a person may delete at will.
  • tune/ itself is not built. No directory, no pipeline, nothing fine-tuned, no such model in the registry. What is built is the half that runs on a machine’s trust path and therefore had to be Rust; the half that is Python and offline is not written, and PLAN.md’s item stays unticked with a note saying which half is which.
  • A sanitizer cannot know that “the Bergmann account” is a name. Free text carries people, employers, customers and projects, and no rule finds them. That limit is in the module’s own documentation, in the specification, and in what tacid trace export prints. It is why collection is opt-in and why the last check is a person reading the file.

Alternatives rejected

Sanitize on the way in. Cheaper, and it would leave a person reviewing text they cannot check against what was said — which makes the review, the one mechanism that catches what no rule can, a formality.

Send bundles automatically once the owner opts in. The obvious convenience, and it converts a decision about one file into a standing permission over everything the machine ever records. Every argument for local-first is an argument against it.

Keep only the shapes of arguments — a path becomes “a path”, a unit becomes “a unit”. Safe and useless. What a tool-use dataset needs is which tool, with which arguments, for which sentence; a corpus of shapes teaches a model to call filesystem.read on the concept of a file.

Classify sensitivity with a model. A classifier is a guess, and a privacy guarantee resting on a guess is a guarantee that fails silently on the case nobody tested. Substitution by identity is exact where it applies; where it does not apply, a person reads.