fix(server): steer exact id sets to bug_info - #21
Merged
Conversation
bugs_quicksearch prefixes the status filter (default ALL) to the query, so under any non-empty status a query that is just a list of bug ids is content-matched and returns every bug that merely MENTIONS one of those numbers — a reported call with 8 ids came back with 14 bugs. With an empty status the query goes upstream bare, and Bugzilla routes a bare all-number query to an exact id lookup (bug_id + anyexact) instead. Either way the right tool for an exact id set is bug_info, whose envelope answers every requested id either as a bug or under 'restricted' instead of silently dropping it. Steer clients there in three places: - The bugs_quicksearch tool description and the query/status field docs now state the status-prefixing rule, its content-matching consequence, and the empty-status exception, and point exact id sets to bug_info. quicksearch_syntax, the README row, and the DESIGN.md row tell the same story, since Bugzilla's syntax page advertises a jump-to-bug- number shortcut that only the bare-query path takes. - A runtime advisory: when the query consists entirely of comma/ whitespace-separated bug ids (optional leading '#' per id), the result envelope carries a `note` saying so. The note is a pure function of the client's request — the query and status strings, never search results, guard verdicts, or anything upstream said — so it opens no new oracle (I2/I3), and the `bugs` array is byte-identical with or without it. On the empty-status path the note drops the content-matching claim (an id lookup is what actually happens there), and a query naming more distinct ids than bug_info's 25-per-call cap steers to batched bug_info calls instead of straight into the too_many_ids refusal — the cap is already public in that refusal's own text. The query is still searched, never rerouted. Integration tests pin all of it, including the oracle edges: the note appears for a pure id-list query, not for a content query; the served bugs match byte-for-byte apart from the note; a policy-hidden bug, an all-hidden (empty) result, and I14 link scrubbing each change the served bugs but never the note; and the wording tracks only the request's status and distinct-id count.
plusky
force-pushed
the
fix/quicksearch-id-routing
branch
from
July 28, 2026 08:13
72ca9f8 to
aa82cf9
Compare
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.
Problem
bugs_quicksearchwith a comma-separated id list does content matching on every path a normal call takes: the tool prefixes thestatusfilter (defaultALL) to the query, so the expression upstream is never purely numeric and each number is full-text matched — it also matches bugs that merely mention it in their text. A reported call with 8 ids returned 14 bugs. The correct tool for an exact set of known ids isbug_info, whose envelope answers every requested id either as a bug or underrestricted, instead of a search that silently drops what it may not show.There is one exception, and the texts now say so instead of overclaiming: with an explicitly empty
statusthe query is sent to Bugzilla bare, and Bugzilla routes a bare query of nothing but numbers to an exact id lookup (bug_id+bug_id_type=anyexact). See "The empty-status exception" below.What changed
bugs_quicksearchand itsquery/statusfield docs state the status-prefixing rule, its content-matching consequence, and the empty-status exception, and steer exact id sets tobug_info(bug_idsarray).quicksearch_syntaxgets the matching caveat: Bugzilla's syntax page advertises a jump-to-bug-number shortcut that only the bare-query path takes.#per id), the result envelope carries an extranotesteering tobug_info. Its wording tracks the request: on the empty-status path it drops the content-matching claim (an id lookup is what actually happens there), and when the query names more distinct ids thanbug_info's 25-per-call cap it steers to batchedbug_infocalls instead of straight into thetoo_many_idsrefusal — the cap is already public in that refusal's own text and in thebug_idsparameter doc, so the note discloses nothing new.bugs_quicksearchrows in README.md and the DESIGN.md tool table tell the same story: status prefixing, content matching under any non-empty status, the bare-query id lookup, and the advisory.The empty-status exception: accurate texts over normalization
QuicksearchParams.statusaccepts"", and such a call reaches the bare-query path where an all-number query IS an exact id lookup upstream. Two fixes were possible:status: ""so the bare path is unreachable and a categorical "never an id lookup" becomes true. Rejected: an empty status today means Bugzilla's default open-bugs search, so silently mapping""toALLchanges result semantics for existing callers, and rejecting""outright changes the API surface — both are behavior changes bought to keep a nicer sentence.p.statusas well asp.queryso it never asserts content matching on the id-lookup path while still steering tobug_infothere. Both are client-supplied request params, so the note remains a pure function of the request; guard behavior is untouched on every path — the id-lookup rows flow throughquicksearch_window→ filtering → I14 link scrubbing exactly like any other search rows.Why the advisory is safe (no new oracle)
The note is a pure function of the client's own request —
id_list_advisory(&p.query, &p.status)reads nothing but bytes the client itself sent. It never consults search results, guard verdicts, or anything upstream said, so its presence and wording tell the client nothing it did not already know (I2/I3 untouched). Thebugsarray is byte-identical with or without it — same bugs, same order — and the query is not rerouted: a client genuinely searching for a number (an error code, a year) still gets the search it asked for. Silent rerouting was considered and rejected as a semantics break for such callers.That property is pinned by mutation testing, not just asserted. Two oracle-shaped mutations survived the suite as it stood before the newest tests:
bugsarray is non-empty. Survived because no test exercised an id-list query whose every match is policy-hidden.allowed.len() != named.len()after the disclosability check). Survived because no test combined the advisory with a link-carrying projection; scrubbed ids are invisible to the client, so this would have been a genuinely covert verdict channel.Both mutations were applied to the source one at a time, confirmed to fail the new tests, and reverted:
quicksearch_advisory_survives_an_all_hidden_result(every match policy-hidden: servedbugsis empty and the note must be byte-identical to the all-visible run) andquicksearch_advisory_wording_tracks_status_and_id_count(notes asserted against an upstream serving no rows at all).quicksearch_advisory_unmoved_by_link_scrubbing(a served bug'sdepends_onnames a policy-hidden bug; the test first proves the link really is scrubbed to[], then that the note is byte-identical to the same request with the linked bug disclosable).The other advisory tests still pin: note for pure id lists only, envelopes identical apart from the note, and a policy-hidden result row never moving the note. All five advisory mutations now live in the coverage-contract header of
tools_wiremock.rs.Verification
cargo fmt --check,cargo clippy --workspace --all-targets --locked -- -D warnings,cargo test --workspace --locked(194 tests, 7 new),cargo deny check,typos— all clean.id_list_advisoryfires on pure id lists only, and its wording tracks exactly the request'sstatusand distinct-id count (batching mentioned only above 25 distinct ids; repeated spellings of one id count once).tools_wiremock.rs, real MCP session through the tool router): the advisory presence/byte-identity tests plus the two mutation killers and the end-to-end wording test (empty-status arm, over-cap arm).Invariants touched: I2/I3 (no new oracle — note keyed on request bytes only), I14 (scrubbing unchanged, now cross-checked against the advisory).