feat(cli): add native dialog approval backend - #1775
Open
panga wants to merge 5 commits into
Open
Conversation
Add a `dialog` approval backend that prompts via a native OS dialog (macOS osascript, Linux zenity/kdialog) instead of the terminal. The `terminal` backend auto-denies or hangs inside terminal UIs that own the controlling terminal (e.g. AI coding agents); a native dialog is delivered by the window/display server, a channel separate from the tty, so it works in that environment. It implements the library `ApprovalBackend` trait with no library change and slots into the existing backend config/registry alongside terminal, webhook, and chain. Selected via `security.approval_backends` / `approval_defaults` and composable inside a chain. Security: - Fail secure: denies immediately with no subprocess when no GUI display is reachable (headless/SSH/container/CI); no terminal fallback. macOS gates on an Aqua session so SSH-into-a-Mac denies rather than hijacking the console screen; Linux requires DISPLAY/WAYLAND_DISPLAY. - No injection: untrusted request fields are ANSI/control stripped and length-bounded, then passed as single process arguments (never interpolated into AppleScript or a shell). The message starts with a fixed non-dash prefix so it cannot be parsed as a CLI option; Linux markup is entity-escaped; binaries resolved by absolute path only. - Fixed Allow/Deny labels with Deny default; bounded wall-clock timeout resolves an unattended prompt to Timeout (a denial). Closes nolabs-ai#1774 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Leonardo Zanivan <leonardo.zanivan@gmail.com>
Contributor
PR Review SummarySize
Affected crates
Blast radius — ModerateThis PR touches: source code,configuration / policy files Updated automatically on each push to this PR. |
…n test Replace the local OnceLock<Mutex<()>> in linux_denies_without_display with crate::test_env::ENV_LOCK so all env-mutating tests in the process share one serialization point. Wrap set_var/remove_var calls in unsafe blocks as required by Rust's updated safety contract for environment mutation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Leonardo Zanivan <leonardo.zanivan@gmail.com>
The project bans direct set_var/remove_var via a custom clippy lint. Use EnvVarGuard::set_all to register keys (capturing originals for restore on drop), then call .remove() on both keys to simulate a headless display environment for the linux_denies_without_display test. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Leonardo Zanivan <leonardo.zanivan@gmail.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Leonardo Zanivan <leonardo.zanivan@gmail.com>
Instead of separate Command:/Args: rows, show the full invocation as one Command: line. Lines wider than 80 chars are broken at word boundaries with a hanging indent so the value columns stay aligned. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Leonardo Zanivan <leonardo.zanivan@gmail.com>
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.
Linked Issue
Closes #1774
Summary
Adds a native OS-dialog approval backend (
dialog) for supervised approvals.The existing
terminalbackend is unusable inside terminal UIs that own thecontrolling terminal (e.g. AI coding agents like Claude Code, and any raw-mode
TUI): stderr is piped so the
is_terminal()check auto-denies, and evenwith a tty the blocking
/dev/ttyread contends with the TUI's raw-modeownership, so the prompt is invisible/unanswerable. The
dialogbackend promptsvia a native modal delivered by the window/display server — a channel separate
from the controlling terminal — so approvals work even while a TUI owns the tty.
osascriptmodal dialog.zenity/kdialogon desktop sessions.security.approval_backends/approval_defaults, and composable inside achain.It is a pure CLI-side addition alongside the existing
terminal/webhook/chainbackends: it implements the library'sApprovalBackendtrait withoutany library change, so the library/CLI boundary is untouched.
Security posture (fail-secure, no injection):
CI) it denies immediately without spawning anything and never falls back to
the terminal. macOS gates on an Aqua GUI session (
launchctl managername) soSSH-into-a-Mac denies rather than hijacking the console user's screen; Linux
requires
$DISPLAY/$WAYLAND_DISPLAY.ApprovalRequestfield is control/ANSIstripped (reusing
sanitize_for_terminal) and length-bounded, then passed tothe dialog tool as a single process argument — never interpolated into an
AppleScript program or a shell string. The message always begins with a fixed
non-
-prefix so untrusted content can never be parsed as a CLI option; Linuxmarkup is entity-escaped; dialog binaries are located by absolute path (never
$PATH).Deny is the default; untrusted text never reaches a button.
resolving a walked-away user to
Timeout(a denial) rather than blocking.Changes
crates/nono-cli/src/dialog_approval.rs(new) —DialogApprovalbackend.crates/nono-cli/src/main.rs— registermod dialog_approval.crates/nono-cli/src/command_policy.rs—Dialogvariant, validation arm(rejects
url/mode/backends), and validation tests.crates/nono-cli/src/approval_runtime.rs— builder arm constructing the backend.crates/nono-cli/data/nono-profile.schema.json— adddialogto thebackend-kind enum.
docs/cli/features/supervisor.mdx— document the backend (and correct thestale "webhook planned" note).
Agent Disclosure
repository maintainer.
AGENTS.md(library/CLI boundary, securityconsiderations, contribution policy);
crates/nono/src/supervisor/mod.rs(
ApprovalBackendtrait) and.../supervisor/types.rs(ApprovalRequest/ApprovalDecision);crates/nono-cli/src/terminal_approval.rs(reusedsanitize_for_terminal, prompt patterns);crates/nono-cli/src/approval_runtime.rs(backend builder/registry patterns, mirrored from
WebhookApproval);crates/nono-cli/src/command_policy.rs(config types and per-type validation).into existing approval-backend extension points and does not move the
library/CLI security boundary.
requirements (see checklist below).
Test Plan
cargo build -p nono-cli cargo clippy --workspace --all-targets --all-features -- -D warnings -D clippy::unwrap_used cargo fmt --all -- --check cargo test -p nono-cliChecklist
Agent Compliance Check
sanitize_for_terminal; mirroredWebhookApprovalconstruction patterns)$PATH)