Repository navigation
feat(mcp): let MCP sessions turn just-in-time access on or off, and revoke access grants - #21626
Conversation
The form, the signed request state, the answer check and its outcome move to tool_confirm.go and take any write that names its procedure and resource, so the next confirmed tools reuse them. decide_issue's tests pass unchanged. The signing purpose loses its tool name; a request state signed before an upgrade no longer verifies, so that call is asked again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MCPConfirmedDecision.Issue becomes Resource, and the gate's refusal takes the procedure, a way to read the resource from the request, the reason and the way out, so the next confirmed writes reuse it. ApproveIssue and RejectIssue keep their wording. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RevokeAccessGrant moves from EXCLUDED to WRITE, so an agent that requested access can revoke the grant when the work is done. It removes access, so it needs no confirmation; bb.accessGrants.revoke still applies, and the handler refuses anything but an active grant. The ladder doc, the Access policy row and the Read-write summary name it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the user confirms A new tool, set_just_in_time_access, writes one project's allow_just_in_time_access through UpdateProject after the user confirms in an MCP form. The form names the project and what the change does to active grants. The confirmation's digest is the exact update, so an answer resent with other arguments is stale. UpdateProject moves to WRITE, and the gate serves it only with the confirmation the tool attaches, so call_api cannot change a project. Both directions go through the form: turning it off is a tightening, but serving it apart would take a second path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new WRITE method reaches every Read-write session at once, including workspaces from before the MCP setting, which have no row. Only the store's default said so; this test removes the row and makes a write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iscovery set_just_in_time_access parses its arguments the way decide_issue does, so the parse error reaches the result (nilerr). UpdateProject is now WRITE, so search_api lists it, as it lists ApproveIssue; call_api meets the gate's denial naming set_just_in_time_access. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oors The wiring test counted three non-door entries; UpdateProject makes a fourth. Naming them keeps a new entry from slipping past the count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A signed request state carried no time, and a just-in-time write's digest repeats for the same project and value, so an accepted answer could be replayed whenever the setting flipped back. The state now carries when the user was asked, under the MAC, and an answer older than ten minutes is reported stale rather than asked again: an older client gets one second round per call. decide_issue gets the same bound. The client-capability check and the signed-answer check move into the helper, so each tool keeps only its own wording. set_just_in_time_access checks the client after its already-in-state answer, so a client that cannot show a form is told nothing needs changing. New tests: a refused update is reported in the server's words, an unasked answer from a client without forms is refused, an expired confirmation decides nothing, and the form's first line shows a 63-character project ID whole at 120 columns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fault in the store The wiring test now checks that every refusal-table entry is a door it drives or one it names, not only their count. The no-saved-setting default moves from a booted server to the store, where it lives; the gate tests already cover Read-write serving a WRITE method. The confirmation's type doc keeps one word for what the user confirms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
The latest Buf updates on your PR. Results from workflow CI / lint-protos (pull_request). |
The question line put a long fixed prefix before the project, so at 80 columns production-analytics-platform-west and -east looked the same. The ID now has a line of its own, "Project: <id>"; a resource ID is at most 63 characters, so it always fits the 74 columns Claude Code shows. The dialog test checks the longest ID whole and two IDs that differ only at the end at both widths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f676636275
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Only UpdateProject is served through set_just_in_time_access; CreateAccessGrant and RevokeAccessGrant are ordinary WRITE methods. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

feat(mcp): let MCP sessions turn just-in-time access on or off, and revoke access grants
Part of BYT-9292 user journey 1, the last of the three PRs in
plans/2026-10-13-mcp-decision-matrix.md(rows 9 and 10), split in two: this one, then masking exemptions. #21588 and #21593 are merged.An agent can now file an access request and, after the user confirms, approve it. Two dead ends remain on that path. In a project with just-in-time access off, the request is refused, and only the console can turn the setting on. And once the work is done, the agent cannot revoke the grant it was given. Both close for users who hold the permissions: admins, DBAs and project owners hold
bb.projects.updateandbb.accessGrants.revoke; a developer's or member's agent still can do neither.What changes:
A new tool,
set_just_in_time_access(project, enabled), turns one project's just-in-time access on or off after the user confirms in an MCP form, the same elicitationdecide_issueuses. The form has three lines: the change and who makes it, the project ID on a line of its own (Project: "hr"), and what the change does:Both are what the code does: with the setting off,
preCheckAccessignores grants for queries and exports, andCreateAccessGrantrefuses new requests. A project already in the requested state is reported as unchanged, without asking.The confirmation covers the exact write. The form's request state carries a digest of the
UpdateProjectrequest the tool will send, signed with the workspace, user, client, procedure and project. A client resends the call with its answer, so an answer that comes back with other arguments is stale, and nothing changes.UpdateProjectmoves from EXCLUDED to WRITE, but onlyset_just_in_time_accesscan use it. The tool attaches the confirmation to its internal request, and the gate serves the call only when the procedure and project match.call_apicannot set a context value, so it is refused, pointed to the tool for this setting and to the console for every other.Both directions go through the form. Turning just-in-time access off is a tightening, which the decision matrix would serve without a prompt. Serving it apart would take a second path through
call_apifor one value of one field.RevokeAccessGrantmoves from EXCLUDED to WRITE. It removes access, so it needs no confirmation.bb.accessGrants.revokestill applies, and the handler refuses anything but an active grant.A confirmation expires after ten minutes. The signed request state now carries when the user was asked, under the MAC. A just-in-time write's digest repeats for the same project and value, so without a bound an accepted answer could be replayed whenever the setting flipped back. An expired answer is reported stale, not asked again, because an older client gets one second round per call.
decide_issuegets the same bound.decide_issue's round trip moves into a shared helper (tool_confirm.go): the client-capability check, the form, the signed request state, the answer check and its outcome. Each tool keeps its own wording.decide_issue's tests pass unchanged on the commit that moves it (a6108db7d8). The confirmed value'sIssuefield becomesResource, and one gate refusal servesApproveIssue,RejectIssueandUpdateProject.Copy: the Access policy page's "Request access grants" row adds "Revoke an active grant" and "Turn a project's "Allow access grants" on or off, after the user confirms". The Read-write summary reads "request and revoke access". Five locales.
Not in this change
UpdateProjectwrites the project's whole settings object, so another settings change that lands in the same moment can be lost. The console has the same race; BOT-153 tracks fixing it once for every writer.UpdateProjectnow appears insearch_api, asApproveIssuedoes since feat(mcp): let MCP sessions approve and reject issues after the user confirms #21593. Acall_apicall on it is refused and names the tool.UpdateProjectalready means confirmed.Size
+1012/−211 lines in 27 files: production Go +467/−153, of which about 100 lines move from
tool_decide.gototool_confirm.go(198 lines;tool_jit.gois 194); tests +514/−27; the rest is copy in five locales, the ladder doc, two proto annotations and generated code. Masking exemptions are split into the next PR to keep this one reviewable.Tests and verification
set_just_in_time_access: turns it on with the exact update and the confirmation the gate admits; says what turning it off does; asks nothing when already set; reports a setting changed while asking as unchanged; changes nothing on decline, cancel or an unticked box; refuses a client that cannot show the form, with the settings link; an answer resent with the other value is stale, and one for another project is asked again; refuses an invalid project; reports a refused update in the server's words; tells a client without forms that nothing needs changing when the setting is already set, and refuses its unasked answer; the form at 80 and 120 columns in the model of Claude Code's dialog: the longest project ID (63 characters) shows whole, and two IDs that differ only at the end (production-analytics-platform-westand-east) stay distinguishable.decide_issueanswer signed eleven minutes earlier decides nothing; the signed state rejects another digest, another time or a missing signature.UpdateProjectrefused without the confirmation, served with it, refused for another project or another procedure, and still held to the ceiling.RevokeAccessGrantandUpdateProjectpinned as WRITE.TestMCPGrantIssuesAndAccessRequests: after the just-in-time refusal,call_apiUpdateProjectis refused, the session turns the setting on through the tool, files the request, has it approved throughdecide_issue, then revokes the grant throughcall_api.TestMCPSettingsWithNoRowAreReadWrite). Read-write serving a WRITE method, Read-only, Disabled and a new workspace's Read-only were already pinned.buf format,buf lintandbuf generate;gofmt;go vet; golangci-lint (no findings in the files this PR changes); the server build;pnpm --dir frontend fixandpnpm --dir frontend test(582 files, 6,595 tests; the frontend has not changed since); comment share 16%; and the full Go suite as CI runs it atba5d0e2c0d: 121 packages ok, none failed.6d672ac3d8andba5d0e2c0d, one declined (pre-checking archived or default projects, which would copy the server's refusals into the tool); the recheck of the fix delta cleared.Release note
MCP sessions in Read-write mode can turn a project's just-in-time access on or off with the new
set_just_in_time_accesstool, after the user confirms the change in their MCP client, and can revoke an active access grant. Bytebase permissions still apply.Receipts
backend/api/v1/sql_service.go:215(preCheckAccess), called at:309(Query) and:1183(Export)backend/api/v1/access_grant_service.go:158UpdateProjectwrites the whole settings objectbackend/api/v1/project_service.go:296,backend/store/project.go:301backend/api/mcp/tool_jit.go(setJustInTimeAccessOnAnswer),tool_decide.go;call_apipasses the session's own context,tool_call.go:74🤖 Generated with Claude Code