feat(mcp): let MCP sessions approve and reject issues after the user confirms by vsai12 · Pull Request #21593 · bytebase/bytebase · GitHub
Skip to content

feat(mcp): let MCP sessions approve and reject issues after the user confirms - #21593

Merged
vsai12 merged 24 commits into
mainfrom
feat/mcp-confirmed-approvals
Oct 20, 2026
Merged

vsai12 merged 24 commits into
mainfrom
feat/mcp-confirmed-approvals

Conversation

@vsai12

@vsai12 vsai12 commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

feat(mcp): let MCP sessions approve and reject issues after the user confirms

Part of BYT-9292 user journey 1 (the second of three PRs in plans/2026-10-13-mcp-decision-matrix.md). The first, #21588, is merged.

Today an agent can prepare a change or file an access request, but every approval waits for someone to open the console: ApproveIssue and RejectIssue are refused to MCP in every mode. A refusal does not stop a user who wants their agent to approve, because they can hand it a browser with their own session. So the guard becomes asking the user. The question is asked by the client, not by the model in chat, so an instruction planted in something the agent reads cannot approve anything.

What changes:

  • A new tool, decide_issue(issue, decision, comment?), approves or rejects a database-change issue or an access request. First it asks the user through MCP elicitation, the MCP specification's way for a server to ask the user something. The form shows the issue's name and title, what it changes, the current approval step, the requester and the comment the decision records, with one checkbox. For an access request, what it changes is the targets, schema and container, the query, whether results are unmasked or exportable, and when the grant expires. For a new database, it is the instance and, when set, the cluster, owner, table, character set and collation. The tool acts only when the user ticks the box and accepts. Declining, cancelling or leaving the box unticked changes nothing.
  • The form fits Claude Code's dialog. Read from the Claude Code 2.1.300 binary and confirmed live on 2.1.302: its dialog shows four lines of a form's message, or the first three and "… (+N more lines)", and cuts each line six columns short of the terminal width. So each line does one job, with its bounded part first: the decision and the issue; the targets, counted before they are named; the statement or query; then the approval step, the requester and the comment. An access request's terms (unmasked, export, expiry) open the last line. Its schema and container stay on the targets line: the requester can set them to any length, so only a fixed label comes before the query. They follow a single database's name, so on an 80-column terminal a long name can cut them, and the dialog's "…" shows the cut. With several databases they come after the count and before the names. A form for one change or for an access request fits in four lines. A plan with several changes shows its first change, numbered with the total ("Change 1 of 3"), then Claude Code's "… (+N more lines)", so what the first change does (its statement, its release or its new database's settings) is always within the first three lines. A long list of targets or a long comment is cut at the end of its own line, not the statement: on an 80-column terminal the statement line shows at least 55 characters of SQL.
  • An answer counts only for a form this server sent, about the issue as it still is. The form goes out with a request state: a digest of what the user is asked about, signed with a key derived from the server secret, together with the workspace, user, client, decision and issue. An answer without a state that verifies is treated as no answer, and the user is asked. The digest includes the update times of the issue, the plan and the access grant. So an accepted answer decides nothing, and the agent is told to call again, when anything changed while the form was open: the plan edited (including a setting the form does not show, such as prior backup), a step decided by someone else, the issue rejected and requested again, or the title changed. Today's clients never see the state: the go-sdk carries it between the two rounds of one call. A client on the 2026-07-28 protocol echoes it back.
  • The decision applies only to the issue the user confirmed. The tool sends the issue's update time with the decision. The review workflow compares it with the issue under the issue's row lock, and refuses the decision if it no longer matches. So a step decided between the tool's check and the approval is refused, not decided again at the next step.
  • Issue text cannot add lines to the form. Every value taken from the issue or the agent is quoted with %q, which escapes line breaks. A cut says what it leaves out: "(partial)" before a statement or query that the form cuts at 300 characters, with its full size after it; "3 of N changes shown"; and "statements not shown" for a release. Without "(partial)", the message carries the whole statement, though a client may still cut the line. The form carries no link, because the MCP specification asks servers not to put clickable URLs in a form. The tool's result carries the issue link.
  • A client that cannot show the form gets a refusal with the issue's console link.
  • After the user confirms, the normal approval workflow decides as it does for a person: the approval rule's role, the project's self-approval setting and the last-editor rule. The approval comment records that the decision was confirmed in the MCP client. A refusal comes back in the server's own words, with the issue link.
  • A requester can decide their own access request. A requester may lack bb.accessGrants.get (project developers do), so the tool reads the user's own request through SearchMyAccessGrants, as the console's issue page does.
  • ApproveIssue and RejectIssue move from FORBIDDEN to WRITE, but only decide_issue can use them. The tool attaches the confirmed decision (procedure, issue and the issue's update time) to the context of its internal request. The gate serves the call only when the procedure and issue match. call_api cannot set a context value, so it is refused and pointed to decide_issue.
  • Role requests stay in the console. Approving one grants a project role, which MCP does not do yet. decide_issue serves database changes and access requests only. Any other type is refused with the console link, and the refusal is written to the audit log out of band, as a refused connection is.
  • RetryIssueApproval and ActivateAccessGrant stay FORBIDDEN. The approval-decision reason now reads "it can let a change or an access request pass its approval without an approver deciding it".

Copy: the "Never, in any mode" line drops "approve issues". A new Read-write row, "Approve or reject issues", says each decision is confirmed in the user's MCP client, role requests are decided in the console, and the approval rules and self-approval setting apply as in the console. The propose and request-access rows no longer say an agent never approves. The change tool's guidance points to decide_issue, and the query tool's guidance no longer says a person approves an access grant. The comments behind the issueless-rollout guard no longer argue from approval being FORBIDDEN to MCP. The guard stays, because without a linked issue no approval check runs at all.

Not in this change

  • ApproveIssue and RejectIssue now appear in search_api. A call_api call on either is refused and names decide_issue.
  • A failed or malformed elicitation surfaces as an SDK error that names the input request, not as a tool message. Nothing changes. The database-choice elicitation in tool_resolve.go has the same go-sdk limit.
  • In a Read-only workspace, the user is asked first, and the ceiling then refuses the approval with its own remedy. A pre-check would duplicate the gate's rule.
  • The form is checked against Claude Code only. Its dialog draws the message as plain text, so Markdown and HTML show as written. No second client here can show a form: Cline 4.1.21 declares no elicitation, so its users get the console link. A client that renders the message as Markdown could style issue text inside the form, because %q escapes line breaks, not markup. No escaping works for every renderer and still keeps SQL readable.
  • The form does not show a change's prior-backup setting or a new database's environment. The plan's update time is in the digest, so changing any of them makes an open form's answer stale.
  • An approver who neither filed an access request nor holds bb.accessGrants.get gets no form, because the tool cannot read the grant. The console shows that approver the issue without the grant.
  • A request state has no expiry. An answer acts only while the issue is at the version the form was sent for, and every review action moves that version.

Size

+2490/−185 lines in 36 files: 866 production lines (tool_decide.go is 743), 1,479 test lines, and the rest copy, proto comments and generated code. The classification change, the tool and the call_api refusal have to ship together, so the PR is not split further. The review fixes (1114c34f88, d437f8fc86, 16b6c98c98, a80de2b578, and 1a26f6c287, which reverses the layout b8e462b3c5 tried) add 695 lines, 496 of them tests.

Tests and verification

  • tool_decide_test.go:
    • Accepting approves, with one form whose message is exactly four lines, the attached confirmation carrying the issue's update time, and the comment. The reject path works the same way.
    • Declining, cancelling, an accepted but unticked box, a malformed answer (the SDK's schema error) and an elicitation error all change nothing.
    • A client with no form support, or only URL elicitation, gets the link. An answer sent unasked by a client that cannot confirm is refused.
    • An answer that a form-capable client sends unasked, with no request state or a forged one, is not acted on: the user is asked.
    • If the plan's statement is replaced, the plan is edited where the form does not show it, a step is decided by another approver, the issue is rejected and requested again, or the title changes while the form is open, nothing is decided, and the result says to call again. A decline is still reported as a decline, so the agent does not ask again.
    • A 2026-07-28 client: the first call returns the form and the state. An answer without the state is asked again. The state with a different comment is refused as stale. The state with the same comment decides.
    • The state verifies only for the same workspace, user, client, decision, issue and digest.
    • Issue text cannot add a line: a create-database name with a line break stays inside its quotes, and a statement's line breaks stay visible. Cuts are stated: "(partial)" before a cut statement or query, and each change of a five-change plan carries its number and the total. A failed read of the plan, a statement or the access grant sends no form.
    • The workflow's refusal and the Read-only ceiling's refusal come back in the server's words, with no cause added by the tool.
    • The access-grant summary is pinned line by line. A grant's schema and container come after one database's name and before a list of names, and a long schema, container or database name never reaches the query line; a cut query is marked partial, and the title is quoted.
    • A user's own access request is read without GetAccessGrant, and a search that finds nothing sends no form. A new database's cluster, owner, table, character set and collation are shown.
    • Role requests are refused, both asked as usual and with an answer sent unasked, and so are unknown types. Each refusal writes one audit row.
    • What Claude Code shows is checked at 80 and 120 columns (TestDecideIssueFormShowsTheChangeInClaudeCodesDialog) with a model of its dialog: four lines, or three and "… (+N more lines)"; each line cut six columns short of the width; wide characters take two columns. The cases are one long target, many targets, a long statement, several changes, an access request with a long query, an access request on one long database, with a long schema and with a long container, and wide characters. In each, the visible lines start with the decision, the targets with their count, the start of the statement (marked when cut), and the approval line or how much is left out. On 2026-10-20 the real dialog in Claude Code 2.1.302 matched the model line for line at 80 and 120 columns, on a 12-target change with a cut statement and on a three-change plan with a Chinese title: 16 of 16 lines, including where wide characters are cut.
  • Review workflow: TestReviewIssueAppliesAConfirmedActionOnlyToTheConfirmedVersion approves on the confirmed version, and refuses with the new reason when the issue is edited between the workflow's read and its lock. In backend/api/v1, TestMapReviewErrorSaysWhenAConfirmedIssueChanged pins the refusal's words, and TestConfirmedUpdateTimeComesFromTheMCPConfirmation pins where the version comes from.
  • Gate:
    • TestMCPGateServesAnIssueDecisionOnlyWithItsConfirmation refuses a call with no confirmation, the wrong issue, the other decision, or a Read-only ceiling. Its confirmations carry an update time, as the tool's do.
    • TestCallAPICarriesNoConfirmation shows that call_api attaches nothing.
    • TestConfirmationReachesTheConnectInterceptors sends a marked request through the in-memory transport into a real Connect handler.
    • The FORBIDDEN membership pin and the classification inventory are updated: READ 68, WRITE 39, EXCLUDED 78, FORBIDDEN 34.
  • Live: TestMCPGrantIssuesAndAccessRequests (backend/tests, renamed from TestMCPGateRefusesGrantIssues) shows call_api refused. It then shows the requester's confirmed self-approval refused by the workflow with self-approval off, and the same approval succeeding with it on, with the grant going ACTIVE. That approval passes the update-time check, so the time survives protojson and Postgres exactly. TestMCPDeveloperApprovesTheirOwnAccessRequest has a project developer, who cannot get access grants, file a request and approve it under an approval rule that names developers, with self-approval on. The grant goes ACTIVE. The test fails without the change.
  • Mutations, each failing a test: removing the provenance check, accepting on decline, skipping the elicitation, reading the answer before the capability check, a type check that always serves, reading the answer before the type check, acting on an answer without a verified state, ignoring a changed digest, leaving the comment out of the digest, leaving the identity or the decision out of the signature, printing the create-database name raw, ignoring a failed statement read, dropping the cut note, not writing the refusal's audit row, checking for a change before a decline, leaving the issue's or the plan's update time out of the digest, sending no version with the decision, the workflow ignoring the version or checking it on its unlocked read, the gate comparing the whole confirmation, the refusal's words ignoring the reason, not showing the schema, the old first line, change lines without the count, always reading the grant with GetAccessGrant, accepting an empty search, leaving out a new database's cluster, owner, table or collation, the statement back on the targets line, the count after the names, the schema and container after several names or before the query, no "(partial)" on a statement or a query, the grant's terms on a line of their own, and the comment on a line of its own.
  • Local gates: buf format, buf lint and buf generate, gofmt, go vet, golangci-lint (no findings on the lines this PR changes), the server build, pnpm --dir frontend fix and pnpm --dir frontend test (565 files, 6,228 tests, on the copy commit; the frontend has not changed since), comment share 15% at open (22% over 19 lines in the first review fixes, 18% over 13 lines in a80de2b578; b8e462b3c5 and 1a26f6c287 together add 6 comment lines and no code), and the full Go suite as CI runs it after each review fix, last at a80de2b578: 120 packages ok each time, none failed. At 1114c34f88 the 11 packages the fixes reach ran fresh, among them backend/tests, backend/api/v1, backend/api/mcp and backend/component/review; at 16b6c98c98, backend/tests, backend/api/v1, backend/api/mcp, backend/server and backend/bin/server/cmd ran fresh; at a80de2b578, all 120. The rest came from Go's test cache, their inputs unchanged since the last fresh run. b8e462b3c5 and 1a26f6c287 change one rendering function that only backend/api/mcp uses, so that package ran fresh at each. After main was merged in (fix(api)!: refuse an access grant where just-in-time access is off #21587 now refuses an access grant where just-in-time access is off, so c53249a176 turns it on in one more test), the full suite ran fresh at c53249a176: 121 packages ok, none failed.

Release note

MCP sessions in Read-write mode can approve and reject database changes and access requests with the new decide_issue tool, after the user confirms each decision in their MCP client. Clients that cannot show the confirmation form get a link to the issue instead. The project's approval rules and self-approval setting apply as in the console, and role requests are still decided in the console.

Receipts

Claim Receipt
The confirmation marker cannot be set by a request backend/api/auth/mcp_confirmation.go (unexported context key)
The gate serves a decision only with its matching confirmation refuseUnconfirmedIssueDecision in backend/api/v1/mcp_gate.go
The internal transport carries the request context to the gate backend/api/mcp/tool_http.go (apiRequest, internalRoundTripper); TestConfirmationReachesTheConnectInterceptors
A client can put an answer on a call it was never asked go-sdk v1.7.0 mcp/mrtr.go, serverMultiRoundTripMiddleware: the first pass gets the client's params; the retry gets the elicited answer and the handler's own RequestState
RequestState is the SDK's carrier between rounds, and servers bind elicitation to the user go-sdk v1.7.0 mcp/protocol.go (CallToolResult.RequestState); MCP 2025-11-25 elicitation, Security Considerations 1: "Servers MUST bind elicitation requests to the client and user identity"
A sheet's name pins its statement common.GetProjectResourceIDSheetSha256; the store keys sheets by content hash
No clickable URLs in a form MCP 2025-11-25 elicitation, Safe URL Handling, server rule 3
Claude Code shows four lines of a form's message and cuts each line Claude Code binary, 2.1.300 and 2.1.302 (minified names change per version; these are 2.1.302's): the form dialog passes the message through BP(text, columns, rows) with rows at most H5+1 (H5=3). BP cuts each line with pt at max(20, columns − Yg), where Yg = max(2·bv, 2·(M6+OO)) = 6, and keeps rows−1 lines plus … (+N more lines) when there are more. No key expands it. Live on 2.1.302 (2026-10-18, a stub server): the seven-line layout this PR opened with showed three lines and … (+4 more lines); the four-line layout showed whole, its change line cut with …
Claude Code draws the message as plain text Same binary: the dialog frame renders the message as a dimmed Ink Text, after a sanitizer (Vi in 2.1.300: tabs, control characters, runs of combining marks). There is no Markdown pass. Live on 2.1.302: **bold**, a Markdown link and <b>html</b> in the comment showed as typed, and the statement's line breaks showed as \n
Every approval change moves the issue's update time updateIssuePayload in backend/component/review/workflow.go (review actions, re-requests), used by approval finding (approval.go) and submission; updateIssue in backend/store/issue.go
A plan spec edit moves the plan's update time UpdatePlan in backend/component/review/plan.go
The workflow checks the confirmed version on the locked row ReviewIssue in backend/component/review/workflow.go, right after lockIssue (SELECT … FOR UPDATE)
Project developers lack bb.accessGrants.get ProjectDeveloperRole in backend/store/predefined_roles.go; TestMCPDeveloperApprovesTheirOwnAccessRequest asserts the 403
The console reads a requester's own grant through SearchMyAccessGrants frontend/src/routes/project/issue-detail/components/IssueDetailAccessGrantDetails.tsx
SearchMyAccessGrants returns only the caller's grants SearchMyAccessGrants in backend/api/v1/access_grant_service.go (Creator: &user.Email); custom auth in access_grant_service.proto
A grant and its issue share a creator CreateAccessGrant in backend/api/v1/access_grant_service.go
Approving a role request grants a project role backend/api/v1/issue_hook.go (UpdateProjectPolicyFromRoleGrantIssue)
The workflow's self-approval rule applies backend/component/review/workflow.go (allow_self_approval check)
A refusal that no interceptor sees is written out of band audit.RecordOutOfBandAudit; refuseConnection in backend/api/mcp/connection_audit.go

🤖 Generated with Claude Code

vsai12 and others added 15 commits October 13, 2026 15:47
…t apply

MCP sessions now serve eleven reads that were EXCLUDED as workspace administration: the project and workspace IAM policies, groups, roles, org policies, and a project's access grants. An agent helping a user could not tell who owns a project, see masking exemptions, or work out who would approve a request, although every holder of these permissions reads the same data through the API. RBAC still runs after the MCP gate, so each read stays limited to the user's own permissions, and no response or policy type carries a credential. The Access policy page and consent screen gain a "Read members, roles and policies" row, and both mode summaries name it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reads

The members row now names the policy types it serves (masking rules and exemptions; rollout, data query, tag and review policies) instead of "other policies", because the project's approval rules stay unreadable, and it drops its own permission bound, which the section's shared line already states. The ADMINISTERS_THE_WORKSPACE comment loses its sentence about reads, which nothing reads and which was false for some methods that keep the reason. TestTheApprovalReadsAreServed pins the eleven reads as READ, because the regenerated inventory records a reclassification without refusing one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ApproveIssue and RejectIssue move from FORBIDDEN to WRITE. The MCP gate serves either only when the request context carries the decision the user confirmed in the MCP client, naming the same procedure and issue. call_api cannot set a context value, so it is refused with a way out that names decide_issue. RetryIssueApproval and ActivateAccessGrant stay FORBIDDEN, and the DRIVES_THE_APPROVAL_DECISION wording now describes only them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… confirms

The tool summarizes the issue (what it changes and the step it decides), asks the user through MCP form elicitation in the same tool call, and on a checked accept calls ApproveIssue or RejectIssue with the confirmation on the request context, so the ceiling, RBAC and the approval workflow decide as for a person. Decline, cancel, an unchecked or malformed answer, and a failed elicitation change nothing. A client that declared no form elicitation gets the issue's console link instead. The comment records that the decision was confirmed in the MCP client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The floor no longer says approving issues is never allowed. A new Read-write row, Approve or reject issues, states the bound: each decision is confirmed in the user's MCP client, and the approval rules and the project's self-approval setting apply as in the console. The propose and request rows and the Read-write summary stop promising that only a person approves. The ladder doc's rows, counts and reasoning follow, in all five locales.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Approving a role request grants a project role, and role grants wait with the rest of access administration. decide_issue now serves database changes and access requests only: it reads the issue on both rounds and refuses a role request (ROLE_GRANT_NOT_SUPPORTED) or any other type (ISSUE_TYPE_NOT_SUPPORTED) before a form is sent or a confirmation is attached, with the issue's console link. The new row says role requests are decided in the console, so the floor's 'administer the workspace' stays true.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iring check

ApproveIssue and RejectIssue joined the request-shape table and are not masked-write doors, like CreateIssue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
decide_issue acted on any answer a call carried. A client that declared form
elicitation could put an accepted answer on its first call and approve with no
form shown, and an answer to a form also applied to the issue as it read after
the form closed, so a plan edited or a step decided while the form was open was
approved unseen.

The form now goes out with a request state: a digest of what the user is asked
about (the issue, its approval step, the plan's specs or the access grant, and
the comment), signed with a key derived from the server secret together with
the workspace, user, client, decision and issue. An answer counts only with a
state that verifies; without one the call is asked like a first one. An
accepted answer whose digest no longer matches a fresh read decides nothing
and says to call again. The SDK carries the state between rounds for today's
clients; a 2026-07-28 client echoes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The form gave issue text three treatments: the title quoted, statements
folded to one line, and the rest printed raw. A create-database name is free
text, so a plan could add a line such as "Approval: not required" to the form,
and folding "-- tidy up\nDROP TABLE users;" made the DROP read as part of a
comment.

Every value taken from the issue or the agent now enters through %q, which
escapes line breaks. A cut says what it leaves out: the first 300 characters
of a statement with its full size, "3 of N changes shown", and "statements
not shown" for a release. The form shows the comment the decision records.
It carries no link, because the MCP specification asks servers not to put
clickable URLs in a form. A failed read of the plan, a statement or the
access grant still sends no form, now pinned by a test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
decide_issue turned every refusal of 400 or more into DECISION_REFUSED with
"nothing changed" and a suggestion that the approval rules decide. That
contradicted the Read-only ceiling's own remedy, and "nothing changed" is not
known for a 5xx: ReviewIssue commits before the handler can still fail.

The server's message now passes through with the status and the issue link,
asking the agent to check the issue's current state, as the transport-error
path above it does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two comments argued the issueless-rollout guard from approval being
FORBIDDEN to MCP, which decide_issue ends. The guard stays: without a linked
issue, no approval check runs at all. Its reason now rests on that, stated
once on rejectMCPOriginatedIssuelessRollout; the CreateRollout and
BatchRunTasks comments point to it.

query_database's guidance no longer says a person approves an access grant:
a decision confirmed in the client is not proof that a person decided.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
decide_issue refuses a role request, or a type it does not know, before any
decision request, so no connect interceptor saw the refusal and it was
neither stored nor streamed, unlike its creation-side twin. It is now written
out of band, as a refused connection is.

Also: the decision is held once, as its procedure; RequestIssue's comment is
cut to its constraint; comments that restated the gate's provenance rule are
gone; and the live test is renamed for the decision it now covers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
decide_issue reports a decline before it checks whether the issue changed
while the form was open, so an agent is told not to retry instead of being
sent back to a user who said no. No test held that order; one does now.

Also: RecordOutOfBandAudit's comment no longer counts its callers, and the
CreateRollout comment drops a sentence that named a PR its earlier lines no
longer introduce.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It cited "the rule the gate PR owes", a PR the CreateRollout comment no
longer introduces; it now names rejectMCPOriginatedIssuelessRollout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-20T07:39:23.091654Z 82186dd New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6f0e899c70

ℹ️ 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".

Comment thread backend/api/mcp/tool_decide.go
Comment thread backend/api/mcp/tool_decide.go
Comment thread backend/api/mcp/tool_decide.go
Comment thread backend/api/mcp/tool_decide.go
Review on #21593 found four gaps in how a confirmation binds to what it
decides, and Claude Code's form dialog turned out to show at most four
lines of a form's message.

- The digest includes the update times of the issue, the plan and the
  access grant, so a plan edit the form does not show (prior backup)
  and a rejection followed by a new request both make an answer stale.
- The confirmed decision carries the issue's update time, and the
  review workflow refuses it under the issue's row lock when it no
  longer matches. This closes the gap between the tool's check and the
  approval.
- An access request shows its schema and container.
- The form puts the decision, the issue name and the change first, and
  an issue with one change fits in four lines. Each cut note comes
  before the text it cuts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1114c34f88

ℹ️ 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".

Comment thread backend/api/mcp/tool_decide.go
@vsai12
vsai12 requested a review from ecmadao October 15, 2026 06:45
vsai12 and others added 2 commits October 14, 2026 23:48
The count of a plan's changes came after the first three, so in Claude
Code's four-line dialog a plan with three or more changes showed two of
them and no count. Each change line of a plan with several now starts
"Change 2 of 5", so the first visible line already says how many there
are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A project developer cannot get access grants, so decide_issue failed
before asking when an approval rule names developers and the project
allows self-approval, though the same user can approve in the console.
The caller's own request is now read through SearchMyAccessGrants, as
the console's issue page reads it. TestMCPDeveloperApprovesTheirOwnAccessRequest
fails without the change.

A new database's ClickHouse cluster and PostgreSQL owner change what
its statement does, so the form now shows them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 16b6c98c98

ℹ️ 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".

Comment thread backend/api/mcp/tool_decide.go Outdated
Comment thread backend/api/mcp/tool_decide.go Outdated
Claude Code cuts each line of a form six columns short of the terminal
width, so a change on several targets, or on one long name, pushed the
statement and its cut note out of view. Each line now does one job: the
decision; the targets, counted before they are named; the statement; then
the approval step, requester and comment. An access request's schema and
container come before its databases, and its terms open the last line, so
a request with a comment fits four lines too. A statement or query cut by
the form is marked (partial) before it, with its full size after it. A new
database's table, character set and collation are shown with its cluster
and owner.

A test checks what Claude Code shows at 80 and 120 columns: one long
target, many targets, a long statement, several changes, an access request
and wide characters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from feat/mcp-approval-reads to main October 19, 2026 00:33
#21588 landed as a squash. Keeps this branch's wording (approve in the
Read-write summary, the Approve or reject row, the approval-reason
comment) and takes main's GetReviewRun count: READ 68, WRITE 39,
EXCLUDED 78, FORBIDDEN 34, total 219.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow CI / lint-protos (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed⏩ skippedOct 20, 2026, 7:50 AM

@vsai12
vsai12 requested a review from ecmadao October 19, 2026 03:03
vsai12 and others added 2 commits October 19, 2026 22:40
On one database with a long name, the schema and container came after the
name on the scope line, and Claude Code's dialog cut them off. They decide
what the query's unqualified names read, so they now open the query line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
b8e462b moved the schema and container in front of the query. The
requester can set them to any length, so a long one pushed the whole query
out of Claude Code's dialog. They go back on the scope line, after a single
database's name, and the query line keeps only its label before the SQL.
Tests pin long schema and container values and a long database name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1a26f6c287

ℹ️ 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".

Comment thread backend/api/mcp/tool_decide.go
vsai12 and others added 2 commits October 20, 2026 00:35
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uest

#21587 refuses an access grant in a project where just-in-time access is
off, so the developer's request in this test now needs it turned on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vsai12
vsai12 merged commit bddc8ca into main Oct 20, 2026
13 of 15 checks passed
@vsai12
vsai12 deleted the feat/mcp-confirmed-approvals branch October 20, 2026 08:22
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.

3 participants