Skip to content

feat(server): widen update_bug_fields per the update-field audit - #40

Merged
plusky merged 1 commit into
mainfrom
update-fields-38
Aug 2, 2026
Merged

feat(server): widen update_bug_fields per the update-field audit#40
plusky merged 1 commit into
mainfrom
update-fields-38

Conversation

@plusky

@plusky plusky commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Closes #38.

Implements the audit posted on the issue: every PUT /rest/bug parameter is now either exposed through a named tool param or withheld with its reason recorded in DESIGN.md — the split is a decision, not an accident of tool growth.

What changed

  • update_bug_fields gains summary, url, whiteboard, version, target_milestone, keywords_add/keywords_remove, and see_also_add/see_also_remove (bug URLs), all under the existing fields capability. Add/remove objects are built the way update_bug_dependencies builds them; the replace-all set variant is unreachable by construction.
  • see_also targets are guard-assessed before the PUT: local-instance URLs are resolved to bug ids (Guard::see_also_local_id, the same parser the read paths use for scrubbing) and each target must pass Capability::Summary — the I14/I8/I11 bar update_bug_dependencies and mark_as_duplicate already apply — so a see_also write can neither link into a policy-denied bug (Bugzilla records a reciprocal entry on the target) nor be used as an existence oracle over denied ids (I2). Non-local URLs pass through untouched.
  • Empty strings/lists count as absent; the at-least-one-field check counts the new params; the cf_ gate (I7) is untouched and now pinned by a test proving see_also cannot be smuggled through custom_fields; one bug per call stays.
  • Server log records only presence/counts for the new params (summaries/whiteboards can carry embargoed content); the audit PARAM_ALLOWLIST records keywords_add/keywords_remove/target_milestone by value (closed instance vocabulary) while the free-text params stay length-only.
  • DESIGN.md carries the full exposed/withheld audit table, including the corrected matcher-visibility bound (see below); README, Capability::Fields rustdoc, and examples/policy.toml updated in step.

Adversarial review record (pre-PR gate)

Three hostile lenses plus mutation verification against the implementation commit; 8 findings, all addressed, none rebutted:

  • blocking (security)see_also existence oracle: as implemented first, see_also targets were never assessed, so writing a link probed denied ids through Bugzilla's distinguishable success/no-such-bug/not-authorized responses and wrote reciprocal entries onto denied bugs. Fixed as described above; pinned by update_fields_see_also_targets_respect_the_guard (uniform denial, zero PUTs). Since this fix landed after the workflow's mutation pass, I re-verified it by hand: neutering the target extraction makes the pin test fail.
  • blocking (docs) — the DESIGN.md comment_is_private row had been reworded into claiming existing-comment privacy is handled elsewhere (it is not — it is withheld); restored to the plan's new-comment-privacy meaning.
  • important — the matcher-visibility paragraph claimed "a bug a rule denies cannot be written at all" as the bound, which holds for deny (grants nothing) but not for a restrict grant that includes fields while withholding read: such a grant can be escaped in one write along a matched axis. The DESIGN.md record now states the bound precisely, including that a restrict rule granting fields on a matcher-visible axis is an escape hatch the operator must not build.
  • minor ×5, all fixed: keywords_*/target_milestone added to the audit param allowlist; the irreversibility direction (a write can push a bug into a denial that then makes the change unrevertable through this server) recorded in DESIGN.md and the shipped policy's comments; cc.remove given its withheld row (completeness claim now true); "comment on every write tool" corrected (add_cc_to_bug takes none); the stale comment_is_private rationale resolved with the blocking docs finding.

Mutation verification: 8/8 killed, no survivors (each applied singly in a detached worktree, killed by the named test, reverted): keywords-as-set, flat see_also array, empty-string passthrough, guard-skip for new-fields-only payloads, remove-side dropped, cf_ gate weakened, at-least-one-field miscount, comment dropped — plus the manual post-fix mutation above.

Full AGENTS.md gate re-run independently in the isolated worktree: fmt, clippy -D warnings, cargo test --workspace --all-targets --locked (294 tests), cargo deny check, typos — all green. No dependency changes.

Expose the remaining agent-relevant PUT /rest/bug parameters as named
params on update_bug_fields, all under the existing fields capability:
summary, url, whiteboard, version, target_milestone, and add/remove
lists for keywords and see_also (bug URLs). Keywords and see_also
travel as {"add": [..], "remove": [..]} — never the replace-all set
variant, which would let a stale view wipe concurrent additions.

A see_also entry that points at this instance is a bug-id link, so the
tool assesses every local target the way update_bug_dependencies
assesses dependency targets (I8/I14): at least Capability::Summary,
uniform denial (I2), no PUT on refusal. Guard::see_also_local_id — the
parse the read paths already scrub links with — is public now so the
write side reuses it; foreign-tracker entries carry no local id and
pass through unassessed. Without this, a see_also write naming a
policy-denied bug would both record the reciprocal link on the hidden
bug and leak its existence through Bugzilla's success vs "does not
exist" answers.

Semantics stay deliberately narrow: empty strings and empty lists are
ignored (clearing a field is unsupported), one bug per call, and the
cf_ prefix gate (I7) is unchanged — see_also as a custom_fields key
still errors before Bugzilla is contacted. The at-least-one-field
check counts the new params. Free-text values (summary/whiteboard/url)
never enter the server log; the tool-entry trace records presence and
counts only, and the audit params allowlist keeps them (and the
see_also URL lists) at _len while adding keywords_add/keywords_remove
and target_milestone by value — closed instance vocabulary, the same
class as the already-allowlisted keywords and version.

DESIGN.md gains the full update-field audit table — every PUT
parameter either exposed via a named tool/param or withheld with its
reason, cc.remove included — and the matcher-visibility record stated
precisely: a write can never lift a bug out of a deny (a deny grants
nothing), but a restrict rule granting fields while matching on a
field fields can write is self-defeating, and a write CAN push a bug
into a denial, irreversibly through this server. examples/policy.toml
restates the operator warning next to the restrict examples.

Eight wiremock tests pin the wire shapes, the empty-value filtering,
the guard denial on new-fields-only calls, the denied see_also target
(uniform denial, zero PUTs, foreign entries never assessed), the I7
gate, and the all-empty refusal.

Closes #38
@plusky plusky added enhancement New feature or request security Guard, key custody, or disclosure surface labels Aug 2, 2026
@plusky
plusky merged commit a23ae8c into main Aug 2, 2026
10 checks passed
@plusky
plusky deleted the update-fields-38 branch August 2, 2026 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request security Guard, key custody, or disclosure surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

update_bug_fields: no way to set See Also - audit which Bugzilla update fields are worth exposing

1 participant