Files
boxmaker/docs/plans/M1/06-proto-records.md
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

132 lines
4.6 KiB
Markdown

# 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:
```json
{"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.**
```rust
// 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.**
```sh
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):
```rust
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.**
```sh
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.