Skip to content

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

Description

@serge-ivo

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.

Where it is

store/console/src/components/ToolPermissions.tsx:152-165:

<div className="text-sm font-semibold mb-0.5">Agent write access</div>
<p className="text-2xs text-muted-soft mb-2">
    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.
</p>
{connectors.map((connector) => (
    ...
    <span className="capitalize font-semibold">{connector}</span>
    <span className="text-muted">write access</span>

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") && (
    <p className="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 0

Checkbox counts by connector: github 23 · terminal 3 · tmux 3 · mcp 3 · supervision 1 · http 1.

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.

This is #90's first unmet bullet

#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.
  • writeConnectors must 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 by toolPolicy.test.ts.
  • 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1: blocks external usersMust be true before someone who is not the owner can run an agent (#68)connectorsConnector + tool frameworkfrontendFrontend / UI worksecuritySecurity hardening / audit finding

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions