feat(mcp): classify every v1 RPC and lint the classification complete (1b-1) - #21180
Open
vsai12 wants to merge 9 commits into
Open
feat(mcp): classify every v1 RPC and lint the classification complete (1b-1)#21180vsai12 wants to merge 9 commits into
vsai12 wants to merge 9 commits into
Conversation
… (1b-1) Every v1 RPC now carries mcp_method_class. 183 methods were unannotated; the 25 that already carried FORBIDDEN are unchanged. READ 48 / WRITE 40 / EXCLUDED 91 / FORBIDDEN 29. Only FORBIDDEN is enforced. READ and WRITE are inert until the gate ships, so the annotation states where a method belongs, not where the boundary sits. The enum comment says so. Two additions to the vocabulary: - EXCLUDED, for methods no phase-1 mode serves. Workspace administration, plus the handful that do something materially worse than the plain permission they share. It is not FORBIDDEN because it is reversible: an admin-capable ceiling could serve these, and none of them breaks the MCP boundary itself. - mcp_exclusion_reason, required on every EXCLUDED row, so an exclusion nobody wrote down cannot happen. Four methods are newly FORBIDDEN and therefore newly denied on merge: ApproveIssue, RejectIssue, RequestIssue and RetryIssueApproval, under the new DRIVES_THE_APPROVAL_DECISION reason. An agent may compose a change and may execute an approved one; it does not approve its own work. The lint asserts, over the compiled descriptors: every RPC carries a class and UNSPECIFIED fails the build; a FORBIDDEN method carries a reason and every reason in use has wording; an EXCLUDED method carries a reason; and no denied class is reachable through any serving mode. Each clause is a function over rows with a RED test that mutates one input and asserts it fires. testdata/mcp_method_classification.md renders the annotations for review. The annotations are the source of truth; regenerate the file with MCP_INVENTORY=write go test ./backend/api/v1/ -run TestMCPClassificationInventory
…r rows Pre-review findings. The third lint clause could not fail. It held every FORBIDDEN and EXCLUDED row against a serving table that lists only READ and WRITE, so the contradiction it looked for was one a single-valued annotation already makes impossible — it lint ed the test's own constant. Replaced with the check that can drift: read both enums out of the compiled descriptors and assert every class is served by some mode or denied, never both and never neither, that every ceiling mode says what it serves, and that no row carries a class the tables skipped. Add a fifth class or a fourth mode and CI fails until somebody decides who serves it. Four RED tests, one per way it can break. CreateProject moves to EXCLUDED. Every other project-lifecycle RPC is EXCLUDED under a reason whose own text says "project lifecycle", and create persists the same governance switches update is excluded for — allow_self_approval, require_issue_approval — so a read-write session could have built itself a project with the review gate off. It also could not read back what it created. GetSubscription moves to EXCLUDED, matching the rest of billing. Licence tier is still readable through GetActuatorInfo. Query stays READ and CreateIssue stays WRITE, both with the debt written where the gate PR will look. Query is decided by its argument, not by the method: it authorizes DML and DDL per statement on every engine with the new ACL, so the serving PR owes a statement clamp. CreateIssue's ROLE_GRANT and ACCESS_GRANT types complete on creation and write a project IAM binding, which a class annotation cannot split off, so that PR owes a type carve-out. The approval rationale claimed containment it does not buy. UpdatePlan and UpdateIssue reset approval findings and stay WRITE, deliberately — editing a proposal is the agent's job — and a plan created without an issue reaches execution with no approval at all. The reason now claims only what holds: an agent never casts the vote on its own change. Also: the MCP change tool told the agent its next action was APPROVE_ISSUE, a call this series now refuses; it says AWAIT_HUMAN_APPROVAL. EXCLUDED joins the interceptor's dispatch test, since 91 methods must keep being served. The interceptor doc no longer describes a rollout this PR finished. Two RED tests for the cross-reason branches, and the wording check that TestForbiddenClass Membership duplicated is gone.
…he lint Recheck of the fix delta. Narrowing DRIVES_THE_APPROVAL_DECISION to "records the review decision" cut the half that covered RetryIssueApproval, which records nothing — and that sentence is the denial the agent is shown, so the refusal would have stated a mechanism the file's own comment denies fifteen lines later. The reason now says what all four do: works the approval step that gates the change, by recording the decision or by re-running the finding that sets it. RetryIssueApproval belongs under the second half — on an auto-approved result the same call activates the grant and enqueues the rollout, so it moves an issue through the gate with no human acting. The serving-decision lint's last loop was dead: `decided` was set for every member of the enum, including one the tables had just failed to decide, so it could only fire on a class value no descriptor can produce. It now holds the classes that came out with exactly one answer, which makes dropping a class from the serving table report every method annotated with it rather than the class alone — the shape of the damage. Its RED test asserts that. Vocabulary membership moved to its own set so a doubly-decided class does not also get told it is not a class. The CreateIssue debt comment named the ROLE_GRANT mechanism for both types. ACCESS_GRANT writes no IAM binding; it sets the grant ACTIVE, which is what AccessGrantService/ActivateAccessGrant is excluded for. Both are named now. The removed duplicate wording check left its comment dangling at the end of a function body; it sits on the assertion it describes.
Recheck round two. Making `decided` an exclusive-or repaired the dead loop and broke the other branch: a class both served and denied came out undecided, so seeding FORBIDDEN into READ_WRITE produced 29 lines telling you AuthService/Login has no serving decision — which is false, and which sorts above the one line that names the real problem. It is an inclusive-or; a class with two decisions is reported once, at class level. The RED test for that branch now passes the real 208 rows, which is what let it through. Also sort the mode list in that message: `serving` is a map, so the violation text was nondeterministic across runs.
…s the message stable The inclusive-or fix left its own doc comment describing the exclusive-or it replaced, which documented the defect as the behavior. It now says `decided` holds the classes some decision claims, both included. The sort added so the mode list prints deterministically had no test that could see it: the one case reaching that message widened a single mode, so the slice had one element. It widens two now, and the expected string is only stable because of the sort. Also: the inline comment pointed at "the class-level loop above" from inside that loop, and the reflowed paragraph had picked up a second short line.
vsai12
requested review from
a team,
RainbowDashy,
d-bytebase and
ecmadao
as code owners
August 14, 2026 21:15
|
The latest Buf updates on your PR. Results from workflow Proto linter / lint-protos (pull_request).
|
…ad it
Codex raised two approval gaps on the review. Both mechanisms are real; both are
pre-existing and human-reachable, and neither is a reason to reclassify. What was
missing is that the debt was recorded in the PR body and under the FORBIDDEN
approval reason, not on the rollout annotations themselves — so a reader of
rollout_service.proto asking why CreateRollout is WRITE saw nothing. The PR
already set that precedent for Query and CreateIssue; this applies it here.
Gap one: both approval checks on the rollout path are guarded on a linked issue
existing, in the handler and again in the locked re-check, so an issueless plan
reaches execution regardless of require_issue_approval. Not an MCP hole — the
shipped GitOps Service Agent role holds no issue permission at all and can only
take that path, and the console says so outright ("the project settings are
client-side gates", bypassDeploy.tsx).
Gap two: an approval binds the plan's specs and its approval_input_version, not
the databases a group target resolves to. CreateRollout re-resolves the group
live, and nothing in UpdateDatabaseGroup, UpdateDatabase or schema sync bumps
that version, so databases joining the group after approval execute under it.
The deployment snapshot used to be that binding and was removed deliberately in
#18589, so restoring it is a product decision, not a classification one.
CreatePlan gets a pointer as the step that produces the issueless plan.
Also: say plainly that the gate PR must lift mcpServingModes rather than declare
a second copy — two copies would let this lint stay green while the runtime
serving rules drift, which is the one thing a specification-shaped test cannot
catch about itself.
…er MCP can fix it The three artifacts stated three contracts: the annotation said 1b-2 "owes a carve-out", the PR body called it 1b-2's decision to take, and BOT-71/72 framed both as product-wide. Codex is right that a security-critical gate PR cannot be handed that. The ambiguity was mine and it was sharper than stated: the two gaps get OPPOSITE answers, because only one is fixable at the MCP layer, and one comment covered both. Issueless rollout — the gate PR OWES A RULE, now stated exactly: on the MCP path, refuse CreateRollout and BatchRunTasks when the project sets require_issue_approval and the plan has no linked approved issue. The justification changed too. "Human-reachable and pre-existing" does not dismiss an MCP restriction — this PR excludes eight human-reachable rows — so the real reason WRITE stays is that issueless deployment is a supported product contract (the GitOps Service Agent role holds no issue permission at all) and READ_WRITE preserves supported contracts. The rule is owed anyway, because without it the four FORBIDDEN approval methods are decorative for the change lane: an agent that cannot clear the gate skips it instead. Group drift — the gate PR owes NOTHING, and must not pretend otherwise. No MCP rule closes it: approval enqueues rollout creation on the auto path with no CreateRollout call, and schema sync enlarges the set with no API call. Only an approval-time digest of the resolved set works, and live expansion was deliberate (#18589), so it is a product decision. BOT-72 alone. CreatePlan needs no rule of its own — a plan never rolled out changes nothing. Dropped the bypassDeploy.tsx quote as load-bearing evidence: the backend does enforce approval for linked issues, including the transactional recheck, so "only enforces bb.rollouts.create" overstates it. The GitOps role is the evidence.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 46661dc640
ℹ️ 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".
Codex is right about the mechanism, and my comment stated the opposite of it. ApproveIssue and RejectIssue record the decision — canReview for an approver role, the self-approval guard, an APPROVED/REJECTED approver appended. RequestIssue is structurally the reverse: it requires the issue to be ALREADY rejected, requires the actor to be the CREATOR, never calls canReview, and records no decision — it strips the REJECTED approvers and returns the issue to PENDING for a fresh human decision. So it approves nothing, and forbidding it protects nothing the other two do not. The cost is concrete: while a rejection stands both Approve and Reject hard-fail, so this is the only exit from the rejected state, and an agent that fixes what a human asked it to fix cannot resubmit. Grouping it with approve/reject was the error the file itself warns about — a denial whose stated reason has drifted from the mechanism. The comment now says what each action does, and marks RequestIssue as the row to widen. The classification is unchanged pending the lead's call, since the four-method set is a spec-level decision.
… exit from rejected Spec §1b-1 named four approval methods as FORBIDDEN. That line grouped by RPC family; the mechanism does not agree, and the mechanism is what the reason annotation is required to describe. ApproveIssue and RejectIssue record the review decision: applyReviewAction requires an approver role via canReview, enforces the self-approval guard, and appends an APPROVED or REJECTED approver. RequestIssue is the reverse — it requires the issue to be already rejected, requires the actor to be the CREATOR, never calls canReview, and records no decision. It strips the REJECTED approvers and returns the issue to PENDING for a fresh human decision. So forbidding it protected nothing the other two do not already protect, and it cost the loop propose_database_change exists for: while a rejection stands both Approve and Reject hard-fail, so this is the only exit from that state. An agent that fixed the SQL a human asked it to fix could not resubmit — the human had to re-request from the console, or the agent had to abandon the issue and create a replacement. The self-approval guard is untouched. A human approval is still required, and ApproveIssue, RejectIssue and RetryIssueApproval stay FORBIDDEN; the last of those can auto-complete a grant and enqueue a rollout with no human acting, which this one cannot. FORBIDDEN 29 -> 28, WRITE 39 -> 40. Found by the Codex review; Vincent took the call.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Every v1 RPC now carries
mcp_method_class, and CI fails on one that does not. That last clause is the point of the PR: five rounds of this series have found MCP-reachable methods one batch at a time, and a new RPC could always arrive unclassified. It cannot now.Only FORBIDDEN is enforced. READ, WRITE and EXCLUDED are recorded classifications with no request-time reader — the gate that selects between them is 1b-2. The enum comment says so, the interceptor comment says so, and the interceptor's dispatch test pins that all three still dispatch. The exception is FORBIDDEN, which the existing interceptor enforces the moment it is annotated; see the behavior change below.
The classification
backend/api/v1/testdata/mcp_method_classification.mdrenders it row by row. It is a rendering, not the source of truth — the annotations are — and a stale file fails CI. Regenerate with:Seeded from the closed #21141's inventory, re-verified against current main handler by handler. Its
ai.op.*identifiers and capability universe are not revived.Admin-class exclusion is a fourth class, not FORBIDDEN
EXCLUDEDis new. It means no phase-1 mode serves the method, and it is deliberately not FORBIDDEN, because the two differ in whether they can ever be undone. An admin-capable ceiling — if one is ever built — could legitimately serve an EXCLUDED method; that is a product decision nobody has made. It could never serve a FORBIDDEN one, because FORBIDDEN names a mechanism that breaks the MCP boundary itself. Folding them together would put the ordinary admin API into the same durable-never set as credential minting, and any future widening would have to re-litigate both at once.mcp_exclusion_reasonis required on every EXCLUDED row, so an exclusion nobody wrote down cannot happen:ADMINISTERS_THE_WORKSPACERETURNS_A_STORED_SECRETREADS_OTHER_USERS_SQLSENDS_DATA_TO_A_THIRD_PARTYOPENS_AN_ADMIN_CONNECTIONCounterargument, pre-written: a fourth value is one more thing the gate has to switch on, and today EXCLUDED and FORBIDDEN behave identically — nothing serves either. YAGNI says use FORBIDDEN and split later. Against that: they do not behave identically now — FORBIDDEN denies at the interceptor and EXCLUDED dispatches, which the dispatch test pins — and every FORBIDDEN row carries a reason describing a credential or boundary mechanism. Ninety-three admin rows have no such mechanism, so folding them in would mean either inventing a false reason or leaving them reasonless, which the lint refuses.
The conservative exclusions
Method granularity means excluding these costs nothing elsewhere, so where a method is dangerous behind an innocuous permission it is out.
RolloutService/GetTaskRunSession—OPENS_AN_ADMIN_CONNECTION. Sharesbb.taskRuns.listwith four store-only reads, but alone opens an admin-credentialed connection to the customer database and returns other sessions' in-flight, unmasked SQL.SQLService/AdminExecute— same reason. Arbitrary SQL over an admin connection, no access-check callback, no masking pass.SavedQueryService/ListSavedQueries—READS_OTHER_USERS_SQL. The successor toListWorksheets. Its own proto comment states the property: "Bindings are ignored: the permission alone reads every matched saved query's content, private ones included," and it acceptsprojects/-.SearchSavedQueriesis caller-scoped and stays READ.QueryHistoryService/ListQueryHistoriesand the deprecatedSQLServicealias — same reason: every user's raw statements, workspace-wide under the wildcard.SQLService/ExportandDatabaseService/DiffSchemaare WRITE, not READ. Both sharebb.databases.getwith plain reads, so a permission-derived classification would have served them to a read-only session. WRITE is a serving mode, not a verb: Export takes a copy of data out of the product, and DiffSchema generates migration DDL from a schema the caller supplied.AIService/ChatandProjectService/TestWebhook—SENDS_DATA_TO_A_THIRD_PARTY.RETURNS_A_STORED_SECRET— eight rows excluded for a defect, not for what they areThese are ordinary reads that belong in a serving class on their merits. They are out because their response carries a stored secret the product already redacts elsewhere. The reason is meant to go away: fixing each leak moves its row to READ as a reviewed widening, rather than leaving a quiet exposure the moment the ceiling starts serving.
TestExcludedOnlyForALeakpins the population.ProjectService/GetProject,ListProjects,BatchGetProjects,SearchProjects—convertToProjectcopies the incoming-webhook URL out verbatim. That URL is a bearer credential, and this repo already treats it as one:redactWebhookmasks it in audit rows beside the OIDC client secret and the LDAP bind password.ListProjectspages the whole workspace.InstanceService/GetInstance,ListInstances,InstanceRoleService/ListInstanceRoles—InstanceRole.Attributeis the rawSHOW GRANTStext the MySQL driver stores. On MariaDB that carriesIDENTIFIED BY PASSWORD '<hash>', verified against mariadb:10.6 and 11.4 containers. The data-source half of the same converter gets this right and says so: "We don't return the password and SSLs on reads."UserService/GetCurrentUser— returnstemp_otp_secretandtemp_recovery_codeswhen the subject is the caller, so a session reading its own profile during an MFA-setup window captures the TOTP seed. Narrowest of the three: the window is opened by the human in the console, andUpdateUser, which starts it, is FORBIDDEN.Counterargument: classifying a method by a bug is wrong — the class should describe what the method is for, and the honest move is to fix the leak and keep them READ. Against that: the classification is inert today, so nothing is lost by excluding now, whereas a READ row whose redaction never lands is an exposure the day the gate ships. Fail-closed is this series' stated posture, and the cost is small — the built-in MCP tools do not call any of these; they are reachable only through
call_api.Behavior change: three methods newly denied on merge
ApproveIssue,RejectIssueandRetryIssueApproval, under a newDRIVES_THE_APPROVAL_DECISIONreason. Anything annotated FORBIDDEN is enforced by the existing interceptor the moment this merges, so these are the only behavior change in the PR.ApproveIssueandRejectIssueare two actions of one handler (issue_review.goreviewIssue) and it records the review decision itself:applyReviewActionrequires an approver role viacanReview, enforces the self-approval guard, and appends an APPROVED or REJECTED approver. An agent composes a change; it does not move its own change through the gate.RequestIssueis deliberately NOT in this set — it is WRITE. Spec §1b-1 named four approval methods; that line grouped by RPC family and the mechanism does not agree.RequestIssuerequires the issue to be already rejected, requires the actor to be the creator, never callscanReview, and records no decision — it strips the REJECTED approvers and returns the issue to PENDING for a fresh human decision. It approves nothing, so refusing it protects nothing the other two do not, and it costs the looppropose_database_changeexists for: while a rejection stands both Approve and Reject hard-fail, so this is the only exit from that state. Raised by the Codex review.RetryIssueApprovalis the judgment call. It casts no vote — it re-runs approval-template finding for an issue stuck in CHECKING, and only the issue creator may call it. Counterargument: refusing it costs an agent the documented self-service recovery for its own stuck issue, and the containment is partial anyway —UpdatePlanwith a specs mask andUpdateIssueon a label change both resetApprovalFindingDoneand force the same re-derivation, and both stay WRITE. Why it is in regardless: on an auto-approved result the same call activates the grant and enqueues the rollout, so it moves an issue through the gate with no human acting. The WRITE siblings are the deliberate line, not an oversight — editing a proposal is the agent's job and re-review after an edit is the system working.The reason text claims only what the classification delivers. It does not claim an agent only executes approved work:
CreatePlan,CreateRolloutandBatchRunTasksare all WRITE, and both approval checks on the execution path are guarded on an issue existing, so a plan created without one reaches execution with no approval at all. Whether an issueless rollout belongs in WRITE is 1b-2's decision to take deliberately rather than inherit.The lint
Four clauses over the compiled descriptor set, each a function over rows so a deliberately broken copy runs the same code. Nine RED tests; all green:
On "no method is both FORBIDDEN and reachable through a serving mode": asserting that row by row proves nothing, because a row carries one class and the contradiction is impossible by construction. What can drift is the vocabulary, so the clause reads
MCPMethodClassandMCPCapabilityout of the compiled descriptors and asserts every class is served by some mode or denied — never both, never neither — that every ceiling mode says what it serves, and that no row carries a class the tables skipped. Add a fifth class or a fourth mode and the build fails until somebody decides who serves it.Two more pins: the three deprecated
SQLServicequery-history aliases must match their canonicalQueryHistoryServicemethod, because the MCP surface resolves them by operation ID and a looser alias is a straight bypass; andTestExcludedOnlyForALeakabove.Debt recorded on the annotation, for 1b-2 and 1b-3
Two rows are classified where they belong and cannot be made safe by a class annotation alone, so the debt sits beside the annotation the gate PR will read:
SQLService/Queryis READ conditionally. On every engine inEngineSupportQueryNewACLthe handler classifies each statement and authorizes DML withbb.sql.dmland DDL withbb.sql.ddl— what it does is decided by its argument. 1b-3's statement clamp is what makes READ safe, and the invariant that READ_ONLY is never admitted at/mcpbefore the clamp is live covers the interval.IssueService/CreateIssueis WRITE for the DATABASE_CHANGE issue it exists for. ROLE_GRANT and ACCESS_GRANT issues complete on creation whenever the workspace rule produces no approval template, each reaching past a method that is EXCLUDED for that outcome — ROLE_GRANT writes the project IAM binding (SetIamPolicy), ACCESS_GRANT sets the grant ACTIVE (ActivateAccessGrant). A class annotation cannot split by request field, so the gate PR owes a type carve-out.UpdateIssuecarries the same debt throughallow_missing.RolloutService/CreateRolloutandBatchRunTasksstay WRITE, and two ways a change reaches execution without a human approval ride them. They get opposite answers, because only one is fixable at the MCP layer, and both are recorded on theCreateRolloutannotation:require_issue_approvalsays. The rule to implement: on the MCP path, refuseCreateRolloutandBatchRunTaskswhen the project setsrequire_issue_approvaland the plan has no linked approved issue. Not because the behavior is a defect — the shipped GitOps Service Agent role holds no issue permission at all, so issueless deployment is a supported product contract and READ_WRITE preserves supported contracts. Because without it the four FORBIDDEN approval methods are decorative for the change lane: an agent that cannot clear the gate can skip it instead. BOT-71 tracks the product-wide question; if the product closes it, this rule becomes redundant rather than wrong.approval_input_version, not the databases a group target resolves to, andCreateRolloutre-resolves the group live. No MCP rule closes it: approving an issue enqueues rollout creation on the auto path with noCreateRolloutcall, and schema sync enlarges the set with no API call. Only binding approval to a digest of the resolved set works, and live expansion was introduced deliberately (refactor: remove deployment snapshot and simplify plan updates #18589), so that is a product decision. BOT-72.CreatePlanstays WRITE with no rule of its own — a plan that is never rolled out changes nothing.Rows that changed on re-verification of #21141
WorksheetService(9 methods) is gone;SavedQueryService(11) replaced it.ListSavedQueriesinheritsListWorksheets' exclusion;SearchSavedQueriesandSearchSavedQueryFoldersare caller-scoped and READ; theGetSavedQueryPolicy/SetSavedQueryPolicypair is EXCLUDED as access control, decided together.InstanceService/ListInstanceDatabasemoves out of the read-only set. Its inline-instance branch dials a caller-supplied connection config, and even the stored branch opens an admin connection under the samebb.instances.getthatGetInstanceuses for a pure store read.IdentityProviderService/ListIdentityProvidersmoves out. With an empty parent it lists across workspaces, and each OIDC entry triggers a server-side fetch of the issuer's well-known document.ProjectService/CreateProjectmoves to EXCLUDED. Every other project-lifecycle RPC is EXCLUDED under a reason whose text names "project lifecycle", and create persists the same governance switches update is excluded for —allow_self_approval,require_issue_approval— so a read-write session could have built itself a project with the review gate off.SubscriptionService/GetSubscriptionandListPurchasePlansmove to EXCLUDED, matching the rest of billing. Licence tier stays readable throughGetActuatorInfo.ai.op.*operations resolve to plain classes; no identifier is minted.Also here
backend/api/mcp/tool_change.gotold the agent its next action wasAPPROVE_ISSUE— a call this PR refuses. It saysAWAIT_HUMAN_APPROVAL, and the issue link the tool already returns is what a human follows.Out of scope, found while classifying
Independent of the ceiling, each reachable by a human through the console today. Worth their own issues:
InstanceRole.Attribute;GetCurrentUserreturns the temp MFA secret. These are the eightRETURNS_A_STORED_SECRETrows.CreateIssuewithtype=ROLE_GRANTwrites a project IAM binding with no approver when no template matches, behind plainbb.issues.create, for a grantee the request names.AccessGrantService/ActivateAccessGrantsets a grant ACTIVE without reading the linked issue's approval state.OrgPolicyService/ListPoliciesreturns the MASKING_EXEMPTION payload gated onbb.policies.listalone, bypassing the per-type permissionGetPolicyenforces.AIService/Chatdeclaresauth_method = CUSTOMbut performs no authorization, so every authenticated workspace user can spend the stored provider key.ListInstanceDatabase's inline branch skipscheckInstanceDataSources/validateExternalSecretForSaaS, whichCreateInstanceruns.UpdateDatabaseCatalogignoresupdate_mask, so a partial request wipes every classification not restated.SettingService/GetSettingsubstitutes per-name permissions thatWorkspaceMemberRoleholds, making the sign-in policy and the MCP switch readable by every member.ListSavedQueries,GetSavedQueryandTestWebhookcarryaudit = falsewhile reading cross-user SQL or posting into customer chat.ExportRequest.admin,GetIssueRequest.forceandListInstanceRolesRequest.refreshare dead fields;ListPlansRequest.parent'sprojects/-wildcard returns zero rows;GetDatabaseMetadataRequest.filterpromises a masking level that does not exist;CheckRelease's comment omits its side effects.require_issue_approvalis enforced only when a plan has a linked issue, at all four check sites — so an issueless rollout skips it, and creating one first makes approval finding refuse afterwards. The question is whether the setting should mean "every rollout is approved" or keep meaning "if there is an issue, it must be approved" (BOT-71).Gates
buf format/lint/generate;gofmt;golangci-lint ./backend/api/...clean;go test ./backend/api/v1/ ./backend/api/mcp/ ./backend/api/auth/...and./backend/tests -run '^TestMCP'; server build; frontendtype-check.