Skip to content

fix(mcp): code-level spend gate + conflict/envelope fixes on MCP-first delivery - #173

Merged
ArtyETH06 merged 1 commit into
milstan/expose-mcp-search-qualifyfrom
ArtyETH06/mcp-first-delivery-fixes
Aug 3, 2026
Merged

fix(mcp): code-level spend gate + conflict/envelope fixes on MCP-first delivery#173
ArtyETH06 merged 1 commit into
milstan/expose-mcp-search-qualifyfrom
ArtyETH06/mcp-first-delivery-fixes

Conversation

@ArtyETH06

@ArtyETH06 ArtyETH06 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

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-qualify rather than main.

Why these, specifically

WORKFLOWS.md had committed conflict markers. Literal <<<<<<< /
======= / >>>>>>> at lines 60–67 and 1025–1120, and main's friction rows
claimed the same numbers 46/47 as the delivery rows. workflows.test.ts and
workflow-contract-schema.test.ts parse this file, so it was a hard audit
failure, not cosmetic. Resolved by keeping both sets: friction stays 46/47,
delivery renumbers to 48/49/50.

leadbay_qualify_leads could spend without consent. qualify defaults to
true backend-side (~94 cost_cents/lead) and the schema had no required
array, so the minimal valid call — {lead_refs: [...500 refs]} — submitted a
paid job with no dry_run, no max_cost, no confirmation, and no request_id
to dedupe a retry. The gate existed only in description prose. It now lives in
code, mirroring the enrich-titles consent gate (product#3848): a paid call
needs confirm: true, otherwise the submit is withheld and a real backend
quote comes back as mode: "needs_confirmation". confirm: false vetoes with
no round-trip; fully free calls (qualify: false, no channels) pass straight
through untouched.

Driving the compiled composite with a client that throws if a paid POST
ever reaches the wire — the previously-unguarded 500-ref call:

mode          : needs_confirmation
submitted     : false
paid_because  : qualify is on (backend default is true — pass qualify:false to keep it free)
quoted max    : $470.00
calls made    : POST /mcp/qualify dry_run=true      ← the FREE quote only

confirm:false → vetoed: true | quote: null | network calls: 0

Prompt↔code drift that fires on the happy path. The leadbay_new_leads
paid-quote step listed min_ai_score among "the exact flags" and then routed
to leadbay_qualify_leads, which has no such property and
additionalProperties: false — following the prompt literally was a hard
schema rejection. Flags are now split per tool.

Also fixed

  • qualify_leads returns {leads, skipped} via the existing 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
    kept for input-order per-ref mapping.
  • lead_job_status forwards since/limit through the block-waiting path —
    waitForJob dropped them, silently turning an incremental poll into a full
    re-read of already-seen leads.
  • waitForJob bounds each sleep by the remaining wait, so wait_seconds: 1
    no longer blocks for a full 4s interval.
  • cost_cents renders as $, not — the rest of the repo (quota-windows,
    leadbay_account_status) denominates the same ledger in dollars.
  • findNewLeads / qualifyLeads / leadJobStatus exported from the core
    entry point.

One existing test touched — deliberately

packages/core/test/unit/composite/qualify-leads.test.ts gains a single
confirm: true. That test purchases an email channel with no consent, so the
new 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:build and in sync.

Not verified live against staging. The /1.6/mcp/* endpoints exist only on
staging, and the tokens available here authenticate against production only
(prod token → 200 on /1.5/users/me, 401 on staging). So the gate is proven
through the compiled bundle and unit tests, not against a real backend job.

No /eval run: the behaviour changed here (a withheld paid submit) has no
existing eval contract, and the harness needs an .env.eval staging-token
decision that hasn't been made.

Not addressed here

  • This is not releasable yet. POST /1.6/mcp/search returns 404 on
    production
    (api-us and api-fr, confirmed with a valid authenticated
    prod token) and 401 on staging. The backend release is the blocker, not this
    code.
  • product#3925 (FullEnrich lastname.empty terminal-fails a qualify job) and
    product#3926 (example_lead.name poisons the seed) are backend-side and
    still open.

…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>
@ArtyETH06 ArtyETH06 self-assigned this Aug 3, 2026
@ArtyETH06
ArtyETH06 merged commit b866a42 into milstan/expose-mcp-search-qualify Aug 3, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant