Skip to content

fix(phase4): code-enforce funds-≥1-FLW capacity guard at connect MCP boundary (#729)#731

Merged
jjackson merged 1 commit into
mainfrom
fix/connect-capacity-guard-code
Jun 6, 2026
Merged

fix(phase4): code-enforce funds-≥1-FLW capacity guard at connect MCP boundary (#729)#731
jjackson merged 1 commit into
mainfrom
fix/connect-capacity-guard-code

Conversation

@jjackson
Copy link
Copy Markdown
Owner

@jjackson jjackson commented Jun 6, 2026

Why

#722 added a "an opportunity must fund ≥1 FLW" guard as SKILL.md prose. It failed live on bednet-spot-check/20260606-2013 (running confirmed v0.13.557): the agent evaluated the guard in dollars ($0.50) while storing cents (50), so number_of_users read as 5 in-head but 0.05 in Connect. Phase 4 shipped an unclaimable opp (amount_cents: 50, total_budget: 50, connect-setup.status: done, no halt). The Deliver smoke "passed" only because total_budget(50) == per-visit-amount(50) — a knife's-edge coincidence in commcare-connect's claim guard, not a healthy config.

(The same validation run confirmed #721 — the opp name carried the 20260606-2013 · Bednet Spot-Check front-prefix — which is how we know the new code was active and the failure was real, not stale code.)

Per CLAUDE.md "class-level preventers > instance fixes" and "enforce at the MCP boundary": a prose guard can't guarantee unit-consistency. Code over the integers actually sent to Connect can.

What

  • mcp/connect/opportunity-capacity.ts (new, pure): minBudgetForOneUser, numberOfUsers, assertFundsAtLeastOneUser, and a typed OpportunityUnderfundedError (non-retryable; structured toJSON with total_budget, min_budget_for_one_user, number_of_users, per-PU breakdown).
  • connect_create_payment_unit(s) gain an optional total_budget arg. When supplied, the REST and Playwright backends assert total_budget ≥ Σ(max_total × (amount + org_amount)) (number_of_users ≥ 1) before POSTing (no orphan PU) and reject opportunity_underfunded otherwise. total_budget is guard-only — never forwarded in the request body (asserted in a test).
  • connect-opp-setup/SKILL.md: ALWAYS pass total_budget; the hand-computed prose guard is replaced by a pointer to the code guard; sub-$1 rates round UP to the $1 minimum, never cents (Connect reads 50 as $50/visit — the exact bednet mix).

Why this can't be fooled: the bednet misconfig (total_budget=50, amount=50, org=50, max_total=10) → code computes 50 / (10×100) = 0.05 < 1rejects. The agent can "think in dollars" all it wants; the code only sees the integers it's about to send.

Tests

  • 10 capacity-helper unit tests (incl. the exact bednet mix + the dollars-vs-cents case).
  • 3 rest-wiring tests: underfunded throws before POST; funded proceeds; total_budget not leaked to body; omitted → guard skipped (back-compat).
  • docs/atom-schemas.md regenerated; staleness + skill-atom-reference + registration-coverage gates pass.
  • Full suite 1930 passed.

Note on scope

The bednet smoke PDD's $0.50 rate is not edited here — the skill now rounds sub-$1 up to $1 and the code guard backstops any underfunding, so the durable fix doesn't depend on the shared AI-authored golden PDD. Re-validation forks from that golden: a healthy run proves the happy path, or opportunity_underfunded proves the guard fires — both validate the fix.

⚠️ MCP code change — needs a full Claude Code restart (not just /reload-plugins) to take effect, and a fresh seeded 3,4,6 re-validation. Supersedes the prose guard in #722.

🤖 Generated with Claude Code

@jjackson jjackson enabled auto-merge June 6, 2026 23:37
…boundary (#729)

The #722 budget guard was SKILL.md prose and failed live on
bednet-spot-check/20260606-2013 (on v0.13.557): the agent evaluated it in
dollars ($0.50) while storing cents (50), so number_of_users read as 5
in-head but 0.05 in Connect — Phase 4 shipped an unclaimable opp and the
guard never tripped. (Validation also confirmed #721's run-id opp-name
prefix works — that's why we know the new code was active.)

Move the guard into code at the MCP boundary, computed over the integers
actually sent to Connect, so no agent unit-confusion can slip an
underfunded opp through:

- New pure helper mcp/connect/opportunity-capacity.ts: minBudgetForOneUser,
  numberOfUsers, assertFundsAtLeastOneUser + typed OpportunityUnderfundedError
  (non-retryable, structured toJSON with per-PU breakdown).
- connect_create_payment_unit(s) take an optional total_budget; when supplied,
  the REST + Playwright backends assert total_budget ≥ Σ(max_total ×
  (amount + org_amount)) BEFORE POSTing (no orphan PU) and reject with
  opportunity_underfunded when number_of_users < 1. total_budget is
  guard-only — never forwarded in the request body.
- connect-opp-setup SKILL.md: ALWAYS pass total_budget; the hand-computed
  prose guard is replaced by a pointer to the code guard; sub-$1 rates round
  UP to the $1 minimum (never cents — Connect reads 50 as $50/visit).

Tests: 10 capacity-helper + 3 rest-wiring (underfunded throws before POST;
funded proceeds; total_budget not leaked to body; omitted → guard skipped).
Full suite 1930 green. Supersedes the prose guard in #722.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jjackson jjackson force-pushed the fix/connect-capacity-guard-code branch from c5bb507 to 6fa87c5 Compare June 6, 2026 23:48
@jjackson jjackson merged commit ff3060f into main Jun 6, 2026
2 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