fix(mcp): code-level spend gate + conflict/envelope fixes on MCP-first delivery - #173
Merged
ArtyETH06 merged 1 commit intoAug 3, 2026
Conversation
…t delivery Review fixes on top of milstan/expose-mcp-search-qualify (PR #168). Not a replacement for that PR — the delivery tools are Milan's; these are the defects found reviewing it. - WORKFLOWS.md: resolve the committed merge-conflict markers. main's friction rows keep 46/47; the delivery rows renumber to 48/49/50. Both contract sets retained. The workflows + contract-schema audits parse this file, so the markers were a hard audit failure. - qualify_leads: enforce the spend gate in code. `qualify` defaults to TRUE backend-side (~94 cost_cents/lead) and the schema had no required params, so `{lead_refs: [...500]}` alone submitted a paid job nobody approved. A paid call now needs `confirm: true`; without it the submit is withheld and a real dry-run quote is returned (`mode: "needs_confirmation"`). `confirm: false` is a veto with no round-trip; fully free calls (qualify:false, no channels) pass straight through. Mirrors the existing enrich-titles consent gate (product#3848). - qualify_leads: also return {leads, skipped} via splitItems. Its own RENDER block reads leads[]/skipped[] and both sibling tools split; returning only items[] left an agent with two empty tables. items[] is preserved for input-order per-ref mapping. - lead_job_status: forward since/limit through the block-waiting path. waitForJob dropped them, silently turning an incremental poll into a full re-read of already-seen items. - waitForJob: bound each sleep by the remaining wait so a short wait_seconds can't block for a whole 4s interval. - Currency: cost_cents renders as $ not € (the rest of the repo, including quota-windows and account_status, denominates the same ledger in dollars). - Export findNewLeads/qualifyLeads/leadJobStatus from the core entry point. - leadbay_new_leads prompt: the paid-quote step listed min_ai_score among the flags then routed to qualify_leads, which has no such property and additionalProperties:false — a literal follow was a hard schema rejection. Flags are now split per tool, and the confirm:true requirement documented. One existing test (qualify-leads.test.ts happy path) gains `confirm: true`: it purchases an email channel, so the gate withholds it. That edit is deliberate and approved — it asserted the behaviour this commit removes. pnpm -r build, -r test (1301 passed), -r typecheck all green. Co-Authored-By: Claude <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.
Review fixes on top of #168. The three delivery tools are Milan's work — this
branch only carries the defects found reviewing that PR, so it targets
milstan/expose-mcp-search-qualifyrather thanmain.Why these, specifically
WORKFLOWS.mdhad committed conflict markers. Literal<<<<<<</=======/>>>>>>>at lines 60–67 and 1025–1120, andmain's friction rowsclaimed the same numbers 46/47 as the delivery rows.
workflows.test.tsandworkflow-contract-schema.test.tsparse this file, so it was a hard auditfailure, not cosmetic. Resolved by keeping both sets: friction stays 46/47,
delivery renumbers to 48/49/50.
leadbay_qualify_leadscould spend without consent.qualifydefaults totruebackend-side (~94 cost_cents/lead) and the schema had norequiredarray, so the minimal valid call —
{lead_refs: [...500 refs]}— submitted apaid job with no
dry_run, nomax_cost, no confirmation, and norequest_idto dedupe a retry. The gate existed only in description prose. It now lives in
code, mirroring the
enrich-titlesconsent gate (product#3848): a paid callneeds
confirm: true, otherwise the submit is withheld and a real backendquote comes back as
mode: "needs_confirmation".confirm: falsevetoes withno round-trip; fully free calls (
qualify: false, no channels) pass straightthrough untouched.
Driving the compiled composite with a client that throws if a paid POST
ever reaches the wire — the previously-unguarded 500-ref call:
Prompt↔code drift that fires on the happy path. The
leadbay_new_leadspaid-quote step listed
min_ai_scoreamong "the exact flags" and then routedto
leadbay_qualify_leads, which has no such property andadditionalProperties: false— following the prompt literally was a hardschema rejection. Flags are now split per tool.
Also fixed
qualify_leadsreturns{leads, skipped}via the existingsplitItems. Itsown RENDER block reads
leads[]/skipped[]and both sibling tools split;returning only
items[]left an agent with two empty tables.items[]iskept for input-order per-ref mapping.
lead_job_statusforwardssince/limitthrough the block-waiting path —waitForJobdropped them, silently turning an incremental poll into a fullre-read of already-seen leads.
waitForJobbounds each sleep by the remaining wait, sowait_seconds: 1no longer blocks for a full 4s interval.
cost_centsrenders as$, not€— the rest of the repo (quota-windows,leadbay_account_status) denominates the same ledger in dollars.findNewLeads/qualifyLeads/leadJobStatusexported from the coreentry point.
One existing test touched — deliberately
packages/core/test/unit/composite/qualify-leads.test.tsgains a singleconfirm: true. That test purchases an email channel with no consent, so thenew gate correctly withholds it — it was asserting the behaviour this branch
removes. Flagging it explicitly because the repo rule is otherwise "never
modify existing test files"; all other coverage is in two NEW files
(
qualify-leads-spend-gate.test.ts,lead-job-status-cursor.test.ts).Verification
pnpm -r build,pnpm -r test(1301 passed, 200 files),pnpm -r typecheck— all green, observed locally. Generated files regenerated via
pnpm prompts:buildand in sync.Not verified live against staging. The
/1.6/mcp/*endpoints exist only onstaging, and the tokens available here authenticate against production only
(prod token →
200on/1.5/users/me,401on staging). So the gate is proventhrough the compiled bundle and unit tests, not against a real backend job.
No
/evalrun: the behaviour changed here (a withheld paid submit) has noexisting eval contract, and the harness needs an
.env.evalstaging-tokendecision that hasn't been made.
Not addressed here
POST /1.6/mcp/searchreturns 404 onproduction (
api-usandapi-fr, confirmed with a valid authenticatedprod token) and 401 on staging. The backend release is the blocker, not this
code.
lastname.emptyterminal-fails a qualify job) andproduct#3926 (
example_lead.namepoisons the seed) are backend-side andstill open.