buyer: carry the capability request on the offer - #900
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Closes the gap between "the filter exists" and "a job is not awarded to a seat that cannot do it". #866 shipped the capability predicate and wired it into both selection paths, then built the request empty at both award sites, so every claim passed and the feature shipped inert. The offer now carries the request as three params, and both award sites read it off the SIGNED OFFER rather than from award params: ["param","harness_family", family] ["param","harness_model", model] ["param","capability", token, ...] An absent request passes every claim, and an offer that requests nothing is byte-identical to one posted before this existed, so filtering is opt-in on the wire rather than only inside the predicate. Model needs a family (#788). A bare model does not say which harness would run it, and the harness request is what binds dispatch, so the predicate refuses it rather than ignoring it. Two gates run before an offer is signed, because posting commits: it arms the auto-award and starts the deadline, so a request nothing can satisfy turns a caller's typo into a committed offer and a guaranteed park. - The vocabulary gate knows the closed lists: a family or token no seat can ever advertise. - The satisfiability gate owns no rules. It synthesizes the claim that would satisfy the request exactly and asks the real award predicate whether that claim passes. A gate restating "a model needs a family" would be a second copy of a rule owned elsewhere, and the day #788 makes a bare model valid it would have to be found and changed by hand. Deriving the verdict means it changes itself. Neither gate is the enforcement boundary. A foreign client can publish either shape straight to the relay, so the award-time refusal and its park row remain the wire-level truth, and both layers are tested. A request that matches no live claim now parks naming what to fix. The reason stays silent unless capability was genuinely the obstacle: if any live claim satisfied the request, or none was live, a capability-shaped reason would blame the request for a price or mint failure and hide the real one. The all-inert tripwire from #866 is replaced by one that pins the new state: every axis read off the offer at both sites, no axis inert, and exactly two award sites. It also pins the test mirror against the production wiring, which is not hypothetical - that mirror shipped with model unwired for one revision and two tests silently asserted that a model request awards the claim it should have refused. The post_job schema descriptions become hard-filter promises in the same commit, since they are caller-facing claims about a money path. Display-only harness_variant and hardware stay unrequestable, now asserted against the filter struct rather than a list of param names.
The display-only guard parsed the declaration as raw text, so a doc comment merely mentioning hardware would have failed it - and that is the comment a careful author would add. It now reads field names, and carries a positive control against a synthetic declaration that IS bad, so a parser returning nothing cannot pass by inspecting nothing. The coupling test gains out-of-vocabulary token rows. The token rule is the one rule both post-time gates can see, and a row is worth more than an argument in a comment that they agree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The red-prove found the mirror check worthless: its needle was a string literal in the same file it searched, so it matched itself rather than the helper, and it passed with the mirror drifted. A guard is not a guard until it has gone red once, and this one could not. The fix removes the thing being guarded. Both award sites now call award_filters_for_offer, so 'the two paths filter identically' is structural rather than detected, and the test helper calls that same constructor instead of restating it. There is nothing left to drift. The tripwire drops the per-axis string scraping for two properties that are stronger and shorter: both sites go through the constructor, and no site hand-rolls a literal. A new request axis now reaches both paths with no edit to the test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two PostJobRequest initializers gated on `live-mints` lacked the three fields #897 adds, so CI's money-path job could not compile. That feature is on in that job and nowhere else in CI, and no local row had built it, so every row that did run was green on a set that excluded these sites. Every PostJobRequest, OfferView and ParsedOffer initializer in the crate is now accounted for across all cfg branches, rather than only the two the compiler happened to name under one feature set. Also drops the AwardFilters import that no longer has a use in buyer/mod.rs. Verified by compiling every feature row in ci.yml with --no-run: 7 of 7, zero errors. The live-mints row is compile-only by design — its tests reach a live third-party mint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A model or family request named a harness nothing selects. Only `requested_agent` reaches execution — `offer_row` persists it alone, `classify_offer` gates on it, `execute_job` hands it to `dispatch`, and `dispatch` runs the seat's FIRST preset when it is absent. So a multi-harness seat could satisfy a codex request and run Claude. A model now requires a preset, and a stated family must agree with the preset's own family or be derived from it. The rule lives in `claim_meets_capability_request`, the one predicate both award paths already consult, so the post-time gate inherits it through the synthesized claim rather than restating it. The capability park reason also could not reach production. Past the deadline `derive_claim_liveness` has demoted every processing claim to expired, so a diagnosis reading `live` found nothing and stayed silent in exactly the case it exists to explain. It now reverses that demotion — `expired` is written only by that function — and re-derives with the same function at the deadline instant, so it owns no liveness rule of its own. Three comments claimed a family request binds dispatch. It does not, and the claim would have ended the search for any reader. Two were introduced with the request params; the third predates them. Feature-union row at this tree: 1229 passed, 0 failed, 2 ignored, rc=0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The post_job schema told callers a model requires harness_family. It requires `harness` — only the preset reaches execution, so a model hung off a family names a harness the job would not actually run on. The family is derived from the preset when unstated, so harness+model is a complete request. The harness_family description claimed a family request binds dispatch. It selects which seats may CLAIM; a multi-harness seat can satisfy it and dispatch something else. Drops the claim that Claude seats advertise no model. That was true when written and is not a fact this schema should assert — replaced with the rule it was an instance of: a seat advertises a model only for harnesses whose ACP session reports one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous commit corrected "three comments" that claimed a family or model request binds dispatch. Three was the count of sites I remembered writing. The real count is 19 hunks across 6 files, and the ones I missed were the normative ones. The claim appears in at least four lexical forms — "binds dispatch", "enforced by dispatch", "ENFORCED, at the seat, when the job is dispatched", and "binds delivery because the seller enforces it exact-or-nothing at dispatch". A sweep built from the wording I had used myself could not match the others, and it indexed only Rust doc comments, so it could not see the spec at all. docs/protocol-v1.md carried it twice and is the higher authority: 4.5.3 called the family "the only one of the three backed by a mechanism", and 6.1.1 justified the model rule with the same inheritance argument. That 4.5.3 sentence predates #897 (7aa40e4); the rest are mine from 93a304b. What is true, re-read rather than recalled: AgentRegistry::dispatch matches on the preset NAME and returns the seat's first preset when none is named. In the whole seller run path requested_harness_family appears only as None in three initializers — nothing reads it. So no filterable field is enforced at dispatch, and 4.5.3's trichotomy is now one echo and two silences. 6.1.1 states the rule the predicate implements: a model needs the agent preset, a stated family must agree with the preset's own family, an unstated one is derived from it, and a family named alone stays valid as a seat filter that binds who may CLAIM and not what dispatches. gateway.rs held four stale comments but enforces no pairing in code — checked, not assumed. It owns no copy of the rule, so it needed no change when the model's anchor moved from the family to the preset. That is the same property the satisfiability gate has, and it is why both were free. Feature-union row at this tree: 1229 passed, 0 failed, 2 ignored, rc=0. Five warnings, all pre-existing, unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comparison loop in `the_post_time_gate_refuses_exactly_what_the_predicate_can_never_pass` reads like the assertion carrying that test. It is not, and a reviewer crediting it with coverage it lacks is the likely next mistake. Both sides of the comparison end in `claim_meets_capability_request`, so the oracle is computed from the subject. When the predicate breaks, both sides move together, the comparison still holds, and the loop reports a pass through the exact fault it appears to watch for. Measured, not feared: deleting the family-contradicts-preset check, and separately the model-requires-preset check, each left the loop green and reddened only the controls. So the controls are the whole of this test's coverage against a predicate fault, and a new rule in the predicate needs a new control here. The comment says both, next to the controls, where someone editing them will read it. The general form is worth more than the instance: whenever an oracle is derived from the thing under test, agreement between two values computed from one broken source is not evidence about that source. Same shape as the self-matching needle deleted earlier in this branch, one layer out. Comment-only: 0 non-comment lines added. Five guard tests re-run at this tree, 5 passed, 0 failed, rc=0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review found the award predicate validated an unknown capability TOKEN and not an unknown requested harness FAMILY. Verified against the tree rather than taken on description: HARNESS_FAMILIES appears in buyer/lifecycle.rs exactly once, in a doc comment, and never in a check. Neither reader filters vocabulary either — harness_families_from_tags and the offer's param reader both only trim and drop blanks. So a foreign offer requesting `harness_family="not-a-family"`, matched by a claim advertising that same string, satisfied the predicate. With payable terms and no requested preset, both award paths selected it. The red-prove shows exactly that: with the check removed, the predicate returns Ok(()) and the auto path returns the claim id. The post-time vocabulary gate already refused this, but it only sees offers built by our own client — and the comment on `unsatisfiable_capability_request` tells the reader that the award-time refusal is the wire-level backstop for foreign offers. That was a comment documenting a safety property the code did not have, on the money path, which is the kind that ends the search for whoever reads it next. The check goes where the token check already lives: the request is judged before the claim, so a nonsense request cannot be laundered into a match by a claim that agrees with it. Only the STATED family needs it; a derived one comes from `harness_family_for_preset`, whose range is HARNESS_FAMILIES by construction. The family axis now matches the token axis at both layers, and both read the same HARNESS_FAMILIES constant, so they cannot drift apart. `UnknownHarnessFamily` is kept distinct from `HarnessFamily` for the reason `UnknownCapabilityToken` is distinct from `Capabilities`: "no seat advertises this" means wait or add a seat, "that is not a real family" means no seat can ever satisfy it. The adversarial exact-match case is the load-bearing one. A test pairing an unknown request with an ORDINARY claim goes red on a plain mismatch even with the vocabulary check deleted, so it would pass without the guard and prove nothing. Request and claim carry the same unknown value. Four rows and one control added to the coupling test, per the note beside those controls: a new rule in the predicate needs a new control, because the comparison loop cannot see a predicate fault. Also corrects a review-flagged comment: the model-only park test said the operator must "add a family"; the implemented rule requires the `agent` preset. Feature-union row at this tree: 1260 passed, 0 failed, 2 ignored, rc=0. Red-prove: check removed ⇒ 3 tests red including the award path; restored ⇒ green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
orveth
force-pushed
the
buyer-897-capability-request
branch
from
August 25, 2026 20:34
0e0b3f4 to
610ef42
Compare
RELEASE_NOTES.md carried the exact premise this PR removed from the code. Two claims, both already false for the RC: `harness_family` is "exact-or-nothing at dispatch", and the runner sheet's family row is worth "enforced at dispatch". Dispatch reads the offer's `agent` preset and never reads `harness_family`, so neither was ever true of the shipped binary. Both lines also cited `docs/protocol-v1.md` §4.5.3/§4.5.4 as their source, and this PR corrects §4.5.3 to read that `harness_family` is NEITHER ENFORCED NOR ECHOED. They cited a spec asserting the opposite of what they claimed, which settles which artifact is wrong without arbitration. Found by enumerating the property (`enforc|exact-or-nothing|at dispatch`) rather than checking the two line numbers under review, with controls: 4 `harness_family` hits in the file and 0 on a negative needle. `heartbeat.rs:139` also says "exact-or-nothing" and is deliberately left alone. It says `AgentRegistry::dispatch` is exact-or-nothing on the preset NAME, which is true. The notes' error was applying a true property to the wrong field. The sheet's own mark is a separate defect on a separate surface. A tree-wide search found the same false claim in `web/app/src/ui/docks.ts` — the live mark, its doc comment and its hover tooltip — and in `web/app/test/capability.test.ts`, where a passing assertion pins the wrong mark green. That is a TypeScript change with its own test and is filed as its own head rather than folded into this money-path PR. Until it lands the sheet still shows the old mark, so these notes state the field's property and stop quoting a mark that is in flux. Doc-only. No code, no test, no behaviour change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #897.
Capability filtering could not refuse anything, because no offer ever carried a capability request. #866 shipped the predicate and wired it into both selection paths; both award sites then built the request empty, so every claim passed and the feature shipped inert. This carries the request on the offer and populates
AwardFiltersfrom it at both sites.Revision 2 — this PR was reviewed and did not survive it. Four separate things were wrong: it did not compile in CI's money-path job, the model/family request did not bind the harness that actually runs, the park reason it added could not be reached in production, and the award predicate never checked the requested family against the known vocabulary at all. All four are fixed here, and each is written up below rather than quietly amended, because in every case the miss is more useful than the fix.
What lands
The offer carries three params, and both award sites read them off the SIGNED OFFER — never from award params, so the request a buyer is held to is the one it published:
Wired end to end:
OfferDraft→ tags →parse_offer→ParsedOffer→OfferView→AwardFiltersat the manualaward_claimRPC and atdrive_auto_award.An absent request passes every claim, and an offer that requests nothing is byte-identical to one posted before this existed. Filtering is opt-in on the wire, not just inside the predicate. Asserted, including the whitespace forms.
Blocker 1 — the request now binds the harness that actually runs
The review found that a model or family request named a harness nothing selects. That was correct, and it was the substantive defect in revision 1.
Verified against the tree rather than taken on description:
offer_rowpersists onlyrequested_agent— not the family, not the model, not the capabilities. They cannot reach execution, which is a restart away from the claim.classify_offergates only onagents.serves(offer.requested_agent).execute_jobcallsdispatch(requested_agent).AgentRegistry::dispatchmatches on the preset name and returns the seat's first configured preset when none is named — its own doc says so.So a multi-harness seat could accept an offer requesting
family=codex, model=…and deterministically run Claude. Every other filter passed, because the seat genuinely serves both.The rule now enforced: a model requires the
agentpreset. When a family is also stated it must equal the preset's own family; when it is not stated it is derived from the preset. A family named alone stays valid and is unchanged — it binds which seats may claim, and it does not bind which harness a multi-harness seat dispatches. A buyer that needs the second guarantee must name the preset.This moves the anchor from the family to the preset and stays fail-closed at every step. Deriving the family rather than demanding it keeps
agent+model— the shape a buyer reaches for first — a valid request instead of a refusal.Where it lives matters. The validation and the derivation are in
claim_meets_capability_request, the one predicate both award paths already consult. The constructor stays a pure copy of the signed offer, and the post-time gate inherits the new rule for free, because it synthesizes a claim and asks the real predicate. Nothing restates the rule anywhere.The synthesized claim advertises both the stated family and the preset's own family, and pairs the model to both. That is deliberate: computing which family is effective would put a second copy of the derive rule in the gate, and the two would rot apart. A maximally-capable seat only has to know which axes were requested, never how they resolve.
This is a behaviour change for existing callers
modelwas previously accepted with a family and refused without one. It now requires the preset. A caller passingfamily+modeland noagentgets a post-time error where revision 1 accepted it.A matched model remains a last-observed self-report, never a promise about the next job (§4.5.4). It narrows who is considered; it does not pin what executes. #785 carries the selection work that would make it a commitment. And a seat advertises a model only for harnesses whose ACP session reports one, so before requesting a model, check that seats in the target family actually advertise one — a model request matches nothing on a family that advertises none.
Blocker 2 — the park reason could not be reached in production
Also correct, and the design that fixes it took two attempts.
derive_claim_livenessflips everyprocessingclaim to expired oncenow > deadline, then marks live only the first processing-or-delivered claim. Past the deadline that set is empty, socapability_park_reason's.filter(|c| c.live)loop never ran,refusalsstayed empty, and it returnedNone— in exactly the case it exists to explain. The revision-1 test hand-builtlive: true, a state the real branch cannot receive.The first fix did not work, and the suite caught it. Re-deriving liveness at the deadline instant looks sufficient and is not: the demotion is destructive. It overwrites
status, so by the time the park runs the information is already spent, and re-deriving sees the same expired claims. Re-running a derivation cannot recover what the first run destroyed.So the fix reverses the marker first —
expired→processing— and then re-derives with production's own function at the deadline instant. The justification is load-bearing and is why this is not a hack:CLAIM_STATUS_EXPIREDis never a relay value.derive_claim_livenessis its only writer, and it writes it only for a claim that wasprocessingwhen the deadline passed. Reversing it reads that function's own marker rather than guessing at a status. The round trip is asserted by the regression, which demotes atdeadline + 1and requires the claim to be a candidate again — so if the demotion ever writes a different status, that test fails rather than the diagnosis quietly seeing nothing.The review's criticism was narrower than the defect, and that is worth recording. It named the hand-built
live: true. The deeper problem was that the helper builds every claimprocessingand varies onlylive, so a "no live claims" case was a processing claim withlive: false— a state production cannot hold at all. A test asserting an unreachable state passes for a reason unrelated to the property it names.Blocker 3 — an unknown harness family was never refused at award time
Also correct, and it is the one with money directly behind it.
Verified in the source before anything was built, rather than taken on the description:
HARNESS_FAMILIESappeared inbuyer/lifecycle.rsexactly once — in a doc comment — and in no executable check.CAPABILITIES, but judged the requested family only against what the claim advertises.harness_families_from_tagsand the offer's param reader both only trim and drop blanks.So a foreign offer requesting
harness_family="not-a-family", matched by a claim advertising that same string, satisfied the predicate. With payable terms and no requested preset, both award paths selected it. The award is the payment decision, so this sat on the money path.The sharp part is why nobody found it by reading.
unsatisfiable_capability_request's own doc tells the reader that the award-time refusal is the wire-level backstop for foreign offers, while the post-time vocabulary gate only ever sees offers our own client built. That was a comment documenting a safety property the code did not have — on the money path, which is the kind that ends the search for whoever reads it next.The check goes where the token check already lives, in the request-validation block: the request is judged before the claim, so a nonsense request cannot be laundered into a match by a claim that happens to agree with it. Only the stated family needs this. A derived one comes from
harness_family_for_preset, whose range isHARNESS_FAMILIESby construction. The family axis now matches the token axis at both layers, and both read the same constant, so they cannot drift apart.UnknownHarnessFamilyis kept distinct fromHarnessFamilyfor the reasonUnknownCapabilityTokenis distinct fromCapabilities: "no seat advertises this" means wait or add a seat, while "that is not a real family" means no seat can ever satisfy it.The adversarial exact-match test is the load-bearing one, and an obvious test here proves nothing. A test pairing an unknown request with an ordinary claim goes red on a plain family mismatch even with the vocabulary check deleted — so it would pass without the guard and certify nothing. The test therefore has the request and the claim carry the same unknown value, which is the only shape the guard alone can refuse. A second case walks all of
HARNESS_FAMILIESand requires each to still be accepted, so the guard is proven not to be a blanket refusal.Two gates before an offer is signed, and why
Posting commits:
post_jobarms the auto-award and puts a signed offer on the relay with its deadline running. A request nothing can satisfy therefore converts a caller's typo into a committed offer plus a guaranteed park. Post time is the cheapest moment that mistake can surface.claim_meets_capability_requestwhether that claim passes. A request the perfect claim cannot pass is one no claim can pass.The second shape is deliberate. A gate restating "a model needs a preset" in its own words would be a second copy of a rule owned elsewhere, and on the day #788 makes a bare model valid someone would have to remember this gate exists and go change it. Because the verdict comes from the predicate, it changes itself.
the_post_time_gate_refuses_exactly_what_the_predicate_can_never_passasserts the two agree across seventeen request shapes, with six positive controls so a pair of stuckNones cannot agree its way to green.Neither gate is the enforcement boundary. A foreign client can publish either shape straight to the relay, and for that offer the award-time refusal and its park row are the wire-level truth. Both layers are tested, and
a_foreign_model_only_offer_is_refused_at_award_and_parks_saying_whydeliberately does not go throughpost_job— the gate would refuse the input first, and the layer would look tested when the only tested thing was the gate.The parked outcome
A request that matches no live claim parks naming what to fix, rather than awarding anyway or failing silently.
The reason stays silent unless capability was genuinely the obstacle:
Nonewhen there is no request, when no claim was live, and — the one worth being strict about — when some live claim satisfied the request. In that last case the award was stopped by price, mint or budget, and a capability-shaped reason would blame the request for it. That is the case where a capability explanation is most plausible and most wrong.With no capability obstacle the park row is byte-identical to what it was before this change.
This is a local operator string, deliberately not a wire reason code. #821 adds
capability_missingto the protocol and #859 makes an undispatchable job carry that label; this is what those two replace on the wire, and it lands first so the diagnosis exists before the code for it does. Stated order, per the issue's item 4.One constructor, so the two paths cannot diverge
award_filters_for_offeris the single place award filters are built, called by the manual RPC, bydrive_auto_award, and by the tests. "Both paths filter identically" is now structural rather than something a test detects after the fact, and a new request axis added to the constructor reaches both paths with no other edit.That shape was arrived at the hard way, and the route is worth recording.
The two award sites originally each built their own
AwardFiltersliteral, which meant the tests needed a third copy to build filters the way production does. That copy drifted: it carriedrequested_model: Nonefor a revision while production read the offer, so two model tests passed while asserting the opposite of the intended behaviour. They went red only because the model cases happened to get written.The first fix was a test asserting the copy matched production. The red-prove showed that check could never fail: its needle was a string literal sitting in the same file it searched, so it matched itself rather than the helper, and it passed with the mirror deliberately drifted. It read as the strongest assertion in the test and was worth nothing.
A hand-written copy of production wiring and a needle published into its own corpus are the same defect — an artifact that agrees with itself instead of with the thing it describes — and in both cases the remedy is to delete the copy, not to test it harder. Hence a constructor rather than a stronger test.
The tripwire that remains pins two properties instead of scraping field spellings: both sites go through the constructor, and no site hand-rolls a literal. The second matters on its own — without it a third site could quietly build its own filters beside two that behave, and the first assertion would confirm the good paths while saying nothing about the bad one.
A row's identity is its command, not its nickname
Revision 1 did not compile in CI's money-path job. Two
PostJobRequestinitializer sites behind#[cfg(feature = "live-mints")]were never given the three new fields, anderror[E0063]killed that job seven seconds in, before any test ran. Every other CI job passed.The fix is two lines; the cause is a habit:
cargo test -p maxplayer-core --release --no-default-features --features gateway,git-delivery,wallet,live-mints --locked.cargo test -p maxplayer-core --lib --features wallet.Different feature set, different profile, different target selection.
live-mintsis on in that job and nowhere else in CI (#720, stated in the workflow's own comment), so it is the single feature no other row covers — and it is exactly the one that carried the broken sites. The local row was named after the CI job, and the name asserted an equivalence the command never had.A row's identity is its command plus its feature set plus its tree. Every row below is stamped with all three, and the money-path row is CI's command copied verbatim rather than approximated.
There is a second, sharper half. Revision 1 certified its rows by name rather than by total, precisely because a feature set that skips a module still prints a healthy green. That instrument is sound, and it could not have caught this: name-certification proves a test ran, and a test in a module that never compiled emits no name at all. Absent and not-in-this-row are the same output. The instrument was fine; the row set was not.
Every
PostJobRequest,OfferViewandParsedOfferinitializer in the crate was then enumerated across allcfgbranches rather than fixing only the two sites the compiler named — a compiler reports what the current feature set compiles, which is the same blind spot one layer down. The enumeration was validated against the previous head, where it correctly reported exactly the two known-missing sites before being trusted on the new one.Docs, and how many places the wrong claim had reached
docs/protocol-v1.md§6.1 gains the three params and a new §6.1.1 defining the request: absent passes everything, both award paths apply it identically, the offer is the authority, a model needs theagentpreset, a stated family must agree with the preset's family and an unstated one is derived from it, a family alone stays valid as a seat filter,capabilityis one multi-value tag, and the display-only fields are not requestable.Revision 1 also asserted, in a comment, the exact property the review proved absent: that a family request binds dispatch and that a family and a preset are both enforced. That sentence would have ended the search for anyone who read it. The review found it from the code instead.
Correcting it turned out to be much larger than the three comments first identified — 19 hunks across 6 files, and the ones initially missed were the normative ones. Two reasons, both worth stating:
docs/protocol-v1.mdoutranks any comment. §4.5.3 called the family "the only one of the three backed by a mechanism"; that sentence predates this work (7aa40e4). §6.1.1 used the same false inheritance argument as the justification for the model rule; that one was mine.§4.5.3 now says what is true: no filterable field is enforced at dispatch. The three are one echo and two silences.
gateway.rsheld four stale comments but enforces no pairing in code — checked, not assumed. It owns no copy of the rule, so it needed no change when the model's anchor moved. That is the same property the satisfiability gate has, and it is why both were free.Caller-facing descriptions
harness_family,modelandcapabilitiesare hard filters, so thepost_jobschema says so.post_job_award_filter_descriptions_match_enforcementasserts per axis rather than in aggregate, and pins the two model facts that are load-bearing and easy to drop: that it requires the harness preset, and that it does not pin what executes.Display-only fields stay unrequestable
harness_variantandhardwaremust never become filterable — they are operator-declared free text nothing can contradict, so filtering on them would decide money on an unfalsifiable claim. This change opens a new filter surface (the offer's request), and the existing guard only covered the seat's advertisement. The new assertion is made against theAwardFiltersdeclaration itself rather than a list of param names, because a test naming params would keep passing if a display-only axis were added straight to the filter struct — the one place a filter can actually read. It carries a positive control.Compatibility, measured
Stated from checks run against the diff, not from intent.
an_absent_capability_request_posts_a_byte_identical_offerandpost_job_emits_the_capability_request_it_was_given, including the blank and all-whitespace forms. An older reader sees an offer it already understands; a param it does not know is one it already ignores.allow_real_mints: boolon the new constructor) and a test literal. Neither is a config key. Read individually rather than counted, because a count would have reported "2 new config keys".crates/maxplayer-core/src/buyer/store.rsis untouched by this diff (emptygit diff --stat), andOfferViewdoes not appear in it. The store's schema and itsput_pending_awardrow are unchanged.OfferViewis serialize-only. It derivesSerializeand notDeserialize, so it is an RPC output view rather than a round-tripped record — no older binary deserializes it, and the new fields additionally carryskip_serializing_if, so an absent request emits no key at all.mainatd022bf54, not onto the base this branch was cut from. Every commit replayed clean, rc=0; the branch is nine commits on that base. The rebase was not cosmetic:mainmoved repeatedly today, and one of those commits (a2b9403f) added a field tohome::SandboxConfig, which turns any exhaustive struct literal of that type intoerror[E0063]. That brokemainitself for about eight hours and stalled several PRs; it was fixed in fix: decide the Codex session value for the doctor engine-floor test #909, which is why this branch sits ond022bf54rather than on the base it was cut from. This branch was never exposed, and that was verified rather than assumed: the twoSandboxConfigliterals in a file this PR touches (capability.rs:386and:501) both use..Default::default(), and this PR's only change to that file is an insertion far from either. A file-list check would have cleared this branch for the wrong reason — a list of files is not a list of literals — so the compile matrix below is what actually settles it.Verification
Every row states its tree and its exact command.
maxplayer-coreisdefault = [], so a barecargo testcompiles none of the gated modules and still prints a healthy green.main, union features: 1244 passed / 0 failed / 2 ignored / rc=0. Measured on a detached checkout atc2bb4cae, and it carries tod022bf54by byte-identity rather than by inference:git diff c2bb4cae d022bf54 -- crates/maxplayer-core/is 0 bytes, and the whole merge in between touches exactly one file,crates/maxplayer/src/doctor.rs. The crate under test is unchanged, so the baseline is not stale.cargo fmtdeliberately not run: there is no fmt gate in CI here and it reformats pre-existing code.Rows at head
610ef42, each log opening with its ownTREE=andPORCELAIN=:cargo test -p maxplayer-core --lib --features acp,gateway,git-delivery,wallet: 1260 passed / 0 failed / 2 ignored / rc=0. Against the 1244 baseline that is +16, matching the measured set-difference exactly.maxplayercrate, both feature rows —cargo test -p maxplayer --locked --no-runand--features acp,wallet: rc=0 each, 0 errors, 0 occurrences ofE0063.ci.yml, each built with--no-run: 8 rows, all rc=0, 0 errors. In full —-p maxplayer-core --release --no-default-features --features gateway,git-delivery,wallet,live-mints --locked·-p maxplayer-core --release --features acp,gateway,git-delivery,wallet --locked·-p maxplayer-core --locked·-p maxplayer-core --features acp --locked·-p maxplayer --locked·-p maxplayer --features acp,wallet --locked· pluscargo build --workspace --lockedand the executing union row above. The driver refuses to start a row when free disk is under its floor, so a truncated matrix cannot be mistaken for a clean one.Two of those rows were red earlier today, and it is worth recording why they were not this PR's.
cargo test -p maxplayerdid not compile onmainfor roughly eight hours:a2b9403fadded a field tohome::SandboxConfigand updated one of the workspace's two exhaustive literals of that type, missing the other because it lives in a different crate. The literal it missed is exhaustive on purpose — its own comment says it is written longhand "so that adding another sandbox field breaks this test and makes someone decide what it should be here." The tripwire fired as designed and went unanswered.That was established here on a detached checkout of unmodified
main, with no PR involved: rc=101, same file, same line, same error, whilegit diff <main> HEAD -- crates/maxplayer/src/doctor.rswas empty at zero bytes. The fix landed in #909 and the rows above are green atd022bf54.Two things from that are worth more than the incident. A fired tripwire left unanswered inverts its own attribution — the longer it sits, the more it reads as "every PR is broken" rather than "main is broken." And this PR's stored merge base predated the breaking commit entirely, so it could never have shown that error; "main was broken" would have been the wrong explanation for any red seen here, and a reassuring explanation closes a question exactly as fast as a correct one.
The delta is +16: 18 tests added and 2 removed (one renamed when the rule moved from the family to the preset, one replaced because it asserted the pre-#897 inert behaviour). Measured as a set-difference over test names against the baseline tree, not counted by hand, and it cross-foots against the run counts independently — 1260 − 1244 is also +16, from an instrument that knows nothing about test names.
The predicted figure was +14 while this branch stood at eight commits; blocker 3 then added its two tests. That is stated rather than back-fitted, because a prediction quietly restated to match a later measurement is not a prediction.
⚠ The money-path row is COMPILE-ONLY here, deliberately. Its
live-mintstests reach a live third-party mint over the public internet and includepayment_walletworker sends, so this lane compiles that row with--no-runand does not execute it. CI is the execution evidence for it, and the compile check is what closes the defect that broke revision 1.cargo test -p maxplayer-core --release --no-default-features --features gateway,git-delivery,wallet,live-mints --locked --no-run— rc=0, 0 errors, CI's command copied verbatim with--no-runappended.Warnings: 5 at the baseline
c2bb4caeand 5 at head610ef42— measured at both trees under the same command, not carried forward from either. Zero new. Revision 1's sixth warning, anunused import: AwardFilters, is gone: 0 occurrences in the head log.A correction to revision 1's own numbers. Its body claimed "Warnings: 5 … Zero new warnings" and a money-path row of 1199 / 0 / rc=0. Both were wrong at the head it described. The filed head generated six warnings — the sixth was an
unused import: AwardFiltersleft behind when the last hand-rolled literal was removed — and that money-path row had been measured two commits earlier, so it certified a tree that was never filed. Neither error was visible without re-measuring, because CI denies no warnings here (no-D warnings, noRUSTFLAGS, no clippy step in any of the three workflow files; checked with a positive control). A remedial commit invalidates a verification row exactly like a feature commit does, and it does not feel like it, because remediation reads as consolidation. Every row log now opens with its own tree.The tests this change adds, certified by name in the rows that compile and run them:
a_family_that_contradicts_the_preset_is_refused_on_a_seat_that_advertises_both·a_foreign_model_only_offer_is_refused_at_award_and_parks_saying_why·a_model_request_without_a_harness_preset_is_refused_not_ignored·an_absent_capability_request_posts_a_byte_identical_offer·an_offer_sourced_request_refuses_a_non_matching_claim_on_both_paths·a_preset_and_a_model_derive_the_family_instead_of_refusing·a_request_matching_no_claim_parks_with_an_actionable_reason·both_award_paths_read_the_capability_request_off_the_offer·display_only_fields_never_reach_the_award_filter·offer_carries_the_capability_request_across_the_wire·post_job_emits_the_capability_request_it_was_given·post_job_refuses_a_request_no_seat_could_satisfy·the_capability_clause_survives_the_real_deadline_demotion·the_capability_request_reader_normalizes_a_hand_written_offer·the_park_reason_declines_to_blame_capability_when_it_was_not_the_obstacle·the_post_time_gate_refuses_exactly_what_the_predicate_can_never_passThree flaky observations in this suite, cited rather than hidden
Flaky: an_accept_naming_another_seats_claim_never_binds_the_loser fails intermittently in Money-path tests #894 —
an_accept_naming_another_seats_claim_never_binds_the_loser. Did not fire in any run here.flaky: a_genuine_wrong_p_restricted_stays_removed races the CLOSED under suite load #901 — a second, distinct intermittent:
seller_node::run::tests::a_genuine_wrong_p_restricted_stays_removedwent red in one full-suite run and green in a rerun on the same tree, having also passed on the unmodified baseline tree and 5/5 in isolation. Its failure is atimeoutexpiring, not a wrong value, and the test self-documents the race.A third single observation, now discriminated rather than left as a suspicion. Revision 1 saw
seller_node::tests::restart_then_award_rebinds_the_parked_claimpanic once withLock(Held), where the test drops aSellerNodeto fake a crash and the reopen races the lock release. One observation is not an intermittent, and diff-scoping could not settle it — adding tests changes parallel scheduling, so "the failing code is untouched" and "this change perturbed a latent race" can both be true. Only a baseline row in the same feature set separates them, and that row had never been run in this lane.It has now been run. Wallet-only features (no
acp), the same command at both trees, N=3 each:Lock(Held)appears 0 times in any of the six runs. It did not reproduce at either tree.c2bb4caewent red 1 of 3 — on unmodifiedmain, containing none of this diff.So the row is flaky at baseline, and this change is not implicated. ⚠ One honest limit: the baseline failure is unattributed — the harness recorded the summary and the suspect's status but deleted the per-run log, so the failing test's name went with it. The corrected harness keeps every log. The conclusion above rests on the suspect passing 6 of 6 and on the baseline reddening without this diff present, neither of which needs that name.
None of the three is being used to excuse this diff; all are named so a future red on any of them is recognised rather than re-investigated.
Red-prove
Every guard was made to fail on purpose, then reverted and re-verified green. Revision 1 ran seven cases and six bit; revision 2 adds five more for the new guards, and all five bit, plus a control on the selection itself.
Revision 1:
found 1against 2.AwardFiltersat one site — tripwire red,found 1against 2.named_claim_awardable— both acceptance tests red, and only the manual-path assertions, so they discriminate which path broke rather than merely noticing that something did.Found: [], which is a parser inspecting nothing while reporting clean.Revision 2:
agent=claudewithfamily=codex, on a seat advertising both) is proven refused at both layers.Some(HarnessFamily { requested: "codex" })againstNone, which is the gate refusing more than the predicate.offer deadline passed before an awardable claim appearedthat the review identified in production.an_unknown_harness_family_is_refused_even_when_the_claim_advertises_it, the post-time gate'sthe_post_time_gate_refuses_exactly_what_the_predicate_can_never_pass, anda_foreign_offer_naming_a_bogus_family_is_refused_on_both_paths_and_parks_blaming_the_request. With the check restored, the full union row at the filed head is 1260 / 0 / rc=0.The most useful result is from cases 8 and 9, and it changes how the coupling test should be read. In both, the coupling test's main comparison loop could not see the break. The gate and the predicate both end in
claim_meets_capability_request, so when the predicate is what broke, the two sides moved together, the comparison still held, and the loop reported a pass through the exact fault it appears to be watching for. Only the positive controls went red.⚠ So a reviewer should not credit that loop with protecting the predicate. The general form, because it is not specific to this test: whenever an oracle is computed from the subject, the controls carry the whole of the coverage. The loop checks that two things agree, and two things derived from the same broken source agree perfectly. That is the revision-1 self-matching needle one layer out — in both cases the test consulted the thing under test for its own answer, and in both cases agreement read as a pass.
The controls are therefore load-bearing rather than belt-and-braces, and adding a rule to the predicate means adding a control here — the loop will not notice. That warning is now written into the test itself, not just recorded here, because a body outlives no one and a comment sits where the next person edits.
Not merged, not tagged. No seat touched.