Skip to content

mcp: the harness field is a hard award filter, not a recorded preference - #206

Merged
orveth merged 3 commits into
devfrom
fix/mcp-harness-description
Jul 30, 2026
Merged

mcp: the harness field is a hard award filter, not a recorded preference#206
orveth merged 3 commits into
devfrom
fix/mcp-harness-description

Conversation

@orveth

@orveth orveth commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

One string, one file, no code paths touched — but it currently tells the reader who acts on it the opposite of the truth.

The defect

post_job's harness field still advertises itself as inert:

"Recorded as an auto-award preference; not yet a hard filter (no claim wire field carries it)."

That was accurate before #201. It isn't now: harness is posted on the offer as ["param","agent",<name>], a seller that cannot run the requested harness never claims, and the buyer's award filter refuses a claim that doesn't advertise it.

Why it isn't cosmetic

This is a tool schema description — the one string an LLM buyer reads to decide whether passing the field accomplishes anything. A buyer that reads "not yet a hard filter" and drops the field as decorative is behaving correctly on the documentation it was given, and silently loses harness selection. The wiring is live and toothed underneath; only the sign-post is wrong, which is the failure mode that doesn't show up in any test.

Mine, from #201: I updated the Rust doc comment on PostJobParams and missed the user-facing schema text.

Scope

  • crates/mobee/src/mcp.rs, the harness description only.
  • The sibling model field carries the identical "not yet a hard filter" caveat and is left alone — it is still true. No wire field carries model.
  • Chosen deliberately as a micro-PR: one file, zero overlap with the run.rs surface in flight, so it can gate in parallel.

Evidence

cargo test -p mobee --all-features   →  rc=0, 31 tests, 0 failed

No behaviour change; nothing to revert-test, because there is no assertion here to bite. The claim the text now makes is the one already toothed in #201 (a_job_requesting_a_harness_is_never_awarded_to_a_claim_without_it, and the claim-gate tooth).

The schema description still said harness was "not yet a hard filter (no
claim wire field carries it)" after the multi-harness slice made it exactly
that. This is the one string an LLM buyer reads to decide whether the field
does anything, so it was telling the reader who acts on it the opposite of
the truth — a buyer would reasonably drop the field as decorative.

The sibling model field's identical caveat is left alone: it is still true.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@orveth

orveth commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Hard pre-award filtering already exists in the daemon — this issue is "apply the existing pattern to a second field," not "add a mechanism"

Measured today (2026-07-29) on main, buying with a real daemon against wss://mobee-relay.orveth.dev.

The daemon already hard-refuses an award on a pre-award property. Not a preference, not a ranking hint — a refusal, before any money is committed:

award {"job_id":"be7ff9e8…","claim_id":"6f99aa1b…"}
-> error -32002: award refused: claim 6f99aa1b… is not live

Evidence that the refusal is total, taken from the ledger rather than the reply. After the refusal, the job is absent from both load-bearing tables, and the wallet is unchanged:

reservations : be7ff9e8…  ABSENT
awards       : be7ff9e8…  ABSENT
wallet       : 897 sats, unchanged

That matters because it rules out the failure mode this issue would otherwise have to design around: there is no reserve-then-discover-it's-unusable path, and no partial state to reconcile. The gate runs before the reservation.

Why this changes the shape of #206

The standing objection to making harness hard has been that the buyer surface does soft pre-award selection and hard filtering would be new machinery. It isn't new. live is a hard pre-award filter today. So the ask here reduces to: give harness the same treatment live already gets.

Relevant asymmetry that makes the current behaviour worse than it looks — claim_serves_requested_agent treats silence as absence: a claim that doesn't advertise harness is filtered out, while a claim that advertises a harness it cannot actually run is not. So the soft path is already strict against honest seats and permissive toward misdescribed ones.

The honest limit, so the two gates don't get conflated

live measures reachable-now — the seat answered a protocol-owed liveness probe. It is the trustworthy kind of liveness signal (a response, never a broadcast), but it says nothing about capability, and it makes no promise about the next five minutes.

Liveness and capability compose; neither substitutes for the other. Measured on the same job: of six distinct sellers who claimed within ~1 second, four advertised no agents field at all, and one of those four was the only seat reading live: true. A seat can be perfectly reachable and still have acp compiled out — which is exactly the failure this issue exists to prevent, wearing a heartbeat.

There is a free discriminator already on the wire for that second axis: an acp-capable seat's claim carries agents: [...]; a seat with acp compiled out has no agents field. Visible in the claim you are already reading, before a single sat is committed.

@orveth

orveth commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Correction to my comment above, before it misleads a reviewer. I framed this PR as "make harness hard, using live as the precedent." That framing is wrong: harness is already a hard filterclaim_serves_requested_agent enforces it — and this PR is fixing a description that claims otherwise. Nothing here needs to add filtering.

The measurement still stands and it now points the other way, which is more useful: it corroborates the new description rather than arguing for it. The daemon demonstrably performs hard pre-award refusal (award refused: claim … is not live, with the job absent from both reservations and awards and the wallet unchanged), so "enforced as a HARD award filter" is describing a mechanism this codebase actually has and exercises.

Two things in my comment that remain load-bearing for this diff:

  1. "a seller that cannot run it will not claim at all" is the weaker half of the sentence. The enforcement that actually protects the buyer is the award-side filter, because it does not depend on seller good behaviour. Worth keeping both clauses, but they are not equally strong guarantees and a reader will take the sentence at the strength of its strongest-sounding claim.

  2. Silence is treated as absence. A claim that does not advertise harness is filtered out, while a claim advertising a harness it cannot actually run is not. Measured on a single open-pool post today: six distinct sellers claimed within ~1 second, and four advertised no agents field at all. So the honest reading of the new description is "only a seller that says it has that harness can be awarded" — an advertisement filter, not a capability filter. If that distinction is worth a clause, this is the description that should carry it.

orveth and others added 2 commits July 30, 2026 11:41
A buyer model reads the post_job description before it reaches the per-property
schema, so that is where "harness is a hard award filter" has to appear. The
per-property text alone leaves the first thing read claiming harness is only a
recorded auto-award preference.

`model` stays a recorded preference because it is one: no buyer-side filter names
it, and the claim-time decision gates on exactly three things — offer freshness,
the rate gate, and agents.serves(requested_agent). The two property descriptions
are lexically identical twins, so only one of them is false, and the twin is
correctly left alone.

Nothing asserts these strings, which is how one drifts.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mobee Ready Ready Preview Jul 30, 2026 6:44pm

Request Review

@orveth

orveth commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Seller-lane hold LIFTED. Diff re-read against current dev (d49ff3d2). A description is a claim, so I checked each of the five this PR makes rather than the one line it changes.

All five hold in current dev:

  1. Wire spelling ["param","agent",<name>]seller_agents.rs:36-39 (AGENT_PARAM), asserted canonicalised on the way out at gateway.rs:831-833.
  2. Hard award filterbuyer/lifecycle.rs:65-67: no request ⇒ every claim passes; a request ⇒ the claim must advertise that harness.
  3. A seller that cannot run it does not claimrun.rs:900-901, ClaimDecision::Skip(SkipReason::AgentUnavailable), and run.rs:882 states the gate is claim-time, not delivery-time.
  4. None / "any" ⇒ no preferencegateway.rs:60, job_lifecycle.rs:214.
  5. Still recorded as a preferencebuyer/store.rs:483 selects harness from pending_awards.

Two things worth a reviewer's attention that the diff cannot show.

The enforcement is stronger than the description claims. buyer/lifecycle.rs:42 reads the requested harness back from the relay, never from award params — so the filter cannot be relaxed by the side doing the awarding. That is the property that makes clause 2 trustworthy, and it is the reason the award-side filter is the half that protects the buyer.

The same false claim had three lexical forms; this PR reached one of them. Pushed a second commit for the tool-level post_job description, which read harness/model are recorded auto-award preferences — false about harness, and the text a buyer model reads before it ever reaches the per-property schema.

The model property description is a lexically identical twin of the harness one and is deliberately untouched, because it is still true: no buyer-side filter names model, and the claim decision gates on exactly three things — offer freshness, the rate gate, and agents.serves(requested_agent). Twins that look the same did not both turn false.

Nothing asserts any of these strings — no test, no snapshot (checked both). That absence is the mechanism by which one of them drifted into being false, and this PR does not close it. Asserting a doc string against the enforcement it describes is a real thing to want; it is out of scope for a description fix and belongs on its own issue if wanted.

Branch mechanics: was 54 commits behind dev, now merged, 0 conflicts, cargo check -p mobee exit 0. Worth recording the trap — the tool description on the stale branch still said "a real mobee job offer … mobee relay" while dev has those flipped to maxplayer. The PR read CLEAN only because it did not touch that line; editing it pre-update would have created the conflict and risked reintroducing the pre-rename string. Update-branch first was load-bearing here, not hygiene.

@orveth
orveth merged commit 3dd3f2d into dev Jul 30, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant