Files
boxmaker/docs/plans/M1/06-proto-records.md
T
kyleandClaude Fable 5.1 3e26c2e3c0 Add M1 plan, given tests and fixtures, AGENTS.md and implementer log
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>
2026-09-17 01:22:43 -07:00

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.rs and log.rs, and add to lib.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 records reports 3 passed.
  • cargo test -p proto reports 40 passed in total across the five test files.
  • make gate prints gate: ok.
  • diff -r crates/proto/tests docs/plans/M1/files/crates/proto/tests prints nothing.

Stop and report if

  • A fixture line cannot be matched byte for byte with the field order given above.