Markdown for LLMs
Logs, audit trail, and decision journals
The source Markdown for this article. Copy it into your assistant or download it as a text file.
# Logs, audit trail, and decision journals
## Goal
Separate the three records the deployment keeps — technical call log,
evaluation audit trail, operator decision journal — and say for each
what it holds, who reads it, and what happens when writing fails.
## Scope
Component `law-mcp` over HTTP (stdio journals nothing, real).
Scenario: a served `tools/call` and its afterlife in the logs. All
call ids, hashes, and package names below are synthetic examples.
## Applies to
| Branch | Coverage in this article |
|---|---|
| MCP (`law-mcp-server --http`) | call journal, audit fields, redaction list |
| `law serve` | decision records live in article 02 (journal mode), [Upgrades and compatibility](/operate/upgrades-compatibility/), [Rollback](/operate/rollback/) — shared journal, per-record pins |
## Prerequisites
- The journal switch state: enabled by default, `LAW_MCP_LOG=0`
disables recording (real, `main.rs`).
- A journald socket at `/run/systemd/journal/socket` if native fields
are wanted; otherwise records fall back to stderr JSON lines
(real, `journal.rs`). `LAW_MCP_LOG_SOCKET` overrides the socket
path (real, test seam).
- `journalctl` access on the host to read native records
(operator-policy-example reader; the server ships no reader).
## Steps
1. Distinguish the three records before configuring anything:
- Technical call log: one journal record per call or refusal,
written by the transport (`reqlog.rs` + `journal.rs`).
- Evaluation audit trail: inside the answer itself — statuses,
applied rules, provenance hashes, `replayControl`; replays via
`law_explain` / `law_inspect`, not via the log.
- Operator decision journal: human entries (who approved what
publication, which retention ran). The engine keeps none; the
operator keeps it as a file or ticket trail
(operator-policy-example, no code writes it).
2. Read a call record. Real fields (`reqlog.rs`, `journal.rs`,
`trace.rs`): `MESSAGE` (`http <method> <tool> ok|<code> <ms>ms`),
`PRIORITY` (`6` ok / `3` failed / `4` refused or overrun-finished),
`SYSLOG_IDENTIFIER=law-mcp`, `MCP_TRANSPORT=http`,
`MCP_PROFILE`, `MCP_CALL_ID` (32 hex), `MCP_PORT`,
`MCP_CLIENT`, `MCP_METHOD`, `MCP_TOOL`, `MCP_OK`,
`MCP_CODE`, `MCP_MS`, `MCP_BYTES_IN`, `MCP_BYTES_OUT`,
`MCP_RPC_ID`, hashes `MCP_PROGRAM_HASH`, `MCP_CASE_HASH`,
`MCP_RESULT_HASH`, `MCP_CODE_HASH`, stages `MCP_STAGES` and
`MCP_MS_<STAGE>`, bodies `MCP_REQUEST`/`MCP_RESPONSE`, and flags
`MCP_TRUNCATED` / `MCP_REDACTED`. `law_report` additionally
spreads `MCP_REPORT_PACKAGE`, `MCP_REPORT_CATEGORY`,
`MCP_REPORT_FRAGMENT`, `MCP_REPORT_PREDICATE`,
`MCP_REPORT_CALL_ID` (real).
3. Know what enters the bodies. `MCP_REQUEST`/`MCP_RESPONSE` carry the
raw JSON-RPC bytes capped at 64 KiB per side by default
(`LAW_MCP_LOG_MAX_BODY`, `0` uncaps, real). Case facts ARE in the
logged bytes when they fit — the journal is not a facts-free
zone. Truncation is marked (`MCP_TRUNCATED=request,response`,
real).
4. Know what is redacted. Exactly three terminal keys are replaced
with `[REDACTED]`: `revokeSecret`, `LAW_ANSWERS_TOKEN`,
`ANSWERS_INTERNAL_TOKEN`, and the record gains
`MCP_REDACTED=answers-secret` (real, `reqlog.rs` `redact`). Same-named
keys in a `tools/list` schema keep their descriptions; only
terminal values hide. Nothing else is redacted — notably,
`LAW_NEURAL_TOKEN` is *not* on the redaction list, so keep it out
of logged tool arguments (current gap, not a policy choice).
5. Replay from the audit trail, not the log. The journal's hashes
(`MCP_RESULT_HASH`, `MCP_CODE_HASH`) plus the original question
re-run through `law_explain` reproduce the proof chain; the
server re-executes and refuses on hash mismatch (reference
behavior of `law_explain`). `GET /healthz` leaves no journal
record (real): health polls are invisible by design.
6. Record operator decisions separately. Created-example entry:
`2026-10-03T09:15Z op:salim prepared p_9f2 (24h), publish only
after counsel sign-off; journal archived to w42`. The format is
the operator's; the engine never reads it.
## Expected result
- Every served call and every transport refusal has exactly one
journal record, correlated across journald, OTLP spans, and the
`X-Law-Call-Id` response header by the 32-hex call id (real,
`otlp.rs`). An edge `X-Request-Id` is accepted into the record
only if short (≤64) and alphanumeric (real, `journal.rs`).
- A timed-out computation that later finishes emits a second record
with `MCP_CODE=OVERRUN_FINISHED` and priority `4` (real,
`http.rs`).
- The answer bytes never depend on logging: disabling the journal
changes no answer (real invariant, `journal.rs` header).
## Result check
- `LAW_MCP_LOG_SOCKET=<created-example test socket>` plus one call:
the receiver gets a real datagram decodable field-by-field (real
seam, covered by `journal.rs` tests).
- A `law_revoke_answer` call journal line contains `[REDACTED]` and
`MCP_REDACTED=answers-secret`, never the secret (real).
- `journalctl --namespace=law-mcp -u law-mcp@8722 MCP_TOOL=law_ask
-o json --all` (operator-policy-example unit name; field names
real) lists only `law_ask` calls.
## Failures and diagnostics
- Journal write failure never fails the call: send fails → resend
without bodies with `MCP_TRUNCATED=oversize`; no socket → stderr
JSON line (real, `Journal::emit`). Silence in journald with lines
on stderr means the socket path is wrong or journald is down.
- OTLP receiver down or slow: the call never waits; spans queue
(256, real) then drop, and the drop counter prints to stderr once
per hundred (real, `otlp.rs`).
- Missing `MCP_RESULT_HASH` on catalog/search answers is by
construction (real, `reqlog.rs`): those tools carry no
reproducibility provenance.
## Support boundaries
- Access rights to records are the host's: journald ACLs, file modes
on the stderr capture, OTLP receiver auth. The server enforces no
read policy on logs it already wrote.
- With HTTP journaling on (default), bodies persist — so retention,
backup, and cleanup of journal records follow the same case-data
policy as the case directory itself (article 08): journald
`MaxRetentionSec`/rotation, who can read the stderr capture and
the OTLP store, what the backups include, and how records are
purged. Turning journaling off stops future copies; it does not
erase records already collected.
- Integrity: records carry no signature or hash chain in code. Tamper
evidence is the host's (journald sealing/forwarding,
append-only storage) — an operator-policy-example layer, not an
engine guarantee. "Verified" in this article means only: hashes in
the answer re-execute to the same bytes via `law_explain`.
- The journal is not a backup (bodies truncate) and not the audit
trail (replay needs the question + `replayControl`).
## Next step
- Article 11 ([Resource limits and cancellation](/operate/resource-limits-cancellation/)) for the timeout/overrun mechanics behind
`OVERRUN_FINISHED`; article 12 ([Health and observability](/operate/health-observability/)) for OTLP span contents and the
health endpoint.