TacidOS
DevelopersGitHub

Decision 0043

Logs are read as data, by a capability of their own

Architecture decision record 0043: Logs are read as data, by a capability of their own

Status
accepted
Date
2026-09-05

Context

The plan’s own eval names “explain a journal” and “diagnose a failing unit” among the first things a machine should do for its owner, and until now the agent had no way to read either: its tools read facts from the kernel, files a person owns, and generations, and the daemon’s user could not open the journal at all. “Why did sshd fail” was answered with the sentence that a model was needed and none was reachable, on a machine that had the answer in its journal.

Two things about a journal decide how it is read. It is written by programs, and a program can write anything — a line that reads as an instruction to whoever reads it next is a line a hostile program can write on purpose. And it can hold what a program should not have printed: a token, a path, a name. The first makes a journal untrusted content; the second makes reading it a capability an owner may want to withhold.

Prior decisions

ADR-0018 makes native tools typed and in-process for trusted data, and puts parsers of untrusted data in sandboxes. ADR-0010 says retrieved content is never an authority and that every context item carries a provenance label. ADR-0009 gives every kind of action a capability with a decision the owner sets.

Decision

Two read-only tools, one capability, and rules that reach them without a model.

journal.read hands the mind the last lines a unit wrote — fifty unless asked, two hundred at most — as journalctl prints them, whole, marked untrusted: what a program logged is data from the world. service.status hands it systemd’s own account of a unit — loaded, active, its result, how it last exited, how often it restarted — and nothing the unit wrote about itself, marked trusted as a fact is. A name that is not a unit’s never reaches either program, and a name without a kind is a service.

The daemon’s user joins systemd-journal, and a capability says whether it may use that: logs.read, auto by default, because a machine that cannot read its own logs cannot explain itself. An owner who sets it to ask or deny is refused with the reason at the call: there is no consent flow for a tool call, only for a change, and a log is read now or not at all.

The rules model, which answers when no language model can, turns “why did sshd fail”, “what happened to cups” and “show me the logs of nginx” into a journal read, and “is sshd running” and “status of cups” into a status, so the offline machine explains a failing unit from what it has.

Consequences

  • The lifecycle test starts a unit that fails on purpose and asks the machine why; the answer is the unit’s own line from the journal, and the status is systemd’s, with the exit code in it.
  • A journal line in the mind’s context is untrusted content, so a turn that read one is capped at level one for side effects, as ADR-0009’s taint rule says: a log cannot talk the agent into a change.
  • Tool visibility is not yet scoped by capability: a tool the policy refuses is still shown to the mind and refused when called. Scoping the toolbox to the policy in force is the next step the plan already names, and this record does not claim it.
  • The journal is read by the daemon’s own user, not through a person’s executor: the journal is the machine’s, and reading it as the machine is the honest form.

Alternatives rejected

  • Reading the journal through the session executor, as a person. The journal is not a person’s file, and a person’s executor reads that person’s files.
  • Marking journal text trusted because systemd wrote the file. Systemd wrote the file; the programs wrote the lines.
  • Scrubbing the journal before the mind sees it. The daemon holds no secrets to scrub for, by design; the owner’s capability is the honest control, and the proxy keeps the credentials a cloud model would otherwise carry away.