# 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 ok| 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_`, 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=` 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.