feat(server): add product and field discovery tools - #74
Merged
Conversation
create_bug takes a fixed parameter set and has no way to learn which
products/components/versions exist or which fields a product
requires on entry, so a product with a mandatory custom field cannot
be filed into at all, and nothing on the client surface answers
"which products may I file into" or "what are this field's legal
values". update_bug_fields already has the cf_* update machinery;
the create side had no discovery equivalent to point an operator or
an LLM client at what create_bug expects.
Add three read-only BugzillaClient methods mirroring stock Bugzilla
Core v1 routes: enterable_product_ids (GET /rest/product_enterable),
products (GET /rest/product, ids/names/include_fields all optional),
and bug_fields (GET /rest/field/bug[/{name}]). Each follows the
existing client conventions: get_json's auth and .without_url()
sanitization (I12), and a missing/malformed envelope is an error,
never a silently empty result (the valid_login precedent).
product_enterable's documented example encodes ids as strings, but a
live probe against bugzilla.mozilla.org returned JSON numbers
instead -- both shapes are accepted. bug_fields percent-encodes a
caller-supplied field name as a single URL path segment via
Url::path_segments_mut, rather than interpolating it into a format
string, so an embedded '/' cannot address a different endpoint.
Verified live against bugzilla.mozilla.org: all three routes
(product_enterable, product, field/bug and field/bug/{name}) behave
as documented.
Add two new MCP tools built on the discovery client endpoints:
bugzilla_products (no args: enterable products as {id, name}; named,
up to 5: components/versions/milestones) and bug_fields (no args:
every bug field except its legal values; named, up to 5: full detail
including legal value names). Both take no bug id, so no guard
capability applies and I8 does not apply.
Both tools are pure pass-through of Bugzilla's own answer to the
server's key -- never filtered against the guard policy. Bugzilla
already scopes product and field visibility to the caller's key;
filtering the catalog again here would itself be a policy-enumeration
oracle, exactly the kind of leak create_bug's padded uniform refusal
exists to prevent for a single product. Operators who want product or
field names withheld leave the new global.allow_discovery switch off
(the default): both tools are then removed from the tool listing via
ToolRouter::remove_route, the same I13 mechanism read-only mode uses,
so a policy-enumeration oracle can never be introduced by accident.
Detail calls are capped at 5 names with a fixed refusal text and zero
upstream requests over the cap -- a large instance's uncapped field
catalog is hundreds of KB of legal values that would land verbatim in
a model's context, so the catalog view never carries values and only
the (bounded) detail view does. Component objects also drop
default_assigned_to/default_qa_contact locally: they are account
emails, and the local projection is the enforced guarantee, not an
unenforced include_fields request to Bugzilla.
Recorded as DESIGN.md invariant I16, with the two new tool rows,
the global.allow_discovery policy key documented in README.md and
examples/policy.toml, and the one-record-per-call audit test
extended to cover the new routes.
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.
Two self-contained commits, each verified independently (builds/tests/lints
clean on its own).
What
create_bugtakes a fixed parameter set (product,component,summary,version,description,severity,priority,op_sys,platform,keywords,groups) and has no way to answer "which products may I fileinto", "which components/versions does this product have", or "which fields
are mandatory on entry and what are their legal values" — the caller has to
already know. Nothing on the tool surface exposes that instance metadata.
feat(core): add product and field discovery endpoints to the client—three new read-only
BugzillaClientmethods:enterable_product_ids(
GET /rest/product_enterable),products(GET /rest/product, withids/names/include_fieldsall optional), andbug_fields(
GET /rest/field/bug[/{name}]). Sameget_json/.without_url()auth andsanitization path as every other client call (I12); a missing/malformed
envelope is an error, never a silently empty result.
feat(server): add guarded product and field discovery tools— two new MCPtools,
bugzilla_productsandbug_fields. Both take no bug id (no guardcapability applies, I8 doesn't apply), and both are exposed only when the
operator sets a new
global.allow_discovery = truepolicy key (defaultfalse); otherwise they're removed from the tool listing via the sameToolRouter::remove_routemechanism read-only mode uses (I13). Detail calls(naming specific products/fields) are capped at 5 names with a fixed
refusal and zero upstream requests over the cap.
default_assigned_to/default_qa_contact(account emails) are dropped by the server's ownprojection — an enforced omission, never an unenforced
include_fieldsrequest.
Why the policy stays unfiltered here
Both tools return exactly what Bugzilla answers to the server's own key —
never filtered against the guard policy. Bugzilla already scopes product and
field visibility per key; filtering the catalog again here would itself be a
policy-enumeration oracle, the exact class of leak
create_bug's paddeduniform refusal pays a wasted upstream request to avoid for a single product.
An operator who wants product or field names withheld simply leaves
allow_discoveryoff (the default), which removes the routes from thelisting entirely rather than serving a filtered — and therefore
information-leaking — catalog.
Invariants touched
never bug data, no capability applies, removed from the listing unless
global.allow_discovery = true.ToolRouter::remove_route), same as read-onlyand
disabled_tools.every existing call; a dedicated wiremock suite asserts the key never
appears in error text.
Verification
cargo fmt --checkcargo clippy --workspace --all-targets --locked -- -D warningscargo clippy -p bugwarden --features gen --all-targets --locked -- -D warningscargo test --workspace --all-targets --locked— all green, including:crates/bugwarden-core/tests/discovery_wiremock.rs(new): envelopeshapes for all three endpoints, string- and number-encoded ids, a
malformed envelope errors rather than returning an empty list, path
percent-encoding for a field name containing
/, no API key in errortext.
crates/bugwarden/tests/tools_wiremock.rs: catalog vs. detailprojection for both tools,
default_assigned_to/default_qa_contactnever appear in a response, the
on_bug_entry_onlyfilter, legalvalues present in detail but absent from the catalog, over-cap refusals
make zero upstream requests, both tools absent from the listing by
default and present under
allow_discovery = true.crates/bugwarden/src/server.rsunit tests: discovery toolsabsent/present per policy, a discovery tool name stays a valid
disabled_toolsentry with discovery off.crates/bugwarden/tests/audit_wiremock.rs: the one-record-per-callguarantee (I15) extended to cover both new routes.
cargo deny check— clean (no new dependencies).test) — both build and pass on their own, not just as a pair.
product_enterable,product,field/bug,field/bug/{name}) againstbugzilla.mozilla.org:product_enterablereturns JSON-number ids there (not the string idsBugzilla's own docs show as an example), which is why the client accepts
both shapes; the other three endpoints matched the documented shape
exactly.