Skip to content

Every Gmail call on a two-mailbox account refuses, and all three surfaces point at a Settings picker that was never built (#715's unfiled follow-up) #736

Description

@serge-ivo

What the owner sees

Connect a second Gmail and every Gmail tool on every agent stops working, on all seven tools of the #711/#713/#716 wave plus find_confirmation_link. Three separate surfaces tell the owner to fix it on the instance Settings tab. There is no such control on the Settings tab, anywhere in the console, or over MCP. The only way out is a hand-written curl.

Measured live, 2026-08-23, operator account, deployed API:

GET /v1/instances/51a3404e-9f78-4dbf-89b1-7a6976ce52c3/connector-accounts
  → { "connector": "gmail",
      "pinned": null,
      "resolves": null,
      "blocked": { "reason": "ambiguous",
        "message": "You have 2 Gmail accounts connected and this agent is not set to use
                    one of them: sivochkin@gmail.com, serge.pro.job@gmail.com. Choose which
                    account this agent should use in its Settings — nothing is sent or read
                    until you do." } }

That instance is the Job Application Assistant — the only one of the account's 43 with permissions.email: true (verified: GET /v1/instances/51a3404e…/state{"permissions":{"email":true}}). Meanwhile GET /v1/email/status returns connected: true, so the console shows Gmail connected and the permission ticked while every call refuses.

The mechanism

Two individually-correct decisions.

1. The refusal is deliberate and right. lib/connector-accounts.ts:110-119:

	if (accounts.length === 1) return { ok: true, account: accounts[0] };

	return {
		ok: false,
		reason: "ambiguous",
		message:
			`You have ${accounts.length} ${displayName} accounts connected and this agent is not set to use one of them: ` +
			`${accounts.map(describe).join(", ")}. ` +
			`Choose which account this agent should use in its Settings — nothing is sent or read until you do.`,
	};

The module header argues for this at length and I agree with it: reading the wrong mailbox "silently answers from the wrong life". Every Gmail tool routes through it — connectors/client.ts:106 chooseAccount for the seven registry tools, and lib/storage-tools.ts:574-584 for find_confirmation_link, which 32f0f414 moved onto the same resolver three days ago (correctly — it used to .first() an arbitrary row).

2. The control the refusal names was never built. #715 closed on 2026-08-20 with its own item 4 unbuilt, and its closing comment says so:

Not done here: the per-agent picker UI on the instance Settings tab. The routes are live and documented (GET/PUT /v1/instances/:id/connector-accounts); until it lands the choice is set over the API. Filing that separately.

It was not filed. Issues #716#733 are all accounted for and none is this.

Verified absence. grep -rn "connector-accounts\|connectorAccounts" over the whole repo returns hits in workers/api/src/{routes/tools.ts,lib/connector-accounts.ts,lib/connectors/*,routes/email.ts} and store/openapi.yaml:6480 — and zero in store/console/src, agents/coder/web/src or workers/mcp/src. store/console/src/tabs/SettingsTab.tsx has no account control; its only sentence on the subject points elsewhere (:549): "or disconnecting an account is account-wide and lives in Preferences → Connections."

Three surfaces make the same false promise

  1. The runtime refusal above — "Choose which account this agent should use in its Settings".
  2. lib/connector-accounts.ts:104-106, the stale-pin refusal — "choose a different account for this agent in its Settings".
  3. store/console/src/components/AccountConnections.tsx:226-230, which is rendering on the live Preferences page right now because this account has two Gmails:
	<p className="text-2xs text-muted-soft mt-1">
		Each agent must be told which of these to use, on its own Settings tab. Until it is,
		that agent will not read or send anything through {entry.label}.
	</p>

The second half of that sentence is true and measured. The first half names a tab that has nothing on it.

What to do — cheapest first

a. The one-screen fix. Render an account picker in the existing Permissions & Connections card on SettingsTab.tsx, gated on needsPerAgentChoice(entry). That predicate already exists and is already exported — store/console/src/lib/accountConnections.ts:77 — and is currently called only from AccountConnections.tsx, i.e. from the surface that tells you to go elsewhere. accountRows(entry) supplies the labels. The wiring is GET /v1/instances/:id/connector-accounts on mount and PUT { connector, accountId } on change; both routes are live, owner-scoped, and refuse an account the owner does not hold (routes/tools.ts:102-126).

Show the current blocked.message inline when resolves === null, so the panel states the consequence rather than presenting an optional-looking dropdown.

b. An MCP pathget_instance_connector_account / set_instance_connector_account. Not required for this issue, but the console is currently the only planned door and an owner debugging over MCP has none. File separately if wanted.

c. Do not relax the refusal to "pick the newest" or "pick the one used last". That is the decision #715 made on purpose and this issue is not asking to reopen it.

Rejected

Acceptance criteria

  1. With ≥2 accounts on a connector, the instance Settings tab shows which account this agent uses and lets the owner change it, without leaving the console.
  2. With pinned: null and ≥2 accounts, the panel renders the blocked.message the API already computes — the owner learns the agent is inert before a tool call tells them.
  3. With exactly one account connected, nothing new renders (matches needsPerAgentChoice's existing threshold, so nobody who never adds a second sees a change).
  4. Setting the pin makes GET …/connector-accounts report resolves: "<accountId>", blocked: null, and a Gmail tool call on that instance stops refusing.
  5. Clearing the pin returns the instance to blocked.reason: "ambiguous" rather than silently reverting to a default.
  6. AccountConnections.tsx:226-230's sentence is true — a test that the target control exists, or the sentence changes with it.

Regression risk

The picker writes agent_instances.config.connectorAccounts through patchInstanceConfig, which is patch-semantics, so it cannot clobber sibling config. The risk worth guarding is the one Preferences.tsx warns about and #355 was filed for: this control must choose among the owner's existing credentials and must never connect or disconnect one. PUT /connector-accounts already refuses an account the owner does not hold; the console test to add is that the panel renders no connect/disconnect affordance.

Verified vs inferred

  • Verified: the live blocked payload, permissions.email: true on 51a3404e, email/status.connected: true, the two Gmail rows, the three quoted code sites, the repo-wide absence of any console/MCP reference to the routes, and that Multiple accounts per connector — one Gmail per user is a primary key, and GitHub already does it right #715's promised follow-up was never filed.
  • Inferred, not observed: that a gmail_search call on a gmail-declaring instance refuses end to end. I did not run one — no instance in the account subscribes to Inbox Chat or Email Assistant, and driving the owner's agents to prove it is out of bounds. The resolver is the same one /connector-accounts reports through, and connectors/client.ts:100 throws HttpError(400, resolved.message) on !resolved.ok, so the path is read but not exercised.

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)bugSomething isn't workingconnectorsConnector + tool frameworkfrontendFrontend / UI work

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions