feat(server): add create_bug and add_attachment - #20
Merged
Conversation
Two write tools the server did not have: filing a bug, and uploading an attachment to one. Filing needs a gate the guard had no shape for. Every existing check asks whether a client may touch a bug that exists; a bug being filed does not. So the bug as REQUESTED is classified against the same rules — with one exception that decides how much the mechanism is worth. Bugzilla unions the product's mandatory groups into whatever the request asked for, so a client's claim about `groups` can only under-state the result. Trusting it inverted the guard: omitting the field failed closed, while naming any group the policy's glob missed sailed through, and the bug that came out was one the policy hides. Supplying a field was strictly more permissive than omitting it. `may_create` therefore treats groups as unknown whatever the payload says, which fails closed (I4) and has a consequence worth stating plainly rather than engineering around: a policy whose rules consult groups refuses ALL creation, because what the bug will look like cannot be known before it exists. Every other field is taken verbatim by Bugzilla or the create fails upstream, so those claims are evidence and are trusted. The prospective bug is dated now, so an age rule refuses creation wherever it would immediately hide the result. Filing a bug nobody may then read is not a service. Both refusals — guard and upstream — return the same text and cost the same one upstream request; the refused path burns a classify against bug id 0, as download_attachment already does. Without that, a client could send a deliberately invalid version so upstream always errored, and read the policy off which refusal came back, field by field, creating nothing. Uploading is gated by Attach, deliberately separate from Attachments: that one is the read side, and an operator who lets a client read attachments has not agreed to let it write them. Both new capabilities are writes, so read_only strips them and delists both tools (I13). Uploads are held to the same max_attachment_bytes ceiling as downloads, measured on the decoded length; the refusal names no number, since the cap is policy (I1). Existing deployments should read the upgrade note: an `allow` rule grants every capability, so both of these arrive switched on. The binary crate gains a lib target so the tools can be driven by tests at all. Without it nothing could call a tool, and three mutations that gut both gates — swapping the write capability for the read one, dropping the create check, dropping the size check — passed the entire suite.
plusky
force-pushed
the
feat/create-bug-and-attachment
branch
from
July 28, 2026 07:18
96e5ab2 to
3891bb9
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.
Two write tools the server did not have: filing a bug, and uploading an
attachment to one.
Filing needed a gate the guard had no shape for
Every existing check asks may this client touch that bug? — a bug being filed
does not exist yet. So the bug as requested is classified against the same
rules, with one exception that decides how much the mechanism is worth.
Bugzilla augments what the request asks for. It unions the product's
mandatory groups into the new bug, so a client's claim about
groupscan onlyever under-state the result. Trusting it inverted the guard:
groupsomittedgroups: ["security-embargo"]groups: ["partner"]security-*and the bug is one the policy hidesSupplying a field was strictly more permissive than omitting it.
may_createnow treats groups as unknown whatever the payload says. The consequence is
stated plainly rather than engineered around: a policy whose rules consult
groups refuses all creation, because what the bug will look like cannot be
known before it exists. Every other create field is taken verbatim by Bugzilla
or the create fails upstream, so those claims are evidence and are trusted.
The prospective bug is dated now, so an age rule refuses creation wherever
it would immediately hide the result.
Both refusals are indistinguishable
Guard refusal and upstream refusal return the same text and cost the same one
upstream request — the refused path burns a classify against bug id 0, the
padding precedent
download_attachmentalready sets. Without it, a clientcould send a deliberately invalid
versionso upstream always errored, andread the policy off which refusal came back — field by field, creating nothing.
A successful create is still distinguishable, which is the tool working, and
it costs a real attributable bug.
Uploading
Gated by a new
Attachcapability, separate from the read-sideAttachments: an operator who lets a client read attachments has not agreed tolet it write them. Both new capabilities are writes, so
read_onlystrips themand delists both tools (I13). Uploads are held to the same
max_attachment_bytesceiling as downloads, measured on the decoded length;the refusal names no number, since the cap is policy (I1).
Upgrade note
Capability::ALLwent 11 → 13, and anallowrule grants all of them — soevery existing deployment that allows anything gains bug filing and attachment
upload on upgrade. To keep the old behaviour:
disabled_tools = ["create_bug", "add_attachment"], or convertallowrules to enumeratedrestrict.Read-only deployments are unaffected. Documented in README and DESIGN.md.
The tests that were missing
The binary crate had no lib target, so nothing could drive a tool — and
three mutations that gut both gates (swapping the write capability for the read
one, dropping the
may_createcall, dropping the size check) passed the entiresuite. There is a lib target now, plus integration tests that stand up a real
server over an in-memory transport and call the tools through an MCP client
against wiremock, so a tool that stops calling its guard fails a test.
187 tests. Every fix mutation-verified, and I re-ran the two most important
mutations independently after the fixes: both now die.
Also fixed:
add_attachmentwas sendingBug.update's{"comment":{"body":…}}shape, but
Bug.add_attachmentdocumentscommentas a plain string. And theshipped
examples/policy.tomladvertised creation while its first rule made itimpossible — it now says so, and a test parses the shipped file to keep that
true.