Repository navigation
Permission mode switch (Auto→Bypass) is not honored by the next turn #3349
Description
Switching the per-session permission mode from Auto to Bypass is not honored by the next turn. This reproduces in both a plain chat session (no Goal) and a running task. After the switch, the next turn still executes under the managed/ask boundary: Bash is still sandboxed and the model is still prompted for approval, even though the picker shows Bypass. The boundary the next turn actually runs under is not refreshed to the newly committed configuration before it executes.
Expected: a permission change is observed by the next turn that starts after it, before that turn's first tool call.
How to reproduce
- In any session (a plain chat, no Goal required), switch the permission picker from Auto to Bypass.
- Send / continue so a new turn runs; issue a Bash command or a filesystem write.
- Observed: the command is still sandboxed / the model is still asked to approve; the turn still runs under the managed boundary.
Note: more frequent on long-running/multi-turn tasks — under an active Goal the continuation keeps the session claim busy, which widens the failure window — but it also reproduces in a plain session, so this is not Goal-specific.
Environment
- Maka version or commit:
98bdd6cf8(main) - OS and version: macOS 26.5.2
- Surface: Desktop
- Node.js version, if running from source: v26.7.0
Logs, screenshots, or additional context
Two code-traced observations, offered as evidence rather than as a design:
- Tools decide sandboxing/approval from the
executionBoundarythey read at call time (packages/runtime/src/tool-runtime.tsreadExecutionBoundary;packages/runtime/src/builtin-tools.tsboundary.kind === 'bypass'), butctx.permissionModeis a separate value frozen into the backend header when the backend is composed (tool-runtime.tsthis.input.header.permissionMode;execution-model-composition.ts). A backend generation is reused across turns and a permission change does not recompose it, so the frozen value never catches up. - Under an active Goal the switch can also be refused before it commits:
setPermissionModebails onhasActiveRuns, and a continuation holds a successor claim near-continuously.
Please treat the existing quiescence requirement as load-bearing until proven otherwise, rather than as part of the defect. Requiring the session to be quiescent is what lets a narrowing change (Bypass→Ask) terminate lineage shells and settle boundary requests without any additional machinery; removing it obliges whoever removes it to rebuild those guarantees by hand. The two directions are not symmetric — a widening grant cannot over-authorize anyone, because every consumer holding a stale value fails closed against a wider boundary and a descendant's admission check only gets easier.
Acceptance is the property above, verified by regression tests for the reporter's cases: a plain session where the switch lands between two turns, and a session under an active Goal. The implementation is open.
Activity
chinawch007 commentedon Aug 20, 2026
I'd like to claim this issue. I've verified the root-cause trace against current main and have a complete-fix implementation plan (single PR) below. Feedback welcome — especially on the three open decision points at the end.
Root-cause confirmation
The reporter's code trace checks out, with two refinements after reading current main:
runSessionQuiescentMutation(packages/runtime/src/runtime-kernel.ts:478-487) rejects withSessionQuiescentMutationBusyErrorwhenever any execution claim exists — it never queues and waits. Under an active Goal, claims are effectively continuous (the coordinator admits the successor turn immediately on settlement), so a switch attempted mid-goal is virtually always rejected assession_busy, or has to win a millisecond-scale inter-turn window. That matches the reported intermittency.commitExecutionResourceTransitiondisposes backends before commit (packages/runtime/src/session-manager.ts:1748-1752). What is real today is the half-applied state inside an already-running turn:readExecutionBoundaryis read live per tool call (packages/runtime/src/tool-runtime.ts:1304) whilectx.permissionModeis frozen at backend build time (tool-runtime.ts:1314). That split explains the mixed symptoms (sandbox lifted, approvals still prompted).One latent amplifier worth fixing while we're here:
sessionConfigurationMatches(packages/runtime-host/src/server/session-catalog-coordinator.ts:770-785) short-circuits tocommittedby comparing header fields only — a header/boundary divergence would be silently preserved instead of repaired.Implementation plan (complete fix, single PR)
Three pillars.
A. Single read model — the boundary is the authority
Follow #1611 to its conclusion: derive
ctx.permissionModelive from the execution boundary at each tool dispatch (viaexecutionBoundaryDisplayMode()) instead of passing the header snapshot frozen at backend build. Migration points:tool-runtime.ts:1314, thefilesystemCallfallback (builtin-tools.ts:585), the composer freeze (runtime-host/src/server/execution-model-composition.ts:322, preserving the plan-mode interplay), and the subagent snapshot (runtime-kernel.ts:2887).No storage schema migration is needed: the store already commits header + boundary atomically (
packages/storage/src/sqlite-session-metadata-store.ts:678-708) and the boundary carries a monotonic revision, so this is a read-side consolidation.B. Queued quiescent commit — replace the eager bail
New kernel primitive
runSessionQueuedQuiescentMutation(sessionIds, operation):releaseExecutionClaim;Deadlock-freedom argument: the slot waits only for attached claims, and attached claims never wait on the tail (they have already passed their barrier); claims created after the reservation are barrier-blocked on the slot itself. The waited-on set and the waiting-on-the-slot set are disjoint, so no cycle is possible. Worst-case delay is one turn.
transitionSessionConfiguration,setPermissionModeandsetExecutionBoundaryKindmove to this primitive; the internalhasActiveRunsre-checks come out (quiescence becomes the kernel's single authority).relocateSessionWorkspacekeeps its fail-fast behavior; thewaiting_for_userrejection is kept as an explicit error (decision point D1).Net semantics: a switch requested mid-turn commits in the inter-turn gap, before the successor turn's first tool call — exactly the expected behavior stated in this issue. The goal-continuation coordinator itself needs no changes; its successor claims naturally serialize behind the reserved slot.
C. Revision guard + catalog short-circuit fix (defense in depth)
BackendGenerationrecords the boundary revision it was built with;ensureActiverebuilds instead of reusing when the store's current revision differs. Any future write path that skips backend disposal self-heals within one turn, so the "all config writes must dispose" invariant stops being a convention and becomes a mechanism.sessionConfigurationMatchesadditionally compares boundary consistency, so a divergent state gets repaired by the next update instead of being short-circuited as already-committed.Testing plan
executionBoundary.kind === 'bypass'andctx.permissionMode === 'bypass'(both assertions, to catch half-application).session_busyon these paths are updated to the queued semantics — an intentional behavior change.Scope and shape
Single PR, roughly 800–900 lines including tests, landed as a commit series: (1) kernel primitive + unit tests, (2) session-manager wiring + red→green regression, (3) read-model consolidation, (4) revision guard, (5) race matrix + stress + assertion updates.
Open decision points
waiting_for_user(recommended: the user is mid permission decision) or queue them as well?permissionModefrom the boundary removes the legacy'execute'value fromctx.permissionMode. I'll audit consumers; flagging in case anything depends on it.I will start when this issue is assigned to me.
Astro-Han commentedon Aug 20, 2026
Assigned!
[-]Permission mode switch (Auto→Bypass) is not honored by the next turn of an active Goal[/-][+]Permission mode switch (Auto→Bypass) is not honored by the next turn[/+]Astro-Han commentedon Aug 21, 2026
@chinawch007 — a scope correction from the reporter: this also reproduces in a plain chat session with no Goal. Switching Auto → Bypass between two user-initiated turns, the next turn still runs under the managed/ask boundary (Bash still sandboxed, approvals still prompted). The Goal case is the most frequent trigger because continuation keeps the session claim busy, but the bug is not Goal-specific — the picker-to-next-turn path itself does not observe the new boundary.
Your two-part fix (queued quiescent commit + deriving
permissionModefrom the boundary) is session-level anyway, so it should cover both. One ask: add a plain-session case to the regression/race matrix — switch between two ordinary turns, no Goal, and assert the successor turn's first tool call seesexecutionBoundary.kind === 'bypass'andctx.permissionMode === 'bypass'. That keeps a Goal-scoped fix from closing the issue while the non-Goal path stays broken.I've updated the issue title/body to drop the "active Goal" scoping.
chinawch007 commentedon Aug 21, 2026
Thanks for the scope correction — understood: it's the picker-to-next-turn observability gap in general, and Goal continuation is just the most frequent trigger via claim continuity.
Two adjustments on my side:
executionBoundary.kind === 'bypass'andctx.permissionMode === 'bypass', as you suggested; the Goal case stays as a second scenario in the race matrix.session_busybail — so the regression will also assert intermediate state (boundary revision bump, backend rebuild, whether the catalog short-circuit fires) to pin down exactly where the plain path diverges. I'll report the findings back here before landing the fix.The fix design itself is session-level as you noted, so the architecture is unchanged.
fix(runtime): apply permission narrowing on the next dispatch, not th…
chore(runtime): add ASF license headers to the new apache#3349 test f…
feat(runtime): queued quiescent mutation with quiescence waiting (apa…
fix(runtime): queue permission transitions behind live execution (apa…
fix(runtime): derive tool permission mode from the live boundary (apa…
feat(runtime): boundary-revision guard for backend generation reuse (a…
test(runtime): permission switch race matrix and seeded interleaving …
fix(runtime): close an admission gate instead of holding the tail whi…
235 remaining items
fix(runtime): serialize live boundary checks through commit (apache#3349
fix(runtime): derive the tool permission mode from the live boundary (a…
fix(runtime): commit a widening permission change without waiting for…
fix(runtime): route live permission updates through configuration aut…
fix(runtime): read the selected permission mode live (apache#3349)
refactor(runtime-host): keep one permission resolver import path (apa…
fix(runtime): preserve legacy permission store compatibility (apache#…
test(runtime-host): cover live permission updates end to end (apache#…
fix(runtime): retain backend refreshes during activation (apache#3349)
fix(runtime): fence Explore read-grant revocation as narrowing (apach…
fix(runtime): retain refreshes across backend preflight snapshots (ap…
fix(runtime): align direct boundary transitions with their mode proje…
fix(runtime): serialize live boundary checks through commit (apache#3349
fix(runtime): apply live permission switches before the next tool call (