TacidOS
Developers
Documentation

Decision 0079

0079. A name out of this machine's own registry is not a URL

Architecture decision record 0079: 0079. A name out of this machine's own registry is not a URL

Status
accepted
Date
2026-09-07

Status: accepted. Builds the fetching half of ADR-0016, and applies ADR-0077’s rule to a second operation.

Context

models/registry.toml names every model this machine is willing to run: its licence, where it comes from, how much memory it needs, and — once somebody has downloaded the file and checked it — its SHA-256. Entry::place streams bytes into a verified file and deletes what does not match. tacid models lists what would fit here.

Nothing fetched. A fresh machine could read about a mind and could not obtain one, and the only route to one was somebody copying a file in by hand — which is the act the registry exists to make unnecessary.

Two things were wrong before anything was written:

place wrote <name> and the supervisor opened <name>.gguf. Two answers to where a model lives, in the same crate, and nothing had noticed because nothing had ever placed a file. The first one placed would have landed where no supervisor looks.

source is a page, not a file. https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF is HTML. A fetcher pointed at it would have written a web page into a machine’s weights directory, and only the hash would have caught it.

Decision

The name, never a URL. ActionItem::ModelInstall { name } carries a name out of this machine’s own registry, and the registry is in the signed core. A caller who could name a URL could name any URL — and the hash would then be checking the arriving bytes against whatever that same caller said they should be, which is a check that always passes.

The registry gains file, beside source, and the two absences are one. source is the page a person reads; file is the artefact. Both file and sha256 are filled in by the same act — download it, check it, record both — so an entry with one and not the other is half of somebody’s work, and a test holds every shipped entry to having neither or both. Every entry ships with neither, which means the verb refuses every model in the shipped registry and says why. That is a true answer rather than a broken one.

One answer to where a model lives, in the crate all three sides may link. tacid_protocol::weights says it. The daemon writes the file and is the least-trusted of the three; the supervisor loads it and is agent-mutable, so nothing core may link it; the keeper records what landed and is core, so it may not link the supervisor’s registry. Left to themselves that is three implementations of one sentence.

The daemon fetches, because it is the only agent-side process with a route out. The supervisor has no network by design — a process with a GPU, a parser for hostile binaries and a way out is the one combination worth refusing outright — and the keeper has none either. So the bytes cross through the proxy like everything else: the host must be on the allow-list, what crosses is in the ledger and against the day’s budget, and the fetch streams, because a machine chosen because it has four gigabytes cannot hold five.

Redirects are followed by the client, not the proxy. Each hop is a fresh request through the proxy, so every host is checked and every hop is a ledger line. A proxy that followed one itself would reach a host nobody declared and write down the one that was.

The keeper looks; it does not compare. The work happens between the verdict and the keeper’s word — which is why Agent::submit_action and Agent::conclude_action are separate — and what the keeper does at the end is hash what is on disk and write that down. It cannot compare that to anything: the list of what a hash should be is the supervisor’s registry, and that crate is the agent’s to rewrite. What compares is the supervisor, at every start, before it loads a byte. A daemon that wrote something else gets a journal line naming a hash nothing will run.

It is asked about. Capability::ModelsInstall, its own rather than NetworkEgress or PackagesInstall: what it spends is gigabytes of a day and gigabytes of a disk, for a file the machine will then run text through, and it comes from whoever the registry names rather than from a channel this machine already trusts. Reversible, because what it produces is a file and a file can be deleted — a cost is not an irreversibility, and conflating the two is how a classifier ends up asking about everything.

Consequences

  • tacid models --install <name> exists, refuses every entry in the shipped registry, and names the reason. Filling in a file and a hash is the maintainer’s act; the path is proven against a fake file server on a real proxy socket, which is how everything else here is proven.
  • Three verifications, none trusting the others: the placer as it writes, the keeper before it records, the supervisor before it loads.
  • The models directory is the daemon’s to write and the supervisor’s to read. Its comment said weights arrive through the update system, which never fetched one.
  • EgressClient::fetch is the second thing that goes out of this machine, and the first that streams. FetchError is its own type: “the provider had no answer” is not a thing that can happen to a file.
  • A stream that runs past what the registry says the file is, is refused while it is arriving. A wrong file is bad; a wrong file that filled the disk arriving is worse.

Alternatives rejected

The keeper fetches. It would put consent, the act and the record in one process, which reads well. It also puts a network path into the one process that must never be compromised, and IPAddressDeny=any on the keeper is a line worth more than the tidiness.

Move the registry into core so the keeper can verify. Then the keeper could compare rather than merely record — and the model list, which is a curated preference about what a machine is willing to run, would become part of the constitution, and sha2, toml and a registry parser would be inside the process that authorises everything. The architecture’s own answer applies: what mind runs was never a trust boundary, which is why tacid-infer is mutable at all.

Let the item carry the hash, so the keeper can check against what was approved. Considered, and it is nearly right — the hash becomes part of what a person consented to. It buys little: a daemon that lied would put a hash a person cannot check by eye in front of them, and the file would still never load. The name alone keeps the item to one field with one meaning.

Fill in file for the shipped entries now. They are guessable, and a URL guessed from the shape of a repository is exactly the kind of thing that looks like a fact. The registry’s own preamble already says both fields are filled in at the moment somebody downloads and checks; writing one of them from memory would make that sentence false in the file that states it.