fix(permission): stop mode changes escalating boundaries and skipping backends - #1546
Conversation
… backends
Two ways a session's permission mode ended up different from the mode
actually being enforced.
**1. Flipping the global default escalated sessions that never chose it.**
The composer's picker is global — it writes `chatDefaults.permissionMode`,
and `applySettingsRuntimeEffects` then pushes that mode onto every
existing session, exempting only Deep Research by label.
But four flows deliberately create sessions at `explore` as their
read-only boundary: Deep Research, bot conversations (prompted by a
remote sender), expert-team review crews ("read-only review crews", says
the comment), and cron automations. Three of them were not exempt. So
changing the picker in an unrelated chat silently moved them to
`execute`/`bypass`. Bot sessions re-pinned on their next inbound message;
expert-team and cron sessions never did.
Now exempt by MODE rather than by label: `explore` is structurally
unreachable as a global default (`ChatDefaultPermissionMode` excludes it)
and the picker never offers it, so a session sitting in `explore` was put
there by a deliberate seed. That covers every current flow and any future
one without needing to remember to add it — which is precisely what went
wrong when only Deep Research was listed.
**2. A lowered mode was persisted but not enforced.**
`AiSdkBackend` snapshots the header at construction and `ToolRuntime`
reads `header.permissionMode` at every decision, so a mode change only
takes effect if the backend is rebuilt. `setPermissionMode` disposes the
backend for exactly this reason and says so. `changesBackendConfig` —
which is what `updateSession` consults — did not list `permissionMode`.
So every path that lowered a mode via `updateSession` was advisory. The
one that matters: the bot-incoming guard re-pins a bound conversation to
`explore` before handling a remote message. If the session already had a
live `execute`/`bypass` backend (e.g. the user opened it in the desktop
and sent one message), the re-pin wrote `explore` to storage and left the
permissive backend serving the remote sender.
Adding `permissionMode` closes it for every caller, not just that one.
Tests: a new contract pins that no explore-pinned session is escalated
while ordinary sessions still follow the default and busy sessions are
still left alone; `changesBackendConfig` gains a case for the mode.
Gates: 1208 core + 2667 runtime + 2900 desktop tests green, biome clean.
|
Reviewed at P2: Updating Adding The reproduced state is: The UI still says the session is waiting, but the backend holding the continuation is gone, so the permission response has no target. Please route this transition through the permission-mode authority, or apply the same waiting-state guard before disposal. Add a real |
Astro-Han
left a comment
There was a problem hiding this comment.
Approved at 73801551. The waiting-for-user disposal case remains a P2 follow-up, not a P0/P1 blocker. CI is green.
Two ways a session's permission mode ended up different from the mode actually being enforced.
1. Flipping the global default escalated sessions that never chose it
The composer's picker is global — it writes
chatDefaults.permissionMode, andapplySettingsRuntimeEffectspushes that mode onto every existing session, exempting only Deep Research by label.But four flows deliberately create sessions at
exploreas their read-only boundary:exploreexploreexploreexploreSo changing the picker in an unrelated chat silently moved three of them to
execute/bypass. Bot sessions re-pinned on their next inbound message; expert-team and cron sessions never did.Now exempt by mode rather than by label:
exploreis structurally unreachable as a global default (ChatDefaultPermissionModeexcludes it) and the picker never offers it, so a session sitting inexplorewas put there by a deliberate seed. That covers every current flow and any future one without anyone having to remember to add it — which is exactly what went wrong when only Deep Research was listed.2. A lowered mode was persisted but not enforced
AiSdkBackendsnapshots the header at construction andToolRuntimereadsheader.permissionModeat every decision, so a mode change only takes effect if the backend is rebuilt.setPermissionModedisposes the backend for exactly this reason and says so in a comment.changesBackendConfig— which is whatupdateSessionconsults — did not listpermissionMode.So every path that lowered a mode via
updateSessionwas advisory. The one that matters: the bot-incoming guard re-pins a bound conversation toexplorebefore handling a remote message. If that session already had a liveexecute/bypassbackend (e.g. the user had opened it in the desktop app and sent a message), the re-pin wroteexploreto storage and left the permissive backend serving the remote sender.Adding
permissionModecloses it for every caller, not just that one.Tests
New contract pins that no explore-pinned session is escalated, that ordinary sessions still follow the new default, and that busy (
running/waiting_for_user) sessions are still left to reconcile later.changesBackendConfiggains a case for the mode.Gates
1208 core + 2667 runtime + 2900 desktop tests green · biome clean.
Second of the permission-review fixes; #1541 was the first.