You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The write-consent panel describes a browser for every connector — 27 of 32 instances are told an agent will "click, type and navigate on your machine" about a cloud API #720
Measured against production and main @ 8bf125c4 on 2026-08-21.
What the owner sees
Settings → Permissions & Connections, on a Repo Coder instance. Rendered text, captured in WebKit at 420px against proagentstore.online:
Agent write access
Lets this agent act as you — click, type, and navigate — through the connector on your machine. Off by default; enable only what you want it to do.
☐ Github write access
Every clause of that sentence is false for the checkbox underneath it. github_create_issue opens an issue on github.com over the REST API, under the owner's GitHub identity. Nothing is clicked, nothing is typed, nothing happens on the owner's machine. The owner is deciding whether to hand an agent a credential and the only description on offer describes a different connector.
One paragraph, one .map(), no per-connector anything. The set comes from writeConnectors (store/console/src/lib/toolPolicy.ts:161), which is correct and should not change — it reads the gate's own verdict. What is missing is anything to say about each member of it.
The mechanism: the sentence was right once, and nothing made it wrong
Two individually-correct decisions.
One. The paragraph was written for the connector that motivated the panel. git log -S "click, type, and navigate" → 2ad71712 (2026-08-03), whose message is explicit: "The #90 write-consent gate shipped backend-only … so a browser agent's write tools (browser_navigate/act) could never be granted — there was no checkbox." For browser, the sentence is exact.
Two. Connectors kept arriving — github, terminal, tmux, supervision, http, mcp, and gmail at #711/#713 — and each one correctly declared itself in the registry and correctly appeared in this list. None of them touched the copy, because there is nowhere in a connector's declaration to put a sentence. Connector (workers/api/src/lib/connectors/types.ts:213-262) declares id, label, auth, scopes, grantModel, tokenEnv, unattended, credentialEnv, oauth, tools. There is no field for what granting write to this connector permits, so the only place to say it is the shared paragraph, and the only way to say something connector-specific is to hardcode an if.
Somebody already did, twenty lines down (ToolPermissions.tsx:168-172):
{connectors.includes("mcp")&&(<pclassName="text-2xs text-muted-soft mt-1.5">
MCP write access is a kill switch, not a permission: the agent still can’t call anything until you name a server and tool below.
</p>)}
That is the tell. It is the correct sentence, in the wrong place — a per-connector fact stored in the console because the connector had nowhere to declare it. Gmail needs one now, which would make three, and the next connector makes four.
Measured, live, 2026-08-21 (43 instances of the operator account)
Mirroring writeConnectors over GET /v1/instances/:id/tools for every instance:
Instances rendering the panel
32 of 43
Rendering ≥1 checkbox for a connector that is not on the user's machine
27 of 32
Rendering a browser checkbox — the connector the sentence describes
Only tmux and terminal are on the owner's machine at all, and neither clicks or navigates — they run shell commands. supervision never leaves the platform. browser is not_declared on every instance including the Job Application Assistant (verified: browser_navigate/browser_snapshot/browser_act all allowed:false, reason:"not_declared"), so the one connector the paragraph accurately describes is currently shown to nobody.
#90 (CLOSED, "Connector consent + write-scope safety + admin visibility") opens its Do list with:
Declared scopes per connector (read vs write) surfaced to the subscriber at grant time: "this agent will be able to write to spreadsheet X / open PRs on repo Y."
Note the shape of the example: per connector, and concrete. The closing comment lists the consent library, the fail-closed gate in runRegistryTool, and /v1/admin/connectors — all real, all shipped, and none of them this. The grant-time statement was never built, and this ticket is that bullet.
What to do — cheapest first
1. Use the label the API already sends, not the raw id.capitalize on an id renders "Github", "Mcp", "Tmux", "Http". GET /v1/connectors and GET /v1/instances/:id/connectors both already carry label — verified live: "GitHub", "MCP server (generic, outbound)", "HTTP / REST (generic)", "Terminal (local runner)", "Supervision (delegate to agents you oversee)", "Gmail". One-line-ish, no schema change, and it stops the panel naming a connector differently from every other surface.
2. Declare what write means, on the connector. Add one required-for-write field to Connector in workers/api/src/lib/connectors/types.ts:
/** What granting this connector's WRITE scope actually lets an agent do, in the owner's * terms — shown on the consent checkbox. Required when `scopes.write` is true: a grant * the owner cannot evaluate is not consent. Say what it does to the world, not which * API it calls. */
writeMeaning?: string;
Suggested initial values, from what the tools actually do (all verified against the handlers):
github — "Open, comment on and edit issues on any repository this agent can reach, as you. It cannot merge, push or delete."
gmail — "Send mail from your mailbox, as you, to anyone. Sent mail cannot be recalled. Also archive and mark messages read."
terminal / tmux — "Type commands into a terminal on your own computer and press Enter. Whatever your shell can do, it can do."
http — "Send POST/PUT/PATCH/DELETE requests to any API you have configured, using your stored key."
mcp — keep the existing kill-switch sentence verbatim, moved here from ToolPermissions.tsx:168-172 (see Regression risk).
supervision — "Hand goals to the other agents you have placed under this one. It stays inside ProAgentStore and spends your tokens."
browser — the current paragraph, which is correct for it: "Click, type and navigate in a browser on your own computer, as you, in your logged-in sessions."
3. Surface and render it. Add writeMeaning to ConnectorPolicyEntry (workers/api/src/lib/instance-connector-policy.ts) so GET /v1/instances/:id/connectors carries it beside the label it already returns. ToolPermissions keeps deriving the checkbox set from writeConnectors(toolPolicy) — that must stay, it is the gate's own verdict — and looks up label + meaning from the connectors response, falling back to the id when either is absent. Replace the shared paragraph with something that makes no per-connector claim: "Each of these lets the agent act with a credential of yours. Off by default; read what each one permits before enabling it."
4. Delete the mcp special case once its sentence lives on the connector.
Alternatives considered and rejected
Just rewrite the paragraph to be vaguer. Rejected: a sentence general enough to be true of supervision and gmail at once ("lets the agent act with your credentials") says nothing an owner can act on, and the specific thing they need to know — sent mail cannot be recalled — has nowhere to go. Vagueness is how this stops being wrong without starting to be useful.
A second inline if (connectors.includes("gmail")). Rejected: it is the third instance of a pattern already visibly failing, in a console that lib/toolPolicy.ts's own header describes being rewritten precisely to stop the console holding opinions the server should state.
Put the prose on each ToolDef instead. Rejected: the consent is per connector, not per tool, so seven Gmail tools would carry seven copies of one fact and the panel would have to pick one. Per-tool description already exists and is already rendered (ToolPermissions.tsx:139) — this is the other, coarser thing.
Derive it from scopes/grantModel/tier. Rejected for the reason tool-reach.ts records for reach: every structural field was measured as a proxy for a human-facing claim and each was wrong in at least one direction. github and tmux are structurally identical here (grantModel:"user", scopes.write:true) and permit entirely different things.
Acceptance criteria
Each write-access checkbox names its connector using the registry label ("GitHub", not "Github").
Each checkbox carries a per-connector statement of what granting it permits, sourced from the connector's own declaration.
The shared paragraph makes no claim that is false for any connector that can appear in the list.
The connectors.includes("mcp") branch in ToolPermissions.tsx is gone, and its sentence still renders for mcp.
A test fails if a connector with scopes.write === true has no writeMeaning. This is the part that keeps the fix from decaying — the current copy survived seven connectors precisely because nothing asserted anything about it.
Regression risk
The MCP sentence is load-bearing and easy to lose in the move. It is the only thing telling an owner that ticking mcp grants no reach on its own (Outbound MCP consent should be per server and per remote tool, not one global write grant #262 grants are per server+tool, McpConnections). Deleting the inline branch without the connector-level replacement rendering first would make the mcp checkbox read as a full grant. Caught by a console test asserting the kill-switch text renders for an mcp-write instance.
A connectors-endpoint failure must degrade, not blank the panel.ToolPermissions.tsx:41-53 documents at length why a dropped read here is the worst possible silent failure: an unchecked box is a claim the agent cannot act as you. A missing meaning must fall back to the id and still render the checkbox.
Not in scope
Ordering. The fine-grained control (per-tool switches, ToolPermissions.tsx:118-145, where gmail_send is individually switchable and the copy already says it binds "the agent's chat, the API and MCP alike") renders above the coarse one, so an owner meets "Gmail — write access" before learning the tools are individually switchable. Real, but a smaller effect than a false description, and reordering panels is a separate judgement about what a reader should meet first. Noted here rather than filed.
Measured against production and
main@8bf125c4on 2026-08-21.What the owner sees
Settings → Permissions & Connections, on a Repo Coder instance. Rendered text, captured in WebKit at 420px against
proagentstore.online:Every clause of that sentence is false for the checkbox underneath it.
github_create_issueopens an issue on github.com over the REST API, under the owner's GitHub identity. Nothing is clicked, nothing is typed, nothing happens on the owner's machine. The owner is deciding whether to hand an agent a credential and the only description on offer describes a different connector.Where it is
store/console/src/components/ToolPermissions.tsx:152-165:One paragraph, one
.map(), no per-connector anything. The set comes fromwriteConnectors(store/console/src/lib/toolPolicy.ts:161), which is correct and should not change — it reads the gate's own verdict. What is missing is anything to say about each member of it.The mechanism: the sentence was right once, and nothing made it wrong
Two individually-correct decisions.
One. The paragraph was written for the connector that motivated the panel.
git log -S "click, type, and navigate"→2ad71712(2026-08-03), whose message is explicit: "The #90 write-consent gate shipped backend-only … so a browser agent's write tools (browser_navigate/act) could never be granted — there was no checkbox." Forbrowser, the sentence is exact.Two. Connectors kept arriving —
github,terminal,tmux,supervision,http,mcp, andgmailat #711/#713 — and each one correctly declared itself in the registry and correctly appeared in this list. None of them touched the copy, because there is nowhere in a connector's declaration to put a sentence.Connector(workers/api/src/lib/connectors/types.ts:213-262) declaresid,label,auth,scopes,grantModel,tokenEnv,unattended,credentialEnv,oauth,tools. There is no field for what granting write to this connector permits, so the only place to say it is the shared paragraph, and the only way to say something connector-specific is to hardcode anif.Somebody already did, twenty lines down (
ToolPermissions.tsx:168-172):That is the tell. It is the correct sentence, in the wrong place — a per-connector fact stored in the console because the connector had nowhere to declare it. Gmail needs one now, which would make three, and the next connector makes four.
Measured, live, 2026-08-21 (43 instances of the operator account)
Mirroring
writeConnectorsoverGET /v1/instances/:id/toolsfor every instance:browsercheckbox — the connector the sentence describesCheckbox counts by connector:
github23 ·terminal3 ·tmux3 ·mcp3 ·supervision1 ·http1.Only
tmuxandterminalare on the owner's machine at all, and neither clicks or navigates — they run shell commands.supervisionnever leaves the platform.browserisnot_declaredon every instance including the Job Application Assistant (verified:browser_navigate/browser_snapshot/browser_actallallowed:false, reason:"not_declared"), so the one connector the paragraph accurately describes is currently shown to nobody.This is #90's first unmet bullet
#90 (CLOSED, "Connector consent + write-scope safety + admin visibility") opens its Do list with:
Note the shape of the example: per connector, and concrete. The closing comment lists the consent library, the fail-closed gate in
runRegistryTool, and/v1/admin/connectors— all real, all shipped, and none of them this. The grant-time statement was never built, and this ticket is that bullet.What to do — cheapest first
1. Use the label the API already sends, not the raw id.
capitalizeon an id renders "Github", "Mcp", "Tmux", "Http".GET /v1/connectorsandGET /v1/instances/:id/connectorsboth already carrylabel— verified live:"GitHub","MCP server (generic, outbound)","HTTP / REST (generic)","Terminal (local runner)","Supervision (delegate to agents you oversee)","Gmail". One-line-ish, no schema change, and it stops the panel naming a connector differently from every other surface.2. Declare what write means, on the connector. Add one required-for-write field to
Connectorinworkers/api/src/lib/connectors/types.ts:Suggested initial values, from what the tools actually do (all verified against the handlers):
github— "Open, comment on and edit issues on any repository this agent can reach, as you. It cannot merge, push or delete."gmail— "Send mail from your mailbox, as you, to anyone. Sent mail cannot be recalled. Also archive and mark messages read."terminal/tmux— "Type commands into a terminal on your own computer and press Enter. Whatever your shell can do, it can do."http— "Send POST/PUT/PATCH/DELETE requests to any API you have configured, using your stored key."mcp— keep the existing kill-switch sentence verbatim, moved here fromToolPermissions.tsx:168-172(see Regression risk).supervision— "Hand goals to the other agents you have placed under this one. It stays inside ProAgentStore and spends your tokens."browser— the current paragraph, which is correct for it: "Click, type and navigate in a browser on your own computer, as you, in your logged-in sessions."3. Surface and render it. Add
writeMeaningtoConnectorPolicyEntry(workers/api/src/lib/instance-connector-policy.ts) soGET /v1/instances/:id/connectorscarries it beside thelabelit already returns.ToolPermissionskeeps deriving the checkbox set fromwriteConnectors(toolPolicy)— that must stay, it is the gate's own verdict — and looks up label + meaning from the connectors response, falling back to the id when either is absent. Replace the shared paragraph with something that makes no per-connector claim: "Each of these lets the agent act with a credential of yours. Off by default; read what each one permits before enabling it."4. Delete the
mcpspecial case once its sentence lives on the connector.Alternatives considered and rejected
supervisionandgmailat once ("lets the agent act with your credentials") says nothing an owner can act on, and the specific thing they need to know — sent mail cannot be recalled — has nowhere to go. Vagueness is how this stops being wrong without starting to be useful.if (connectors.includes("gmail")). Rejected: it is the third instance of a pattern already visibly failing, in a console thatlib/toolPolicy.ts's own header describes being rewritten precisely to stop the console holding opinions the server should state.ToolDefinstead. Rejected: the consent is per connector, not per tool, so seven Gmail tools would carry seven copies of one fact and the panel would have to pick one. Per-tool description already exists and is already rendered (ToolPermissions.tsx:139) — this is the other, coarser thing.scopes/grantModel/tier. Rejected for the reasontool-reach.tsrecords forreach: every structural field was measured as a proxy for a human-facing claim and each was wrong in at least one direction.githubandtmuxare structurally identical here (grantModel:"user",scopes.write:true) and permit entirely different things.Acceptance criteria
label("GitHub", not"Github").connectors.includes("mcp")branch inToolPermissions.tsxis gone, and its sentence still renders formcp.scopes.write === truehas nowriteMeaning. This is the part that keeps the fix from decaying — the current copy survived seven connectors precisely because nothing asserted anything about it.Regression risk
mcpgrants no reach on its own (Outbound MCP consent should be per server and per remote tool, not one global write grant #262 grants are per server+tool,McpConnections). Deleting the inline branch without the connector-level replacement rendering first would make themcpcheckbox read as a full grant. Caught by a console test asserting the kill-switch text renders for anmcp-write instance.writeConnectorsmust not be re-derived from the connectors endpoint.toolPolicy.ts:161's comment and [bug] The tool listing reports write-gated tools as allowed=true, reason=ok — agents burn a turn discovering the consent gate #351 are explicit that the checkbox set comes from the server's tool verdict so the gate and the UI cannot disagree. This change adds a lookup, not a second source. Caught bytoolPolicy.test.ts.ToolPermissions.tsx:41-53documents at length why a dropped read here is the worst possible silent failure: an unchecked box is a claim the agent cannot act as you. A missing meaning must fall back to the id and still render the checkbox.Not in scope
ToolPermissions.tsx:118-145, wheregmail_sendis individually switchable and the copy already says it binds "the agent's chat, the API and MCP alike") renders above the coarse one, so an owner meets "Gmail — write access" before learning the tools are individually switchable. Real, but a smaller effect than a false description, and reordering panels is a separate judgement about what a reader should meet first. Noted here rather than filed.