Skip to content

[P1] permission prompt = locked inline radios (one approval path) - #53

Merged
echobt merged 3 commits into
mainfrom
cursor/p1-permission-prompt-inline-radios-4710
Sep 8, 2026
Merged

[P1] permission prompt = locked inline radios (one approval path)#53
echobt merged 3 commits into
mainfrom
cursor/p1-permission-prompt-inline-radios-4710

Conversation

@echobt

@echobt echobt commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Replace the centred modal Tool Approval path with the locked inline numbered radios under the command (SPEC §3.10 / lock permission-prompt). One production approval path only.

Chrome: inky + green selection #1F4945 1:1 with the Designer lock. Composer placeholder is Choose an option above; > is dim while the prompt owns focus.

lock_v2 permission-prompt and permission-prompt-hover now come from production ApprovalState (request_tool_approval), not synthetic radios() stand-ins.

Runtime permission-prompt PNGs are on tip 96c4067 for Designer QA. Signed designer boards in docs/media/tui-lock-v2/{40x12,120x40}/ were not overwritten. Tip 2cc5b25 is a behavior-neutral complexity split only (no chrome / PNG regen).

Keep draft until Designer cli reviews shots. Do not undraft for the quality-gate fix.

Source/dependency policy (CI fix)

python scripts/readiness/quality.py --base e77d80ca52751208aa82fd907cc53a620c3c4dfd0 regressions.

Function Base Failed tip After extract
handle_interactive_key 37 42 36
handle_key_event 58 59 58

Helpers: handle_non_search_char, try_permission_prompt_edit, reject_pending_approval_and_exit_interactive (all ≤ 25). Permission-prompt frames stay pixel-identical.

Runtime PNGs (Designer cli)

Download from the branch (or gh raw):

Path Terminal PNG px sha256
docs/media/tui-lock-v2/runtime/40x12/permission-prompt.png 40×12 432×296 4df3824135de4f2f42dca845c91d2c10b24ca0f188a36766df473ffc8a419d77
docs/media/tui-lock-v2/runtime/120x40/permission-prompt.png 120×40 1232×912 1380a9d505f1a08ccdb6106be4dc5ad6d921f201a7d3354fd112f8bd2980fc4f
docs/media/tui-lock-v2/runtime/120x40/permission-prompt-hover.png 120×40 1232×912 cc682e964de037b9e697df80ef2c2e0d285f4f538761ccf9194fbdd94580ba9f

Sampling: exact #1F4945 is present on the focused > (37 px each). Selection bar is charcoal #262626; hover row is #1A1A1A. Zero #A78BFA / cyan. Inky #000, text #F5F5F5, dim #6B7280.

./scripts/render-tui-lock-v2.sh has no scene filter; only these three frames were rasterised and committed (full unique-PNG pack check not re-run).

What changed

  • request_tool_approval stays on Session and opens build_permission_prompt(&ApprovalState) — SPEC copy:
    • 1 Yes, run once
    • 2 Yes, always allow {snippet} in this project
    • 3 Edit command
    • 4 No — tell Cortex what to do instead
  • AppView::Approval no longer paints the centred modal; ApprovalView is a dead-path no-op.
  • card_handler overlay path is a no-op (one approval path).
  • Related scenes also use production builders where cheap: permissions-picker, sandbox-deny, question, plan-confirm, clear-confirm.
  • /clear now confirms first (clear-confirm). /permissions uses the SPEC 3-option picker.
  • Key-handler complexity: 'e' → Edit and Esc-reject live in private helpers so the quality gate does not regress vs e77d80ca.

Designer cli — how to recapture

Scene ids (lock v2):

Scene Viewports Notes
permission-prompt 40×12 + 120×40 Primary. Focused row 1, green #1F4945, dim composer >
permission-prompt-hover 120×40 only Mouse over option 2
permissions-picker lock set Related; production builder
sandbox-deny lock set Related; production builder
question lock set Related; production builder
plan-confirm lock set Related; production builder
clear-confirm lock set Related; production builder

Capture runtime frames (does not overwrite signed PNG packs):

./scripts/render-tui-lock-v2.sh
# → docs/media/tui-lock-v2/runtime/{40x12,120x40}/

Single viewport:

cargo run -p cortex-tui --bin generate_tui_lock_screenshots -- \
  --v2 --width 120 --height 40 --output target/tui-lock-v2/120x40

Compare against:

  • SPEC: docs/media/tui-lock-v2/SPEC.md §3.10
  • Index: docs/media/tui-lock-v2/index.md (permission-prompt, permission-prompt-hover)
  • Signed boards: docs/media/tui-lock-v2/{40x12,120x40}/permission-prompt.png

Check: numbered radios under the command, inky chrome, selection #1F4945, no violet, no inverted wash, placeholder Choose an option above.

Test plan

  • cargo fmt --all -- --check
  • ./scripts/clippy.sh -p cortex-tui --lib
  • Targeted tests (not full workspace — Rust workspace is large; clippy compiled the crate):
    • permission_prompt* (4)
    • lock_v2 (12) including uniqueness (77 / 31)
    • card_handler (23)
    • builders::approval (5)
    • views::approval (3)
    • request_tool_approval (1)
    • ux_contract (12) — real approval request → numbered rows at 40×12 and 120×40; keys 1-4, ↑↓, Enter, Esc
  • TUI / snapshot: ux-contract + lock_v2 uniqueness (frame count unchanged)
  • cargo audit — not re-run in this agent; no new deps
  • Source/dependency policy vs real PR base e77d80caquality.py 0 regressions / 0 policy failures
  • Local QA: unit + lock uniqueness; no TTY interactive run in this environment
  • Runtime permission-prompt / hover PNGs regenerated from live lock_v2 scenes (unchanged on this tip)

Attestation (required)

I attest that:

  • Security reviewed — auth, exec/sandbox, path traversal, network egress, and secret handling in this diff were reviewed. Approval still goes through existing approve / reject / ApproveAlways. No secrets, tokens, or keyring dumps are in the change.
  • Product-facing errors — no new API-down or provider-facing copy. User-visible labels are SPEC §3.10 Cortex product copy only.
  • TUI verified — permission-prompt surfaces exercised with ux-contract (40×12 / 120×40) plus lock_v2 uniqueness. Runtime PNGs recaptured for Designer QA. No interactive TTY in this environment. This tip does not change chrome.
  • Tests added — ux-contract drives a real approval request; lock_v2 asserts production ApprovalState; dead-path modal tests assert no "Tool Approval Required". Handler unit test covers 'e' only on the permission prompt. No mock-success.
  • No secrets — no API keys, WorkOS secrets, R2/AWS credentials, or .env files are included.

Risk

  • Approval UX only (TUI). Exec policy / sandbox decisions are unchanged; only the prompt chrome and input path changed.
  • /clear now requires confirm (clear-confirm) — small behavior change.
  • Live QuestionPromptView may still be a centred path; sandbox-deny / question Custom handlers currently close only. Follow-up if Designer wants those wired to live question/sandbox APIs.
  • Do not merge until Designer cli signs off on shots.

Follow-ups (out of scope if they balloon)

  • Wire live QuestionPromptView / sandbox-deny to the same inline radios instead of close-only Custom handlers.
  • Regen signed PNG packs only if Designer asks after reviewing runtime frames.
Open in Web Open in Cursor 

echobt and others added 3 commits September 8, 2026 03:40
Replace the centred Tool Approval modal with SPEC §3.10 inline radios
driven by production ApprovalState. lock_v2 permission-prompt scenes
now use the real builder instead of synthetic radios().

Co-authored-by: Mathis <echobt@users.noreply.github.com>
Capture live MockTerminal frames for permission-prompt (40x12, 120x40)
and permission-prompt-hover (120x40) so Designer can QA the inline radios.
Signed designer boards under 40x12/120x40 are unchanged.

Co-authored-by: Mathis <echobt@users.noreply.github.com>
Move permission-prompt 'e' and cancel-reject out of the two
over-budget handlers so quality.py reports no regressions.

Co-authored-by: Mathis <echobt@users.noreply.github.com>
@echobt
echobt marked this pull request as ready for review September 8, 2026 04:55
@echobt
echobt merged commit dca01ee into main Sep 8, 2026
16 checks passed
@echobt
echobt deleted the cursor/p1-permission-prompt-inline-radios-4710 branch September 8, 2026 04:55
@greptile-apps

greptile-apps Bot commented Sep 8, 2026

Copy link
Copy Markdown

Greptile Summary

This change moves tool approvals into the session view and adds numbered permission choices. The new flow can approve operations without showing their complete details, labels a tool-wide persistent grant as though it applies only to the displayed command, and can leave authorization behavior out of sync with the selected permission mode. It also does not return users to Help, Questions, or subagent conversations after completing an approval.

Confidence Score: 0/5

Not safe to merge until approval disclosure, persistent-approval scope, and active permission-policy synchronization are corrected. The view-restoration issue is non-blocking but should be addressed to avoid interrupting user workflows.

Three independently reproduced high-impact failures affect authorization disclosure, authorization scope, and enforcement of the selected permission setting. Two directly weaken authorization boundaries.

Files Needing Attention: src/cortex-tui/src/interactive/builders/approval.rs, src/cortex-tui/src/runner/event_loop/modal.rs, src/cortex-tui/src/runner/event_loop/actions.rs, src/cortex-tui/src/permissions/mod.rs, and src/cortex-tui/src/app/methods.rs

Security Review

Approval consent can occur without the full command arguments or edit preview, and the command-labeled persistent approval grants future access to every invocation of the same tool. Both issues weaken the user’s ability to make informed authorization decisions.

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex produced a proof for the posted P1 finding and linked a hosted proof log for review.
  • T-Rex validated the approval flow contract validation, confirming how command derivation, truncation, and handling of tool arguments are implemented.
  • T-Rex validated the always-allow prompt flow and confirmed the UI label maps to the internal key, matching the executed prompt to the displayed text.
  • T-Rex executed a permissions picker validation script and observed a clean run with all tests passing.
  • T-Rex verified the open_permission_prompt session handling and confirmed six approve/reject flows remain within the Session context.

View all artifacts

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (3)

  1. General comment

    P2 Approval prompt omits full pending approval context and collapsed shell tiles truncate commands

    • Bug
      • The inline prompt represents only its title and four radio choices. It does not expose the pending arguments or diff preview, and it exposes command text only indirectly in the always-allow label. Separately, collapsed shell tile summaries intentionally reduce commands longer than 50 characters to an ellipsis form.
    • Cause
      • build_permission_prompt converts the approval only into command/always labels and InteractiveState items; format_tool_summary applies truncate_str(cmd_str, 50) for shell-like tools.
    • Fix
      • If full approval context is required before consent, add dedicated command/arguments/diff-preview display data to the inline prompt state and renderer. Preserve or provide an expandable/full-command view for collapsed shell tiles.

    T-Rex Ran code and verified through T-Rex

  2. General comment

    P1 Command-scoped “always allow” option persists a tool-wide approval

    • Bug
      • The inline option displays a command-derived snippet (for example, npm install) but selecting it adds only approval.tool_name to the always-allowed set. Every later invocation with the same tool name is auto-approved, including commands that do not match the displayed snippet.
    • Cause
      • handle_approve_always calls self.permission_manager.allow_always(&tool_name) at src/cortex-tui/src/runner/event_loop/actions.rs:346-350; PermissionManager::should_ask checks only always_allowed.contains(tool_name) at src/cortex-tui/src/permissions/mod.rs:135-138.
    • Fix
      • Either change the label to explicitly state tool-wide approval, or persist and match a command-scoped policy (normalized command/prefix plus tool name) before bypassing prompts.

    T-Rex Ran code and verified through T-Rex

  3. General comment

    P2 Approval completion does not restore the originating view

    • Bug
      • A pending approval opened from Help, Questions, or a subagent conversation changes the active view to Session. Approving or rejecting then exits interactive mode but leaves the user in Session, with the origin still stranded in previous_view.
    • Cause
      • open_permission_prompt calls set_view(AppView::Session) for every non-Session origin (methods.rs:67-69), while approve/reject invoke go_back only if view == AppView::Approval (methods.rs:80-82, 90-92). The inline prompt intentionally uses Session rather than Approval.
    • Fix
      • Preserve and restore the pre-approval view independently of the legacy AppView::Approval branch, or explicitly call go_back when completing an inline approval that changed views.

    T-Rex Ran code and verified through T-Rex

Reviews (1): Last reviewed commit: "refactor(tui): extract helpers to drop k..." | Re-trigger Greptile

Comment on lines +121 to +143
pub fn build_permission_prompt(approval: &ApprovalState) -> InteractiveState {
let command = permission_command_line(approval);
let always = permission_always_label(&command);
let items = vec![
InteractiveItem::new("once", PERMISSION_ONCE_LABEL)
.with_description("run this command once")
.with_shortcut('1'),
InteractiveItem::new("always", always)
.with_description("remember for this project")
.with_shortcut('2'),
InteractiveItem::new("edit", PERMISSION_EDIT_LABEL)
.with_description("edit before running")
.with_shortcut('3'),
InteractiveItem::new("no", PERMISSION_NO_LABEL)
.with_description("reject")
.with_shortcut('4'),
];
InteractiveState::new(
PERMISSION_PROMPT_TITLE,
items,
InteractiveAction::Custom(PERMISSION_PROMPT_ACTION.into()),
)
.with_prompt_focus()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Approval details are hidden

The inline approval panel shows its title and four choices, but not the pending tool arguments or diff_preview. It exposes command text only indirectly in the “always allow” choice, while collapsed shell summaries truncate commands after 50 characters. A harmful command suffix or file diff can therefore remain undisclosed when the user approves an operation.

How this was verified: The rendered approval state contains no argument or diff fields, and shell summaries truncate commands longer than 50 characters.

Artifacts

Evidence from the check

  • Generated and executed script that inspects the exact approval builder and collapsed shell-summary paths, then runs focused Rust tests; it provides reproducible confirmation of the claimed behavior.

Command output from the check

  • Observed output from the executed validation script, including cited source lines, passing assertions, and passing focused Rust tests; it confirms context omission and 50-character shell-command truncation.

View artifacts

T-Rex Ran code and verified through T-Rex

Comment on lines +972 to +974
"always" => {
let _ = self.handle_action(KeyAction::ApproveAlways).await;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Approval scope is too broad

Option 2 says it will always allow the displayed command snippet, but this branch dispatches ApproveAlways, which stores only the tool name. Approving an npm install request therefore allows every later shell invocation to bypass permission prompts, including unrelated destructive commands. Scope the exception to the command and project named by the option, or clearly disclose the broader grant.

How this was verified: Selecting option 2 persists only the tool name, and subsequent permission decisions bypass prompts for that tool name.

Artifacts

Evidence from the check

  • Runs the focused before/after permission and TUI prompt checks while recording their commands, paths, outputs, and exit codes; it provides repeatable validation.

Command output from the check

  • Shows the command-snippet label, modal option-2 dispatch at lines 972-974, and a passing runtime test that high-risk Bash requires approval before an always allowance exists; the baseline prompts.

Command output from the check

  • Shows `ApproveAlways` storing only `tool_name`, the tool-name-only bypass gate, and passing runtime tests for always-allowed bypass and the displayed npm-install snippet; the allowance is tool-wide.

View artifacts

T-Rex Ran code and verified through T-Rex

Comment on lines +735 to +749
"permissions-picker" => {
self.app_state.permission_mode = match item_id.as_str() {
"ro" => crate::permissions::PermissionMode::High,
"smart" => crate::permissions::PermissionMode::Medium,
"full" => crate::permissions::PermissionMode::Low,
_ => return false,
};
let label = match item_id.as_str() {
"ro" => "Read-only",
"smart" => "Smart",
"full" => "Full access",
_ => "Permissions",
};
self.app_state.toasts.info(format!("Permissions: {label}"));
return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Synchronize the active policy

Selecting a /permissions option updates only app_state.permission_mode, while tool authorization reads permission_manager.mode. This branch never calls sync_permission_mode(). For example, switching from Full access to Read-only can leave the previous permissive policy active even though the UI reports “Permissions: Read-only,” allowing later tools to run under the wrong policy. Synchronize the permission manager before closing the picker.

T-Rex Ran code and verified through T-Rex

Comment on lines +63 to +73
pub fn open_permission_prompt(&mut self) {
if self.view == AppView::Approval {
self.go_back();
}
if self.view != AppView::Session {
self.set_view(AppView::Session);
}
if let Some(ref approval) = self.pending_approval {
let interactive = crate::interactive::builders::build_permission_prompt(approval);
self.enter_interactive_mode(interactive);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Restore the previous view

If an approval arrives while the user is in Help, Questions, or a subagent conversation, this code switches to Session. Approving or rejecting leaves the user there instead of returning to their prior view. This is non-blocking, but it interrupts the user’s current task and strands the prior view in navigation state.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Artifacts

Evidence from the check

  • The authored shell script builds and runs a small external Rust harness against the public TUI API for Help, Questions, and subagent origins, so the transition behavior is exercised without changing application source.

Command output from the check

  • The executed pre-completion run shows each origin becomes Session when the approval prompt opens, while previous_view records the origin.

Command output from the check

  • The executed completion run shows both approve and reject leave all three origins in Session with interactive mode disabled, confirming the prior view is not restored.

View artifacts

T-Rex Ran code and verified through T-Rex

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.

2 participants