feat(permission): scope the tool permission mode to each session - #2170
Merged
Conversation
The permission mode behind the chat input control was stored as two global config knobs (policy preset + auto-approve preference), so switching it in one conversation moved every other open session. Users who keep a read-only exploration session next to an editing session had no way to hold different modes at once. Make the mode a first-class value resolved per submission as `turn -> session -> project -> global default`: - Add `PermissionMode` (ask / auto_approve / full_access) to product-domains, with the preset and auto-approve knobs derived from that single value so a round can no longer run with a preset from one mode and an approval behavior from another. The project layer is reserved, not yet written. - Persist the session's own selection on `SessionConfig.permission_mode`. `None` keeps following the user-level default, so existing sessions behave exactly as before and still track later changes to that default. - Resolve the mode once per submission in the coordinator and read it from a single context key downstream. The legacy `auto_approve_ask` metadata key is still honored for CLI, dispatch, and persisted submissions. - Demote the settings-page value to the default for sessions that have not chosen their own mode, and surface the session override in the chat input with a scope label, an indicator, and a reset action. - Add an opt-in "next message only" mode that rides on the submission and is never persisted. Also fixes two subagent inheritance gaps found while wiring this up: the Task tool forwarded only boolean invocation facts, and the external subagent delegation path read submission metadata without resolving the session layer. Both dropped a delegated child back to the global default. The parent runtime ceiling still bounds the child, and a `full_access` mode remains bounded by project, agent, enforced, and constraint layers.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The permission mode behind the chat input control was stored as two global config knobs (
tool_permissions.policy.preset+interaction.auto_approve_ask), so switching it in one conversation moved every other open session. Keeping a read-only exploration session next to an editing session was impossible.This makes the mode a first-class value resolved per submission as
turn -> session -> project -> global default, and moves the chat input control from writing global config to writing the session.Type and Areas
Type: Feature (with two bug fixes found while wiring it up)
Areas: Rust core (
product-domains,runtime-ports,agent-runtime,assembly/core), desktop/Tauri, web UI, CLI, localesMotivation / Impact
Users. Each session now holds its own permission mode. The settings-page value is demoted to the default for sessions that have not chosen one, so existing sessions behave exactly as before and still track later changes to that default. The chat input menu gains a scope label, a per-mode one-off action, and a reset that returns the session to the default.
Behavior that did not change. A mode only replaces the preset baseline. Project rules, agent profile rules, enforced rules, agent constraint layers, the parent runtime ceiling, and
requiresFreshApprovalare all still evaluated afterwards, so a session onfull_accessremains bounded by every deny those layers own. Contract tests pin this.Two bugs fixed. Delegated subagents dropped back to the global default:
forward_subagent_invocation_contextforwarded only boolean invocation facts (the mode is a string), and the external subagent delegation path read submission metadata without resolving the session layer.Verification
cargo test -p bitfun-product-domains --test tool_permission_contracts # 31 passed
cargo test -p bitfun-agent-runtime # 513 passed
cargo test -p bitfun-core --features product-full --lib permission # 51 passed
cargo test -p bitfun-cli approval_metadata # 2 passed
cargo test -p bitfun-desktop --lib remote_workspace_policy # 8 passed
cargo check -p bitfun-core --features product-full && cargo check -p bitfun-desktop
pnpm --dir src/web-ui run test:run src/flow_chat/components/ChatInputWorkspaceStrip.test.tsx # 22 passed
pnpm --dir src/web-ui run test:run src/flow_chat/utils/permissionMode.test.ts # 5 passed
pnpm run type-check:web
pnpm --dir src/web-ui run gen:types # 97 bindings, unchanged
pnpm run i18n:audit
pnpm run theme:color-audit:all
pnpm run check:core-boundaries
pnpm run check:repo-hygiene
pnpm run fmt:rs
Focused suites cover: layered mode resolution and source attribution, the mode → preset/auto-approve projection,
full_accessstaying bounded by project / enforced / ceiling layers, the legacyauto_approve_askkey not downgrading a resolved mode, per-session isolation and clearing, fork inheritance, subagent inheritance (including rejecting an unparseable value), persisted-field tolerance, and the chat input menu's session-vs-one-off separation.Reviewer Notes
Persisted / wire changes — two additions, both new fields, no schema version change, no existing field modified or removed:
SessionConfig.permission_mode: Option<PermissionMode>in the per-session state file.#[serde(default, skip_serializing_if = "Option::is_none")], so old files read asNone(follow the default) and sessions without a selection write byte-identical files — downgrade is a no-op. Values are"ask" | "auto_approve" | "full_access"; these literals are now a long-term compatibility surface.permission_modeinsideUserMessageData.metadata, the free-form submission blob that also happens to be persisted with each turn (same channel ascomposerPresentation,sessionReferences,auto_approve_ask). It is read only at submission time; the sole read-back of persisted user-message metadata isworkspace_references_from_metadata. If a replay/resubmit-from- history feature is ever added, it must decide explicitly whether to strip this key — honoring it would let an old turn's one-off override fire again.Unchanged: global config schema,
tool-permissions.sqlite(still schema version 1),SessionMetadata, project permission files, and every exported permission DTO.Deliberate tolerance.
SessionConfig.permission_modeusesdeserialize_optional_permission_mode: an unrecognized value degrades toNoneinstead of failing the entire session state file. This is the failure mode that previously blocked startup on incompatible model settings. The unreadable selection is dropped rather than preserved — a value this build cannot evaluate must not decide how tools get authorized.Known divergence.
PermissionModehas no generated TS binding: nothing in the app-server schema references it, sogen:typesnever emitted one. The TS side uses a hand-writtenSessionPermissionModeunion inAgentAPI.ts, matching the existing hand-written desktop DTO route. Adding a fourth mode means editing both sides; the compiler will not catch a mismatch.Rollback. Revert is safe. Sessions that wrote a mode leave an unknown key that older builds ignore.
Checklist