Skip to content

feat: identify this build to Bugzilla with a User-Agent - #58

Merged
plusky merged 1 commit into
mainfrom
issue-55-user-agent
Aug 4, 2026
Merged

feat: identify this build to Bugzilla with a User-Agent#58
plusky merged 1 commit into
mainfrom
issue-55-user-agent

Conversation

@plusky

@plusky plusky commented Aug 4, 2026

Copy link
Copy Markdown
Owner

What changed

BugzillaClient::new takes a user_agent and sets it once on the shared
reqwest::Client, so every request carries it — the authenticated REST calls,
the POST/PUT bodies, and the unauthenticated page.cgi fetch alike.
bugwarden-core never picks a value of its own: the binary crate supplies
server::USER_AGENT, {name}/{version} (+{repository}) from its own manifest,
and main constructs through the new server::bugzilla_client(&cli).

Why

Every request to Bugzilla was anonymous — accept and host and nothing more.
With a server-held key (#27) and no per-caller identity yet (#32), one Bugzilla
account carries a whole fleet's traffic, and the operator on the other end had
no name to allowlist or complain about.

Setting the header where the client lives is the trap:
env!("CARGO_PKG_NAME") in bugwarden-core expands to bugwarden-core and
would name the library in the access log of every binary embedding it — the
shape of #53 one layer down, and it would look entirely plausible. Hence a
parameter, and hence bugzilla_client living in server.rs where a test can
reach it.

Issue #55 left "whether to include a contact URL" open; this resolves it as
yes, read from CARGO_PKG_REPOSITORY, so the name leads somewhere without
becoming a second copy to keep in step.

Invariants touched

No guard behaviour moves: the policy engine, the tool gates and the audit
schema are untouched, and no CLI flag or policy key is added, so
examples/policy.toml and examples/audit.toml need no change.

I12 is cited as a discipline, not extended. Its text — the API key must
never appear in logs, error messages, or tool results — does not name outbound
request metadata, and this PR does not widen it. The header is held to the same
standard: it carries name, version and repository only, and cannot carry
runtime data, being a concat! of compile-time literals.

Version disclosure is accepted deliberately and now recorded in DESIGN.md.
Unlike #53's, this disclosure is not forced — User-Agent is optional, and
nothing was disclosed before — so it is a choice. It is accepted because the
party learning it already authenticates this deployment by API key and sees
every query, and because a version is what lets an operator tell whether a
reported misbehaviour is fixed in what is deployed.

How it was verified

cargo fmt --check, both clippy invocations, cargo test --workspace --all-targets --locked (357 tests), cargo deny check and typos are clean.

  • tests/binary_user_agent.rs spawns the shipped executable against a real
    HTTP server, drives an MCP session over stdio, and reads the header off the
    request that arrives. This exists because every assertion one call frame in
    still passes for a main that builds its own client — verified by mutation,
    see below. The same run pins that --use-auth-header reaches the constructor.
  • crates/bugwarden-core/tests/user_agent_wiremock.rs: the caller's value
    reaches all four request shapes in both auth modes, using an agent naming
    neither crate.
  • crates/bugwarden/src/server.rs: a request through bugzilla_client carries
    the production value, matching the identity the MCP handshake advertises.
  • crates/bugwarden-core/src/client.rs: blank and non-header-value identities
    fail at construction.
  • Beyond the suite, the built binary was probed by hand over stdio against a
    local HTTP server and sent
    bugwarden/0.3.0 (+https://github.com/plusky/bugwarden).

Adversarial review

Three reviewers (completeness, test-strength with mutation, docs and process).
Findings addressed in this branch:

  • Five surviving mutants, all now killed and re-verified: main.rs
    bypassing bugzilla_client and shipping curl/7.68.0; the identity dropped
    whenever --use-auth-header is set; bugzilla_client ignoring that flag and
    putting the key back in the URL; a per-request header on the PUT path only
    (reqwest replaces the client default rather than duplicating it, so this
    class of defect is silent); and an assert_eq! comparing the header to the
    same manifest fields the code reads, which agreed with any value they took.
  • I12 cited for constraints it does not contain (policy path, host,
    outbound headers), in three places — narrowed to "the discipline of I12 on a
    surface I12 does not name".
  • The DESIGN.md Testing bullet named the wrong files and omitted the
    discover test; corrected.
  • Prose that outran what the code proves: "cannot drift from where the project
    actually lives", a blank-identity clause reading as if this binary could hit
    it, a test-constant comment claiming a proof that lives in another file, and
    an assertion that no key material reaches a compile-time constant — which
    cannot fail, and which DESIGN.md then recorded as proven. Removed.

Rebutted: validating base_url at construction so a transposed
new(agent, bool, url) fails at startup rather than at request time. A real
hazard, but a separate concern from #55; the one production call site is
covered, since a transposition there fails the new tests. Worth a follow-up
issue if wanted.

Note for the reviewer

The DESIGN.md "Identity tests" bullet also covers #53's tests, which landed in
f944402 without a Testing entry. Documented here because the two directions
of one identity read as a single bullet; say the word and it splits into its
own docs(design): commit on this branch.

reqwest sends no User-Agent unless one is configured, so every request
bugwarden made to Bugzilla arrived anonymous: accept and host and
nothing more. Run as fleet infrastructure with a server-held key (#27)
and no per-caller identity yet (#32), one Bugzilla account carries the
whole fleet's traffic, and the operator on the other end had no name to
allowlist or complain about — nothing distinguished it from a person
with a browser.

The identity is threaded in as a parameter rather than built where the
client lives. Built in bugwarden-core, env!("CARGO_PKG_NAME") expands to
bugwarden-core and would name the library in the access log of every
binary embedding it: the shape of #53 one layer down, and just as
plausible-looking. The binary crate supplies server::USER_AGENT,
{name}/{version} (+{repository}) from its own manifest, and main builds
its client through server::bugzilla_client — the single production
construction path, and unlike a constructor call in main, one a test can
reach. A test drives the shipped binary itself, because every assertion
one call frame in still passes for a main that builds its own client.

BugzillaClient::new refuses a blank or non-header-value user_agent at
construction, so an embedder cannot get an anonymous client back from a
value that silently resolved to nothing.

The header is public — it lands in every configured Bugzilla's access
log — so it carries name, version and the project repository and nothing
else: no key material, no policy path, no host. That is the discipline
of I12 on a surface I12 does not itself name; no invariant text changes.
Including the version is a deliberate choice recorded in DESIGN.md:
unlike #53's, this disclosure is optional, and the party learning it is
the Bugzilla this deployment already authenticates to with an API key
and sends every query.

BREAKING CHANGE: BugzillaClient::new takes a third argument, so the next
bugwarden-core release cannot be a patch.

Closes #55
@plusky
plusky merged commit e279802 into main Aug 4, 2026
11 checks passed
@plusky
plusky deleted the issue-55-user-agent branch August 4, 2026 20:55
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.

1 participant