Skip to content

fix(notifications): refuse a level the update scope does not accept - #852

Merged
jmrplens merged 2 commits into
plan-l16from
plan-l17
Sep 20, 2026
Merged

jmrplens merged 2 commits into
plan-l16from
plan-l17

Conversation

@jmrplens

@jmrplens jmrplens commented Sep 20, 2026

Copy link
Copy Markdown
Owner

One six-value enum was published on all three update actions, which put global on notification_global_update. client-go refuses that value in UpdateGlobalSettings before it builds a request, so a model picking it off the published list was guaranteed an error, and the 400 hint the same handler returned offered it the very list it had just failed with. GitLab itself imposes no such restriction, so the narrowing is the SDK's, but it sits upstream of the wire and nothing here gets past it.

An unrecognized level was worse than wrong. buildUpdateOpts dropped it, the PUT went out carrying everything else and no level, GitLab kept the level it already had and answered 200, and the handler rendered that unchanged level as a successful update. Nothing in the answer distinguished it from the change the caller asked for. The enum did not close this either: the default dynamic surface and the opaque meta surface validate parameter names and requiredness and nothing else, so only the individual surface ever refuses a value on the enum's account.

Each scope now carries its own level list, the account-wide one derived from the scoped one so the inherit level stays the only difference between them, and each update handler passes its own to buildUpdateOpts, which refuses anything outside it. The same list renders the enum, the parameter description and the 400 hint. The shared eventFields tag names no values, since one embed cannot say two things, and a test holds every update action's enum, description and Usage sentence to its own list.

(cherry picked from commit 2b3a33f2d7a632f9784a38d200620e0012372b1b)

One consequence surfaced by make check-meta-descriptions once this layer stood on its own: gitlab_user's Notifications block still offered global as a level for notification_global_update, which this very change makes the action refuse, since global means "inherit the setting one scope up" and the account has no scope above it. The curated prose in internal/tools/testdata/tools_meta.json is corrected here, the usage line renders the level list per scope so the two cannot drift again, and the audit reads the served surface for it.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @jmrplens, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 10 hours and 47 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@coderabbitai

coderabbitai Bot commented Sep 20, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 11 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: jmrplens/gitlab-mcp-server/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: d92059ce-8faf-431e-a05f-38c7371a01fc

📥 Commits

Reviewing files that changed from the base of the PR and between dd5303a and 8f50fc7.

📒 Files selected for processing (9)
  • cmd/audit_meta_descriptions/main.go
  • cmd/audit_meta_descriptions/main_test.go
  • internal/tools/notifications/action_specs.go
  • internal/tools/notifications/action_specs_test.go
  • internal/tools/notifications/notifications.go
  • internal/tools/notifications/notifications_test.go
  • internal/tools/testdata/tools_meta.json
  • internal/tools/testdata/tools_meta_compact.json
  • internal/tools/testdata/tools_meta_full.json

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jmrplens
jmrplens added this pull request to stack #863 September 20, 2026 10:16
@github-actions github-actions Bot added the bug Something isn't working label Sep 20, 2026
@jmrplens jmrplens added this to the 3.1.0 milestone Sep 20, 2026
@github-actions github-actions Bot added the v3.1.0 Targeted at the 3.1.0 release label Sep 20, 2026
@jmrplens
jmrplens force-pushed the plan-l17 branch 2 times, most recently from 696b22e to add2d27 Compare September 20, 2026 14:24
@github-actions github-actions Bot added the tooling The audit and generator commands under cmd/, and the Makefile targets that run them label Sep 20, 2026
@jmrplens
jmrplens removed this pull request from stack #863 September 20, 2026 19:40
@jmrplens
jmrplens added this pull request to stack #866 September 20, 2026 19:40
One six-value enum was published on all three update actions, which put
`global` on notification_global_update. client-go refuses that value in
UpdateGlobalSettings before it builds a request, so a model picking it
off the published list was guaranteed an error, and the 400 hint the
same handler returned offered it the very list it had just failed with.
GitLab itself imposes no such restriction, so the narrowing is the
SDK's, but it sits upstream of the wire and nothing here gets past it.

An unrecognized level was worse than wrong. buildUpdateOpts dropped it,
the PUT went out carrying everything else and no level, GitLab kept the
level it already had and answered 200, and the handler rendered that
unchanged level as a successful update. Nothing in the answer
distinguished it from the change the caller asked for. The enum did not
close this either: the default dynamic surface and the opaque meta
surface validate parameter names and requiredness and nothing else, so
only the individual surface ever refuses a value on the enum's account.

Each scope now carries its own level list, the account-wide one derived
from the scoped one so the inherit level stays the only difference
between them, and each update handler passes its own to buildUpdateOpts,
which refuses anything outside it. The same list renders the enum, the
parameter description and the 400 hint. The shared eventFields tag names
no values, since one embed cannot say two things, and a test holds every
update action's enum, description and Usage sentence to its own list.

(cherry picked from commit 2b3a33f2d7a632f9784a38d200620e0012372b1b)
`make check-meta-descriptions` failed on one line. gitlab_user's
Notifications block offered `global` as a value of `level` for
`notification_global_update`, and that action publishes one level fewer
than its project and group siblings: `global` means "inherit the setting
one scope up", the account has no scope above it, and client-go refuses
`GlobalNotificationLevel` there before it builds a request. The enum and
the parameter description were already right; the prose a model reads on
the default surfaces was not.

Nothing generates that line. A meta group's description is curated text
embedded from `internal/tools/testdata/tools_meta.json`, which
`catalogGroupDescription` reads and the snapshot regenerator writes back
from, so the round trip cannot notice one going stale. That is what
`cmd/audit_meta_descriptions` exists to be the third party for, and this
is it catching one. The line is corrected where it lives, in the
embedded snapshot and in the two mode goldens carrying the same
description.

The Usage sentence was the third copy of that list and the one nothing
compares: `StripMetaToolDescriptionPrefix` removes the "Action guidance"
block before the audit reads a description, so a level spelled there
that the action's enum refuses would reach a model unchallenged. It is
rendered now by `updateUsage` from the same per-scope list the enum and
the parameter description already come from, which reproduces today's
three sentences byte for byte and cannot disagree with them tomorrow.

`auditServedSurface` is split out of `run` so a test can hold one rule
over the real surface without reassembling the setup.
`TestAuditServedSurface_OffersNoValueTheActionItNamesRejects` reads the
accepted values back from each action's own schema rather than listing
them, and names the action and the value when a line offers one the
schema rejects. It fails on the base commit with exactly this finding.

The audit reads 1720 description lines, refuses none, and this was the
only one on the surface that disagreed: every other action carrying a
per-action enum override is clean, as is `(enabled, disabled, or
dry_run)` in `runnercontrollers`, the only other Usage sentence that
spells a value set.

Two generated artifacts move with the served text and are left for the
stack to refresh at its tip: `llms-full.txt` and
`llms-full-meta-tools.txt`, seven bytes each, and
`docs/development/token-footprint.md`.
@sonarqubecloud

Copy link
Copy Markdown

@jmrplens
jmrplens merged commit 549c49f into main Sep 20, 2026
37 checks passed
@jmrplens
jmrplens deleted the plan-l17 branch September 20, 2026 21:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working tooling The audit and generator commands under cmd/, and the Makefile targets that run them v3.1.0 Targeted at the 3.1.0 release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant