Skip to content

feat: implement delegate user input prompting#3811

Merged
sanity merged 5 commits intomainfrom
fix-1499
Apr 10, 2026
Merged

feat: implement delegate user input prompting#3811
sanity merged 5 commits intomainfrom
fix-1499

Conversation

@sanity
Copy link
Copy Markdown
Collaborator

@sanity sanity commented Apr 10, 2026

Problem

RequestUserInput was stubbed at delegate.rs:348-360 -- every request was auto-approved by fabricating a Response::Allowed using token-generator-specific types hardcoded into the runtime. This meant any web page could silently trigger delegate operations (like ghostkey signing) without user consent.

Approach

Replace the stub with proper user prompting infrastructure that follows the existing contract request pattern:

  1. Runtime pass-through: RequestUserInput now flows through process_outbound() to the executor loop, just like GetContractRequest/PutContractRequest
  2. UserInputPrompter trait: Abstraction for prompting with three implementations:
    • SubprocessPrompter (production): spawns freenet prompt as a child process
    • AutoApprovePrompter (testing): returns first response immediately
    • AutoDenyPrompter (headless): always denies
  3. freenet prompt subcommand: Native OS dialogs via zenity/kdialog (Linux), osascript (macOS), PowerShell (Windows), with terminal fallback. Returns selected button index on stdout.
  4. 60s timeout: Auto-denies to prevent indefinite blocking

The delegate's own WASM code handles context updates via InboundDelegateMsg::UserResponse (already implemented at delegate.rs:541). The runtime no longer touches delegate-specific types.

Key design decisions

  • Subprocess pattern: The prompt runs as a child process to get its own main thread for GUI event loops (macOS Cocoa and Linux GTK require main thread). No event loop conflicts with the node's tokio runtime.
  • Platform-native dialogs: Uses OS-provided dialog tools (zenity, osascript, PowerShell) for truly native look. Future enhancement: wry+tao webview for rich HTML dialogs with custom styling.
  • Executor loop integration: RequestUserInput is intercepted and fulfilled in handle_delegate_with_contract_requests(), following the exact same pattern as contract requests (GET/PUT/UPDATE/SUBSCRIBE).

Testing

  • 4 new unit tests for UserInputPrompter implementations
  • All 2132 existing tests pass (including delegate tests that previously relied on the auto-approve stub)
  • cargo fmt and cargo clippy clean

Closes #1499

[AI-assisted - Claude]

Replace the auto-allow stub in delegate.rs that silently approved every
RequestUserInput with proper user prompting infrastructure.

Changes:
- Remove token-generator-specific Response/Context types from runtime
- Route RequestUserInput through the executor loop (same pattern as
  GET/PUT/UPDATE contract requests)
- Add UserInputPrompter trait with SubprocessPrompter (production),
  AutoApprovePrompter (testing), and AutoDenyPrompter (headless)
- Add `freenet prompt` subcommand that shows native OS dialogs:
  zenity/kdialog on Linux, osascript on macOS, PowerShell on Windows,
  with terminal fallback
- 60s timeout auto-denies to prevent indefinite blocking

The delegate's own WASM code handles context updates via UserResponse
(already implemented at delegate.rs:541). The runtime no longer touches
delegate-specific types.

Closes #1499

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 10, 2026

Rule Review: Minor style issues only — no blockers

Rules checked: code-style.md, testing.md, git-workflow.md, contracts.md
Files reviewed: 8 (mod.rs, prompt.rs, freenet.rs, contract.rs, user_input.rs, p2p_impl.rs, in_memory.rs, delegate.rs)

Warnings

None.

Info

  • crates/core/src/bin/commands/prompt.rs:303drop(stdin.write_all(message.as_bytes())) silently swallows stdin write failure in the macOS path. If the write fails, osascript reads empty stdin, the msg AppleScript variable is empty, and the function falls through to return Some(-1). Safe-fail, but silent misbehavior. Prefer stdin.write_all(message.as_bytes()).ok(); child.stdin.take(); or a return None on error. (rule: code-style.md — use explicit match/if-let in production code)

  • crates/core/src/bin/commands/prompt.rs:341drop(f.write_all(data.to_string().as_bytes())) same pattern in the Windows path. If the temp file write fails, PowerShell parses an empty/truncated JSON, $form.Tag stays at -1, and the function returns Some(-1). Again safe-fail but worth propagating. (rule: code-style.md)

  • crates/core/src/contract/user_input.rs:901AutoDenyPrompter carries #[allow(dead_code)] with a comment promising future wiring. The comment is clear, but consider leaving a // TODO(config): marker or tracking it in an issue so it doesn't linger indefinitely. (rule: code-style.md)


Rule review against .claude/rules/. WARNING findings block merge.

sanity and others added 3 commits April 10, 2026 10:44
- Rename atty_is_terminal to stdin_is_terminal (clearer, no atty crate involved)
- Replace match-with-empty-arm with simpler if-expression for zenity exit code
- Remove misleading comments (Windows dialog, kdialog menu support)
- Document why terminal_prompt ignores timeout_secs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Security: prevent command injection in osascript (pipe message via
  stdin instead of string interpolation) and PowerShell (read from
  temp JSON file instead of inline interpolation). Add input
  sanitization (length limits, control char stripping) for all platforms.
- Fix zenity extra-button logic: check stdout for button text BEFORE
  falling back to Cancel interpretation on exit code 1.
- Send denial response to delegate: on timeout/dismiss, send an empty
  ClientResponse via UserResponse so the delegate knows the request
  was denied rather than silently dropping it.
- Fix parse_message: try JSON deserialization first to unwrap
  quotes/escapes from NotificationMessage bytes before falling back
  to raw UTF-8.
- Add 6 new tests: parse_message JSON/raw/quotes, parse_button_labels
  invalid UTF-8, auto_approve/deny with 3+ responses.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add 8 tests for sanitize_message/sanitize_label: normal text,
  newline preservation, control char stripping, truncation at limits,
  empty strings
- Rename misleading test_parse_message_invalid_utf8_fallback to
  test_parse_message_json_with_quotes (test body doesn't exercise
  invalid UTF-8 path)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sanity
Copy link
Copy Markdown
Collaborator Author

sanity commented Apr 10, 2026

/ack

The handle_delegate_with_contract_requests integration test gap is acknowledged. The prompter logic has 18 unit tests covering all implementations, sanitization, and edge cases. A full integration test requires a complete ContractHandler setup (WASM delegate that emits RequestUserInput), which is significant infrastructure work. The AutoApprovePrompter injection ensures all existing delegate tests exercise the new pass-through path.

The info items (AutoDenyPrompter doc comment, constant rationale) are addressed in the latest commit.

[AI-assisted - Claude]

- Document why MAX_MESSAGE_LEN=2048, MAX_LABEL_LEN=64, MAX_LABELS=10
- Add doc comment explaining AutoDenyPrompter's intended use case

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sanity sanity enabled auto-merge April 10, 2026 16:34
@sanity sanity added this pull request to the merge queue Apr 10, 2026
Merged via the queue into main with commit 1ca803c Apr 10, 2026
12 checks passed
@sanity sanity deleted the fix-1499 branch April 10, 2026 16:56
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.

Delegates should be able to interact with users

1 participant