v2.6.1
v2.6.1 — Read-only and safe mode actually keep reads working
This release fixes a defect that made both protective modes unusable on the default tool surface in v2.6.0, and completes the client-go v2.55.1 update.
⚠️ Fix: read-only and safe mode blocked reads too
Both modes decided policy from the visible tool's ReadOnlyHint. Meta-tool dispatchers and gitlab_execute_action each cover many actions at once, so both modes classified them as mutating and removed or intercepted them whole — taking their read actions down with them.
Measured against a running v2.6.0 server:
| Surface | Safe mode + a read action | Read-only |
|---|---|---|
| individual | executed (correct) | 403 read tools (correct) |
| meta | gitlab_issue action=list → {"status":"blocked"} |
6 of 33 groups; gitlab_issue gone |
| dynamic (default) | project.list → {"status":"blocked"} |
only gitlab_find_action survived |
On the default surface, GITLAB_READ_ONLY=true or GITLAB_SAFE_MODE=true left the server unable to read anything from GitLab — the opposite of what the documentation promised.
Both policies now act per action, applied to the action catalog that the meta and dynamic surfaces are built from:
- Read-only keeps the read-only actions inside each domain and drops the rest, so
gitlab_issuesurvives with its reads andgitlab_execute_actionstays available (annotated read-only) for the reads it can still route. Meta mode now keeps 29 groups instead of 6. - Safe mode replaces each mutating action's handler with a preview naming the canonical action (
issue.createrather than the dispatcher), leaving reads executable. Tools registered outside the catalog — thegitlab_interactive_*utilities — are still intercepted at the tool level. - With both enabled, read-only takes precedence: mutating operations are absent rather than previewable.
If you run with either mode on the meta or dynamic surface, upgrading restores read access. The individual surface was never affected.
client-go v2.55.1
The three SDK releases add no service methods, only fields, all now mirrored 1:1:
custom_attributesfilter onproject.listanduser.list(encoded ascustom_attributes[key]=value), distinct fromwith_custom_attributes, which only controls whether attributes are returned. Also available on the fork and user-scoped project listings.statusonwork_item.create— previously available on update only. Constrained toTODO,IN_PROGRESS,DONE,WONT_DO,DUPLICATE.- Feature flag strategies gain
user_list_idand_destroyon input anduser_liston output, sogitlabUserListstrategies are addressable and an update can remove a strategy.
Clearing assignees and CRM contacts
The SDK dropped its internal guard, so assignee_ids and crm_contact_ids on work_item.update now replace the list: an explicit empty array [] removes every entry, and omitting the field leaves it untouched. The schemas had claimed this before it was true; it now works and says so.
Because that deletes data, clearing is guarded: an empty list that would remove existing entries requires confirm=true or an approved confirmation prompt, following the same precedence as destructive actions. Clearing an already-empty assignee list proceeds silently, and updates that do not touch these fields are unaffected.
Also in this release
- Surfaces evaluator can now evaluate the protective modes:
--server-mode=read-only|safe-mode(SERVER_MODE=on the Docker eval target), which previously only ever scored the unrestricted catalog. - Dependency updates:
gopsutilv4.26.7 and three Go indirects,html-validate11.6.1,playwright1.62.1,@astrojs/starlight0.41.6, andhadolint-actionv3.4.0. - Documentation: a new Read-Only and Safe Mode section in the security guide explaining the per-action model, plus corrections to the Admin tool count and to iteration state guidance that advertised numeric values the schema never accepted.
Changelog
✨ Features
- a13a226: feat(deps): client-go v2.55.1 with new API surface, dependency updates, and 2.6.1 (#264) (@jmrplens)
🐛 Bug Fixes
- 11cbeed: fix(modes)!: apply read-only and safe mode per action so dispatcher surfaces keep serving reads (#263) (@jmrplens)