Repository navigation
Description
Summary
Buyer B can accept a negotiation that buyer A opened on A's own proposal, by naming A's proposal_id. Nothing checks that B is the negotiation's buyer. We observed this on v2.4.2 plus the #72 approval-resume patch, and on main.
On main (902e3f57), the consequence is larger. B names its own quote_id in the accept, and the quote-binding check passes because B owns that quote. B then books its quote at the price A negotiated, and the stored deal is linked through the quote index to A's proposal.
#96 stopped a buyer binding another buyer's quote to its own negotiation. This is the other direction: a buyer binding its own quote to someone else's negotiation.
Setup
- seller-agent commit
902e3f57e6c5c55dd2c00956132f1fd44e70d513(the testedmainsnapshot); - CSV ad server, SQLite and a fresh database for each run;
APPROVAL_GATE_ENABLED=true,APPROVAL_REQUIRED_FLOWS=proposal_decision;- a synthetic product with base CPM US$45.00 and floor US$38.25;
- a deterministic stand-in answering the proposal review; subsequent steps run the seller's own code;
- buyer A and buyer B each have their own agency-tier API key.
The older comparison target was v2.4.2 at e5b367d2b780aa4d0e03b6363d744ba3adf2b221, plus the #72 approval-resume patch.
The test supplied buyer A's proposal ID to buyer B. It did not test how B might discover that identifier. All data is synthetic.
Steps and observed result on main (identifiers from our run)
- Buyer A:
POST /proposalsat US$42.08 →prop-57cae977,accept,pending_approval. The operator approves it. - Buyer A:
POST /proposals/prop-57cae977/counterat US$40.00. The seller counters at US$40.25 and opensneg-514d30e5. The stored negotiation recordsbuyer_tier: agencyand no buyer identity. - Buyer B:
POST /api/v1/quotesattarget_cpmUS$40.00 →qt-1699a95cbb20, issued to B. - Buyer B:
POST /api/v1/negotiations/messageswithaction: accept,proposal_id: prop-57cae977,quote_id: qt-1699a95cbb20→ 200.neg-514d30e5becomesacceptedat US$40.25 with B's quote bound to it, andnegotiation_by_quote:qt-1699a95cbb20points toprop-57cae977. - Buyer B:
POST /api/v1/dealswithqt-1699a95cbb20→ 200. A deal is stored withstatus: proposedat US$40.25, rationale "… | Negotiated to $40.25 CPM (neg-514d30e5)". B's quote becomesbooked.
The seller's idempotency records for steps 3 to 5 are scoped to buyer B.
Control: with buyer A making steps 3 to 5, the result is the same: a linked deal at US$40.25.
At v2.4.2 plus #72: step 4 also returns 200 and marks A's negotiation accepted. At that version, booking doesn't read a proposal-led negotiation, so B's deal is stored at its quote's US$40.00 and isn't linked to A's proposal. So the missing ownership check predates main. What's new on main is that it lets B take A's negotiated price.
Nothing was executed externally, and the price stayed above the floor.
Questions
- Should accepting a negotiation (and countering it) be limited to the buyer it belongs to, as booking is limited to the buyer the quote was issued to (fix: booking accepted anonymous callers - require verified buyer matching the quote #77)?
- Should the stored negotiation record that buyer's identity, so there is something to check against?
Activity
jaanijuk commentedon Nov 4, 2026
Related: #129 covers the approval decision on the linked negotiation-and-booking path.