Seven task files for the implementing model under docs/plans/M1/, with the test files, byte-exact fixtures, Makefile, deny.toml and gate-script self-test they copy into place. All of it was verified against a private reference implementation: the gate passes after every task in order. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
4.6 KiB
M1 task 06: proto audit and session log records
Branch: m1 (run git switch m1; git status --short must be empty, otherwise stop)
Commit subject: Add audit and session log record types to proto
Goal
Add the record types for the two log files. Each log is JSONL: one JSON object per line. This task adds only the types. Writing files, hashing and locking come in later milestones.
Context
From the design brief: "Audit log: append-only JSONL, hash-chained, one record per decision
including denials." and "sessions/<id>/<epoch>.jsonl — append-only session log". The assistant's
messages are stored "exactly as the server returned" them, because the next request must repeat
them byte for byte or the inference server's cache is lost.
The hash chain hashes the exact bytes of each stored line, so a record must always encode to the same bytes. That is why the fixtures are compared byte for byte in both directions.
One audit line:
{"seq":1,"time":"2026-09-17T08:05:01.250Z","prev":"000102…1e1f","session":"mm-thread-42","call":2,"tool":"shell","arguments":"{\"command\":\"df -h\"}","session_taint":"private","decision":{"outcome":"approved","grant":"shell-ask","approver":"u8f3k2","post":"p9x7"}}
Files
- Copy:
crates/proto/tests/records.rs,crates/proto/tests/fixtures/records/(2 files) - Create:
crates/proto/src/audit.rs,crates/proto/src/log.rs - Modify:
crates/proto/src/lib.rs,docs/implementer-log.md
Interfaces
Consumes from tasks 02 and 03: CallId, DataClass, DenyReason, Epoch, Hash32, SessionId,
Timestamp.
Produces, re-exported from the crate root. Keep fields and variants in exactly this order.
// crates/proto/src/audit.rs
// JSON: {"outcome":"allowed","grant":"…"} ; the tag sits beside the fields; outcomes are snake_case
pub enum DecisionRecord {
Allowed { grant: String },
Approved { grant: String, approver: String, post: Option<String> },
Denied { reason: DenyReason, grant: Option<String> },
}
pub struct AuditRecord {
pub seq: u64,
pub time: Timestamp,
pub prev: Hash32,
pub session: SessionId,
pub call: CallId,
pub tool: String,
pub arguments: String,
pub session_taint: DataClass,
pub decision: DecisionRecord,
}
// crates/proto/src/log.rs
pub struct ToolCall { pub id: String, pub name: String, pub arguments: String }
// JSON: {"type":"user","time":"…","content":"…"} ; the tag sits beside the fields; types are snake_case
pub enum LogRecord {
SessionStart { time: Timestamp, session: SessionId, epoch: Epoch, slot: u32, baseline: Hash32 },
User { time: Timestamp, content: String },
Assistant { time: Timestamp, content: Option<String>, reasoning_content: Option<String>,
tool_calls: Vec<ToolCall> },
ToolResult { time: Timestamp, call: CallId, tool_call_id: String, content: String,
class: DataClass, untrusted: bool, truncated: bool },
CacheLoss { time: Timestamp, expected: u64, got: u64 },
EpochEnd { time: Timestamp, next: Epoch, summary: String },
}
Derives: every type gets Debug, Clone, PartialEq, Eq, Serialize, Deserialize.
Rules the tests check: a None is written as null, never left out (so do not use
skip_serializing_if). Unknown fields and unknown type or outcome values are errors.
API notes
Internally tagged enums and deny_unknown_fields work as in task 03:
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)].
Steps
- 1. Copy the test and fixtures.
git switch m1
cp docs/plans/M1/files/crates/proto/tests/records.rs crates/proto/tests/
cp -r docs/plans/M1/files/crates/proto/tests/fixtures/records crates/proto/tests/fixtures/
-
2. See the test fail.
cargo test -p proto --test records. Expected: it does not compile. -
3. Write
audit.rsandlog.rs, and add tolib.rs(alphabetical order):
pub mod audit;
pub mod log;
pub use audit::{AuditRecord, DecisionRecord};
pub use log::{LogRecord, ToolCall};
-
4. See the test pass.
cargo test -p proto --test records. Expected:3 passed; 0 failed. -
5. Run the gate.
make gate. Expected last line:gate: ok. -
6. Log and commit.
git add crates/proto docs/implementer-log.md
git commit
Done when
cargo test -p proto --test recordsreports 3 passed.cargo test -p protoreports 40 passed in total across the five test files.make gateprintsgate: ok.diff -r crates/proto/tests docs/plans/M1/files/crates/proto/testsprints nothing.
Stop and report if
- A fixture line cannot be matched byte for byte with the field order given above.