Skip to content

feat(permission): scope the tool permission mode to each session - #2170

Merged
wsp1911 merged 1 commit into
GCWing:mainfrom
wsp1911:dev
Aug 8, 2026
Merged

feat(permission): scope the tool permission mode to each session#2170
wsp1911 merged 1 commit into
GCWing:mainfrom
wsp1911:dev

Conversation

@wsp1911

@wsp1911 wsp1911 commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

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, locales

Motivation / 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 requiresFreshApproval are all still evaluated afterwards, so a session on full_access remains 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_context forwarded 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_access staying bounded by project / enforced / ceiling layers, the legacy auto_approve_ask key 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:

  1. SessionConfig.permission_mode: Option<PermissionMode> in the per-session state file. #[serde(default, skip_serializing_if = "Option::is_none")], so old files read as None (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.
  2. permission_mode inside UserMessageData.metadata, the free-form submission blob that also happens to be persisted with each turn (same channel as composerPresentation, sessionReferences, auto_approve_ask). It is read only at submission time; the sole read-back of persisted user-message metadata is workspace_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_mode uses deserialize_optional_permission_mode: an unrecognized value degrades to None instead 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. PermissionMode has no generated TS binding: nothing in the app-server schema references it, so gen:types never emitted one. The TS side uses a hand-written SessionPermissionMode union in AgentAPI.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

  • This PR is focused and does not include secrets, temporary prompts, generated scratch files, or unrelated artifacts.
  • Relevant verification is recorded above, or skipped checks are explained.
  • User-facing strings, docs, and locales are updated where applicable (en-US / zh-CN / zh-TW)

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.
@wsp1911
wsp1911 merged commit e846d86 into GCWing:main Aug 8, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant