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
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
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.
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:106chooseAccount 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
The runtime refusal above — "Choose which account this agent should use in its Settings".
lib/connector-accounts.ts:104-106, the stale-pin refusal — "choose a different account for this agent in its Settings".
store/console/src/components/AccountConnections.tsx:226-230, which is rendering on the live Preferences page right now because this account has two Gmails:
<pclassName="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 path — get_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.
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.
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.
With exactly one account connected, nothing new renders (matches needsPerAgentChoice's existing threshold, so nobody who never adds a second sees a change).
Setting the pin makes GET …/connector-accounts report resolves: "<accountId>", blocked: null, and a Gmail tool call on that instance stops refusing.
Clearing the pin returns the instance to blocked.reason: "ambiguous" rather than silently reverting to a default.
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.
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.
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-writtencurl.Measured live, 2026-08-23, operator account, deployed API:
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}}). MeanwhileGET /v1/email/statusreturnsconnected: 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: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:106chooseAccountfor the seven registry tools, andlib/storage-tools.ts:574-584forfind_confirmation_link, which32f0f414moved onto the same resolver three days ago (correctly — it used to.first()an arbitrary row).2. The control the refusal names was never built.
#715closed on 2026-08-20 with its own item 4 unbuilt, and its closing comment says so: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 inworkers/api/src/{routes/tools.ts,lib/connector-accounts.ts,lib/connectors/*,routes/email.ts}andstore/openapi.yaml:6480— and zero instore/console/src,agents/coder/web/srcorworkers/mcp/src.store/console/src/tabs/SettingsTab.tsxhas 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
lib/connector-accounts.ts:104-106, the stale-pin refusal — "choose a different account for this agent in its Settings".store/console/src/components/AccountConnections.tsx:226-230, which is rendering on the live Preferences page right now because this account has two Gmails: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 onneedsPerAgentChoice(entry). That predicate already exists and is already exported —store/console/src/lib/accountConnections.ts:77— and is currently called only fromAccountConnections.tsx, i.e. from the surface that tells you to go elsewhere.accountRows(entry)supplies the labels. The wiring isGET /v1/instances/:id/connector-accountson mount andPUT { 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.messageinline whenresolves === null, so the panel states the consequence rather than presenting an optional-looking dropdown.b. An MCP path —
get_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
gmail.send). It resolves today's account by accident and breaks the moment both mailboxes have the scope, which is the shape of bug Multiple accounts per connector — one Gmail per user is a primary key, and GitHub already does it right #715 exists to prevent.lib/instance-tool-policy.tsand at the checkbox, why hiding that control is a regression:agent-think.tsoffersfind_confirmation_linkon the flag alone, so removing the checkbox deletes the only off-switch for a live capability. Same argument applies here.Acceptance criteria
pinned: nulland ≥2 accounts, the panel renders theblocked.messagethe API already computes — the owner learns the agent is inert before a tool call tells them.needsPerAgentChoice's existing threshold, so nobody who never adds a second sees a change).GET …/connector-accountsreportresolves: "<accountId>",blocked: null, and a Gmail tool call on that instance stops refusing.blocked.reason: "ambiguous"rather than silently reverting to a default.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.connectorAccountsthroughpatchInstanceConfig, which is patch-semantics, so it cannot clobber sibling config. The risk worth guarding is the onePreferences.tsxwarns 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-accountsalready 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
blockedpayload,permissions.email: trueon 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.gmail_searchcall 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-accountsreports through, andconnectors/client.ts:100throwsHttpError(400, resolved.message)on!resolved.ok, so the path is read but not exercised.