Skip to content

feat(server): add create_bug and add_attachment - #20

Merged
plusky merged 1 commit into
mainfrom
feat/create-bug-and-attachment
Jul 28, 2026
Merged

feat(server): add create_bug and add_attachment#20
plusky merged 1 commit into
mainfrom
feat/create-bug-and-attachment

Conversation

@plusky

@plusky plusky commented Jul 28, 2026

Copy link
Copy Markdown
Owner

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 groups can only
ever under-state the result. Trusting it inverted the guard:

payload old behaviour
groups omitted Unknown → denied (fail closed, correct)
groups: ["security-embargo"] denied
groups: ["partner"] allowed — then Bugzilla adds security-* and the bug is one the policy hides

Supplying a field was strictly more permissive than omitting it. may_create
now 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_attachment already sets. Without it, 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.
A successful create is still distinguishable, which is the tool working, and
it costs a real attributable bug.

Uploading

Gated by a new Attach capability, separate from the read-side
Attachments: 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).

Upgrade note

Capability::ALL went 11 → 13, and an allow rule grants all of them — so
every 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 convert allow rules to enumerated restrict.
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_create call, dropping the size check) passed the entire
suite. 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_attachment was sending Bug.update's {"comment":{"body":…}}
shape, but Bug.add_attachment documents comment as a plain string. And the
shipped examples/policy.toml advertised creation while its first rule made it
impossible — it now says so, and a test parses the shipped file to keep that
true.

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
plusky force-pushed the feat/create-bug-and-attachment branch from 96e5ab2 to 3891bb9 Compare July 28, 2026 07:18
@plusky
plusky merged commit fef9355 into main Jul 28, 2026
10 checks passed
@plusky
plusky deleted the feat/create-bug-and-attachment branch July 28, 2026 07:21
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