Task files, the files they copy in (byte-identical to the reference on m3a-ref), each area's check record, and a README with the per-task table of what each check exposed. The handoff note is done with. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
11 KiB
M3a checks, part B: the policy side (tasks 02, 04 to 08)
What each task's given tests were checked against before hand-over, and every defect that the checking exposed in the spec, the task or the tests. Written for the record that decides how much checking the next milestone gets (decision of 2026-09-18, tip T17).
What happened to the plan. The decision gave policy the oracle and config, args,
grants, state a compiling skeleton. All five got a minimal reference implementation instead
(about 900 lines in all), because the reference for the broker and the approval handoff (tasks 10
to 15) cannot run without working policy, grants, arguments and state underneath it. So the
saving the decision expected from these five did not happen in M3a. The record below says what
the references caught that a skeleton or the oracle would not have, so the next milestone can
judge: it is very little.
| Task | Check it had | Tests | First run against the check |
|---|---|---|---|
| 02 proto admin wire | the contract types, run | 30 in four files | 2 failures, both real (below) |
| 04 config | reference | 7 | all passed |
| 05 args | reference | 13 | all passed |
| 06 grants | reference | 17 | all passed |
| 07 policy | oracle, and a reference | 28 in four files, 7 doctests | all passed |
| 08 state | reference | 9 | all passed |
All of brokerd's suites pass three times in a row and once under four busy loops; clippy with
-D warnings is clean on brokerd, bxctl, loopd and on proto's library and the four test
files of task 02. proto/tests/records.rs does not compile in this worktree: it is task 01's.
Task 02: the contract types, run against the tests
- Contract defect, real.
DecisionRecordaccepted unknown fields:{"outcome":"allowed","zz":1}decoded. serde does not applydeny_unknown_fieldsto the unit variants of an internally tagged enum. Found bystrict.rs, which walks every object of every fixture (tip T2), on the firstapprove_resultfixture. It affects the audit records of task 01 as much as the wire. Fixed in the worktree'sproto/src/audit.rsby decoding through a privateRawDecision { outcome, reason: Option<DenyReason> }with#[serde(try_from = …)]; the public type, its variants and its JSON are unchanged. A hand-picked test now pins it (an_outcome_rejects_unknown_and_misplaced_fields). Left over:{"outcome":"allowed", "reason":null}still decodes, because an absent and a nullOptionlook alike to serde. Task 01 must carry this fix; task 02 checks for it and stops if it is missing. - Observation, not fixed. serde's derived
Deserializefor a struct also accepts a JSON array of the fields in order, so"body":[]decodes asEmptyand a four-element array decodes as aToolRequest. This has been true of everyprotostruct since M1. It is not an authority problem (the values are validated the same way), and closing it means a hand-writtenDeserializefor every struct. The test that tried to forbid"body":[]was removed. Worth a decision, not worth a task now. - Plan defect.
strict.rsis touched by task 01 (the audit fixture's object count changes with the nestedevent) and by task 02 (sixteen wire fixtures). Task 02's copy overwrites task 01's, so the file handed over with task 02 must contain both changes. The version here has only task 02's. - Task 02 adds a test the directive did not list: an approval id is a JSON number, and a string or a negative number is refused.
Neither defect would have been found by a compiling skeleton. Both were found by running the
tests against real types, which for proto costs almost nothing.
Task 04: config
- Spec gap. The spec shows the socket paths written out under the default home and does not
say what they are when only
homeis set. Settled as inloopd: an empty socket path means "underhome", read throughbroker_socket()andadmin_socket(). - Repo defect found on the way. The "Used by" column of
docs/dependencies.mdis stale: it names onlyprotoforserdeandserde_json, thoughloopdandbxctluse both.check-dep-docs.shchecks crate names only. The task has the implementer addbrokerd. - Test design: the default-home test does not set
BOXMAKER_HOME(in edition 2024set_varisunsafe, and it would race); it computes what the default must be in this process. - The reference caught nothing. A skeleton would have done.
Task 05: args
- Spec defect. Section 3 "Hosts" says a URL's host "is a host name as above (so no IP
literals…)". That does not follow:
127.0.0.1,127.1and10.0.0.0x1all fit the host-name grammar (labels of[a-z0-9-]), and curl normalises each to an IPv4 address. The runbook also tells the owner that an IP address in a grant is a problem, which the grammar did not make true. Rule added: the last label starts with a lettera-z. Proposed wording for the spec: "…each label 1 to 63 bytes of[a-z0-9-]not starting or ending with-, and the last label starts with a letter, which excludes every spelling of an IPv4 address." Found by writing the test table (listing the spellings), not by the reference. - Spec gaps settled in the task: an absent
cwdis left out ofcanonical_json, and"cwd": nullmeans absent;hostis never incanonical_json;https://example.com?xis invalid because what follows the host must be the end,:443or/; the root/is a valid argument path (it is only invalid in a grant). - Tooling. The editing tool decodes backslash-u escapes in text it writes (the same fault that hit the spec). The test that needs the escaped slash builds it from pieces at run time.
- The reference caught nothing the tests had wrong.
Task 06: grants
- Spec gaps settled in the task: a missing or unreadable grants directory is a problem, not
an empty set (I12); an empty directory is a valid empty set; problems come out sorted by file
name; when
toolis unknown the constraint table (rule 6) is skipped for that grant; a hidden file such as.tomlhas an invalid id;from_grantsrefuses two grants with one id, which files cannot produce but the property test's generator could. - Fixture defect, caught before hand-over. A fixture named
notes-read.toml~(to prove editor backups are ignored) is matched by the owner's global gitignore, so it would never have been committed and the implementer's copy would have differed from the plan's. The case now lives in a temporary directory the test makes. Rule for fixture authors: rungit check-ignoreon odd names. - The reference caught nothing: 17 of 17 on the first run, line numbers included. A skeleton would
have done, at the cost of not knowing that
toml's span for an unknown field points at the key.
Task 07: policy
- Spec ambiguity. "
write_file… is inside one of the grant'spathsand is not the grant path itself", together with "the longest one is the matched path", does not say what happens when a grant lists/sand/s/outand the call writes/s/out. Settled: a grant path equal to the argument does not count; the longest path that counts is the matched one, so the write is covered through/s. Both the reference and the oracle were written to this reading, so neither could have caught the other; the ambiguity was found while writing the table test. - Test defect inherited from M1. The M1 pattern of
compile_faildoctests loses its teeth under the new shape: a struct literal that leaves a field out fails to compile whether or not the fields are private, and a call toDecision::newfails because there is nonew. Replaced by a literal that names all three fields withmatched: todo!()(compiles if and only if the fields are public) and by trait-bound probes forCloneandDeserializeOwned. Teeth checked: with the fields madepub, the literal doctest fails. The task has the implementer repeat that. - Spec gap.
redecidereturningResult<Decision, DenyReason>loses thedenygrant's id and file hash, which theApprovalrecord needs.Denial { reason, grant, grant_sha256 }carries them, fordecidetoo. - Scope note for tasks 12 and 13. Section 12's properties over sequences of calls (taint never
goes down, every
Resultfollows aDecision, the runtime sees a call only afterallowedor an approval) cannot be tested in a purepolicytest. They belong with the ledger and broker. So does "one invalid file among valid ones denies a call a valid file would allow":grantsproves the set does not load; the denial withgrants_invalidis the broker's. - Oracle against reference. Zero disagreements in 15,000 cases on the first run. That is weak
evidence by itself, since one author wrote both (T5), so the oracle was tested: six mutants
of the reference (expiry
>for>=, highest id for lowest, prefix match by bytes, taint>=for>, write allowed at the grant path itself, theuntrustedlabel dropped) were each caught by the oracle alone, and each also by a table test. The reference caught nothing the oracle would not have. Its uses were to show that the generator reaches every kind of outcome (each test asserts more than 200 cases of each) and to stand under the broker's reference.
Task 08: state
- Spec gaps settled in the task: a state file that says
publicis treated as damaged (a session is never belowprivate, sobrokerddid not write it);raisealways writes, so the file exists from the first result;raisetrusts the state it is given and does not read again, because the caller holds the ledger lock; the file ends with a newline and reads with or without one; the directory is made 0700 with its parents, the file 0600. - Test design: the write-failure test makes the directory read-only, so it skips itself with a
message when the user can read a mode 000 file (root).
TempDir'sDropputs the permissions back before removing the directory. - The reference caught nothing. A skeleton would have done.
Summary for the process decision
| Found by | Defects |
|---|---|
| Running real types against the tests (task 02) | 2: DecisionRecord accepts unknown fields; the array form |
| Writing the test tables | 3: IPv4 spellings pass the host grammar; the write_file ambiguity; toothless doctests |
| Writing the task files | 5 spec gaps settled (config sockets, grants directory, Denial, state public, canonical cwd) and the stale dependency table |
| Preparing fixtures | 1: a fixture name swallowed by a global gitignore |
| The references for tasks 04 to 08 | 0 |
| The oracle, against six mutants of the reference | 6 of 6 |
For plumbing the skeleton level would have been enough. For policy the oracle is enough,
provided it is mutation-tested once: an oracle that has never failed has not been shown to work.
Resolved at the merge (2026-09-18). Area A fixed the same DecisionRecord hole with empty
struct variants, Allowed {} and Ask {}. That fix was kept: it is four lines of derive rather
than 35 of hand-written decoding, and it also refuses {"outcome":"allowed","reason":null}, which
admin_wire.rs now pins. Task 02's "Check first" looks for Allowed {},.