Skip to content

feat(server): add product and field discovery tools - #74

Merged
plusky merged 2 commits into
plusky:mainfrom
mimi1vx:feat/discovery-tools
Aug 8, 2026
Merged

feat(server): add product and field discovery tools#74
plusky merged 2 commits into
plusky:mainfrom
mimi1vx:feat/discovery-tools

Conversation

@mimi1vx

@mimi1vx mimi1vx commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Two self-contained commits, each verified independently (builds/tests/lints
clean on its own).

What

create_bug takes 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 file
into", "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 BugzillaClient methods: enterable_product_ids
    (GET /rest/product_enterable), products (GET /rest/product, with
    ids/names/include_fields all optional), and bug_fields
    (GET /rest/field/bug[/{name}]). Same get_json/.without_url() auth and
    sanitization 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 MCP
    tools, bugzilla_products and bug_fields. Both take no bug id (no guard
    capability applies, I8 doesn't apply), and both are exposed only when the
    operator sets a new global.allow_discovery = true policy key (default
    false); otherwise they're removed from the tool listing via the same
    ToolRouter::remove_route mechanism 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 own
    projection — an enforced omission, never an unenforced include_fields
    request.

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 padded
uniform refusal pays a wasted upstream request to avoid for a single product.
An operator who wants product or field names withheld simply leaves
allow_discovery off (the default), which removes the routes from the
listing entirely rather than serving a filtered — and therefore
information-leaking — catalog.

Invariants touched

  • New I16: both tools are unfiltered pass-through of instance metadata,
    never bug data, no capability applies, removed from the listing unless
    global.allow_discovery = true.
  • I8 — unaffected; explicitly does not apply (no bug id to assess).
  • I13 — reused mechanism (ToolRouter::remove_route), same as read-only
    and disabled_tools.
  • I12 — the new client methods share the exact same auth/sanitize path as
    every existing call; a dedicated wiremock suite asserts the key never
    appears in error text.

Verification

  • cargo fmt --check
  • cargo clippy --workspace --all-targets --locked -- -D warnings
  • cargo clippy -p bugwarden --features gen --all-targets --locked -- -D warnings
  • cargo test --workspace --all-targets --locked — all green, including:
    • crates/bugwarden-core/tests/discovery_wiremock.rs (new): envelope
      shapes 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 error
      text.
    • crates/bugwarden/tests/tools_wiremock.rs: catalog vs. detail
      projection for both tools, default_assigned_to/default_qa_contact
      never appear in a response, the on_bug_entry_only filter, legal
      values 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.rs unit tests: discovery tools
      absent/present per policy, a discovery tool name stays a valid
      disabled_tools entry with discovery off.
    • crates/bugwarden/tests/audit_wiremock.rs: the one-record-per-call
      guarantee (I15) extended to cover both new routes.
  • cargo deny check — clean (no new dependencies).
  • Each commit verified standalone in an isolated worktree (fmt, clippy,
    test) — both build and pass on their own, not just as a pair.
  • Live-probed all four Bugzilla routes (product_enterable, product,
    field/bug, field/bug/{name}) against bugzilla.mozilla.org:
    product_enterable returns JSON-number ids there (not the string ids
    Bugzilla's own docs show as an example), which is why the client accepts
    both shapes; the other three endpoints matched the documented shape
    exactly.

mimi1vx added 2 commits August 8, 2026 16:59
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.
@plusky
plusky merged commit 337082f into plusky:main Aug 8, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants