Skip to content

feat(permissions): approval notes and session-scoped user rules for the AI auto-approve judge [WIP] - #2229

Open
YodonTan wants to merge 1 commit into
GCWing:mainfrom
YodonTan:feature/approval-notes-user-rules
Open

feat(permissions): approval notes and session-scoped user rules for the AI auto-approve judge [WIP]#2229
YodonTan wants to merge 1 commit into
GCWing:mainfrom
YodonTan:feature/approval-notes-user-rules

Conversation

@YodonTan

Copy link
Copy Markdown

Fixes #2228

Summary

Extends the AI auto-approve permission mode with two capabilities:

  1. Approval notes — users can attach an optional note when approving (once / always / batch) a permission request. The note is persisted in the permission audit and injected into the model-visible tool result.
  2. Session-scoped user rules — always-approvals, approvals with a note, and rejections with a note are rebuilt once per dialog turn into a <user_rules> section the fast-model judge reads between the stable session context and the growing tool history. Subagents inherit the parent session's rules.

Design

1. Approval notes: wire-compatible DTO extension

PermissionReply::Once and PermissionReply::Always become struct variants with an optional feedback field (#[serde(default, skip_serializing_if = "Option::is_none")]):

Once  { feedback: Option<String> },
Always{ feedback: Option<String> },
Reject{ feedback: Option<String> },

The old wire shape {"reply":"once"} still serializes unchanged and still deserializes (contract test covers both directions). All construction/match sites across the workspace were updated (desktop command, CLI peer host, ACP prompt, IPC protocol tests, app-server tests).

2. Note propagation

  • Desktop respond_permission fills feedback into Once/Always replies (the request DTO already carried the field for rejections).
  • PermissionAuthorization::Allowed now carries user_feedback; the pipeline appends User approved this tool call with feedback: {note} to the tool result so the agent honors the intent in the same turn.
  • The note is stored on the tool task and rendered in judge tool history as (user note: "...") so the judge sees in-turn intent.
  • The audit record contains the reply verbatim (including the note) with source: user.

3. Session-scoped user rules for the judge

Prompt structure (stable prefix, then growing history, then the call):

<session_context>   (stable per turn)
<user_rules>        (new: stable per turn, rebuilt at turn start)
<tool_history>      (monotonically growing)
<tool_call>         (the request being judged)

Extraction (load_session_rules): from the project permission audit, filtered to the session (plus the parent session for subagents) and to source: user replies with durable intent:

  • Always approvals → Always approved: {action} on {resources}
  • approvals with a note → User approved {action} on {resources} with note: "..."
  • rejections with a note → User rejected {action} on {resources}: "..." (so the agent stops retrying refused operations)

Project grants are appended separately as Persistent grant: authorization facts so the judge knows what is already auto-allowed for the project.

Ordering and cap: newest-first (audit recency), deduplicated by a stable rule id (kind + action + resources), capped at MAX_USER_RULES = 50.

Turn stability / KV cache: rules are built once per dialog turn (cache key = project + session + dialog turn) and reused for every judge call in that turn — the prefix is byte-stable and KV-cache friendly. New approvals inside a turn only appear in the growing <tool_history> with their note marker; they are formalized into <user_rules> at the next turn boundary.

Fail-closed: the rules section opens with a fixed preamble — rules express user intent, are not blank checks; only directly matching calls count, and dangerous operations are never approved by a rule. The system prompt additionally teaches the judge to treat an in-turn (user note: "...") on a directly matching history entry as pre-approved intent.

Subagent inheritance: load_user_rules_for_batch merges delegation.parent_session_id into the session filter, so delegated tool calls judge against the same user intent.

4. LRU ordering was implemented, measured, and removed

The first iteration ordered rules by an LRU of "rule hits": each judged request was structurally matched (action + wildcard resources) against the rules and matches were recorded, with per-session persistence (permission-rule-lru/<session>.json).

Real-world testing showed this mechanism did not work as intended:

  • The judge decides semantically (reading note text), so the structural matcher essentially never fired — the LRU silently degraded to recency order.
  • A fuzzy variant (shared-substring matching) was rejected by design review: rules are both positive and negative ("approve ..." vs "forbid ..."), and fuzzy hits would wrongly promote negative rules.

The LRU machinery (matcher, port, JSON store, record/flush methods) was therefore removed entirely. Ordering is plain audit recency — simple, predictable, and aligned with "the most recent approval expresses the user's current intent". The per-turn byte-stable cache remains, as it is an independent KV-cache win.

5. Safety boundaries (unchanged behavior)

  • Inherently read-only tools (read/search/grep/glob/web fetch on non-sensitive resources) keep the deterministic fast path — no model call, zero latency.
  • Sensitive resources (.env, credentials, private keys, tokens, ...) still go through the judge.
  • rm -rf / and similar destructive/secret-exposing/system-wide operations are still rejected outright (deny + criticalReject), and the judge is explicitly told rules never cover them.
  • Any model/parse failure degrades to escalate (ask the user).

Testing

  • Unit: rule extraction (session/source/reply-kind filtering, notes, grants, dedup, recency ordering, 50-cap), prompt rendering order + fail-closed preamble, user-note marker, rule-id stability; wire compatibility (legacy {"reply":"once"} round-trip); permission manager reply/audit with notes.
  • Integration (tool_pipeline): approval with a note → tool executes and the result contains the note; approval without a note → result text unchanged; escalation → user approval with note flows through.
  • CLI/peer: approval metadata and reply construction updated.
  • UI: panel keeps allow actions enabled when a note is present and forwards the note; blank note omits the argument; three locales updated.
  • Real-device verification (user-installed build): read-only fast path; note injected into tool result and audit; <user_rules> rendered at the next turn with the user's notes; similar operations auto-approved from the next turn; subagent judge input contains the parent session's rules; rm -rf / still rejected outright with "not covered by any user rule".

@YodonTan YodonTan changed the title feat(permissions): approval notes and session-scoped user rules for the AI auto-approve judge feat(permissions): approval notes and session-scoped user rules for the AI auto-approve judge [WIP] Aug 11, 2026
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.

feat(permissions): approval notes and session-scoped user rules for the AI auto-approve judge

1 participant