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>
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user