Skip to content

Budget-funds-≥1-FLW guard (#722) is SKILL.md prose — didn't fire live; enforce in code at the MCP boundary #729

@jjackson

Description

@jjackson

Finding

Validation run bednet-spot-check/20260606-2013 (seeded 3,4,6) ran on confirmed v0.13.557 (opp name carried #721's 20260606-2013 · Bednet Spot-Check front-prefix — proof the new connect-opp-setup code was active). Despite that, Phase 4 shipped an unclaimable-by-design opp and the #722 budget guard did not halt:

  • connect-opp-setup.md / run_state.yaml: payment unit Household Visit, amount_cents: 50, org_amount 50 cents, max_total: 10; opp total_budget: $50.
  • Real capacity: number_of_users = total_budget / Σ(max_total × (amount + org_amount)) = 50 / (10 × (50 + 50)) = 0.05 — well under 1.
  • phases.connect-setup.status: done (NOT error). No [BLOCKER]. Guard never tripped.
  • Deliver smoke "passed" only because total_budget(50) == minimum_budget_per_visit(50), so commcare-connect's per-visit claim guard passes at equality — a coincidence, not a healthy config.

Why #722 failed

#722 added the guard as SKILL.md prose ("compute min_budget_for_one_user from the created PUs, assert total_budget ≥ it, else HALT"). Two prose-level failure modes both fired:

  1. Cents still used. The new whole-units-only amount guidance did NOT stop the agent passing 50 (cents) for a $0.50 PDD rate. A sub-$1 rate is unrepresentable in whole USD, so the agent fell back to cents — exactly what the guidance forbids, but prose doesn't enforce it.
  2. Guard computed in the wrong unit. Most likely the agent evaluated the guard in dollars (min = 10 × (0.5 + 0.5) = $10 ≤ $50 → "passes") while storing cents (50). The cents/dollars ambiguity the guard was meant to eliminate re-entered inside the guard. A prose guard cannot guarantee unit-consistency.

(Confirmed unit: whole USD — the earlier stale run's mobile Download gate showed "Earn up to 100 USD for visit" for amount=100.)

Fix — enforce at the MCP boundary (code, not prose)

Per CLAUDE.md "class-level preventers > instance fixes" and "MCP capabilities are atomic / enforce at the boundary":

  1. Code-enforced capacity guard. In the connect MCP, compute number_of_users from the integer request values actually sent (reliable — unlike the HTML-scraped connect_list_payment_units read-back where amount is undefined) and hard-reject when < 1. Best enforcement point: connect_create_payment_unit (knows the PU's amount/org_amount/max_total from the request) cross-checked against a fetched opportunity.total_budget; for multi-PU opps accumulate across PUs (or enforce at connect_activate_opportunity, the transition that invites/claims depend on). Reject with a typed error naming $X total_budget vs $Y = Σ(max_total×(amount+org_amount)).
  2. Kill the cents path at the boundary too. Amounts are whole USD integers; a sub-$1 PDD rate that rounds to 0 should be a typed rejection, not a silent cents fallback.
  3. Fix the bednet smoke PDD (ACE/bednet-spot-check/inputs/) to a whole-dollar rate (e.g. $1/visit) so the smoke is representable — removes the sub-$1 trigger (instance fix, complements the class fix).

Note: MCP code changes need a full Claude Code restart to take effect (CLAUDE.md § MCP changes need a full Claude restart), and re-validation needs another seeded 3,4,6 run.

Evidence

Fix lands here: mcp/connect/ (capability map + backend for connect_create_payment_unit / connect_activate_opportunity), plus the bednet smoke PDD input.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions