supercode-harness 0.5.45

The optional native Volter Harness agent and tool harness
Documentation
//! P5-4's three load-bearing interactive handlers — the THIS-MODULE side of
//! the three deferred chains P5-1/P5-2/P5-3 each explicitly left for `tui`:
//!
//! 1. [`TuiApprovalHandler`] implements
//!    [`crate::permissions::PermissionsApprovalHandler`] (P5-1's seam):
//!    installed via [`crate::agent::Agent::set_permissions_approval_handler`].
//! 2. [`TuiElicitationHandler`] implements [`crate::mcp::McpElicitationHandler`]
//!    (P5-2's seam): installed via
//!    [`crate::mcp::McpClient::set_elicitation_handler`].
//! 3. [`TuiChildApprovalHandler`] — also a `PermissionsApprovalHandler`,
//!    installed via the factory
//!    [`crate::agent::Agent::set_child_approval_handler_factory`] (P5-4's
//!    OWN new seam, added specifically to close P5-3's §2.2 C6 "queued,
//!    never-blocking" chain into a genuinely answerable one).
//!
//! All three follow the same shape: push a `Pending*` request (from
//! [`crate::tui::bridge`]) onto a channel the render loop polls, then block
//! (synchronously for the two `PermissionsApprovalHandler`s — `ask` is a
//! sync trait method by design, see that trait's doc comment; via `.await`
//! for the async `McpElicitationHandler`) for the reply. If the reply
//! channel is ever dropped without a reply (the render loop panicked, or
//! the TUI process is shutting down mid-request), every handler here
//! resolves to the FAIL-CLOSED outcome (`Deny`/decline) — never a hang,
//! and never an implicit allow.
//!
//! **Security invariant (repeated at each handler below).** None of these
//! handlers can escalate what the permissions/elicitation engines already
//! decided: [`crate::permissions::approval::resolve_ask`] only ever calls a
//! `PermissionsApprovalHandler::ask` when the rule engine already resolved
//! the call to `Ask` (`Deny` short-circuits before any handler runs;
//! `Allow` never needs one) — a TUI "allow" here can only grant what the
//! policy already routed to a human prompt. Likewise an elicitation answer
//! is exactly what the user typed into the modal — never fabricated,
//! never auto-accepted.

use std::sync::{mpsc, Arc, Mutex};

use async_trait::async_trait;

use crate::mcp::{ElicitationRequest, ElicitationResponse, McpElicitationHandler};
use crate::permissions::{ApprovalOutcome, ApprovalRequest, PermissionsApprovalHandler};
use crate::subagents::QueuedApproval;

use super::bridge::{
    PendingApprovalRequest, PendingChildApproval, PendingElicitation, PendingOAuthDisplay,
};

/// P5-1's interactive ask-UI, closing the deferred chain
/// `crate::permissions::approval`'s module doc comment names. `ask` pushes
/// the request onto `tx` and blocks on a fresh one-shot reply channel;
/// `Self::tx` being closed (the render loop is gone) makes the blocking
/// `recv()` return an `Err`, which resolves to
/// [`ApprovalOutcome::Deny`] — fail-closed, matching the trait's own "no
/// handler ⇒ deny" default posture for the "handler installed but
/// unreachable" case too.
pub struct TuiApprovalHandler {
    tx: mpsc::Sender<PendingApprovalRequest>,
}

impl TuiApprovalHandler {
    /// Construct a handler that feeds `tx` — the matching `Receiver` half
    /// is [`TuiBridge::approval_rx`].
    pub fn new(tx: mpsc::Sender<PendingApprovalRequest>) -> Self {
        TuiApprovalHandler { tx }
    }
}

impl PermissionsApprovalHandler for TuiApprovalHandler {
    fn ask(&self, req: &ApprovalRequest) -> ApprovalOutcome {
        let (reply_tx, reply_rx) = mpsc::channel();
        let pending = PendingApprovalRequest {
            tool: req.tool.to_string(),
            subject: req.subject.map(String::from),
            raw_args: req.raw_args.clone(),
            reply_tx,
        };
        if self.tx.send(pending).is_err() {
            return ApprovalOutcome::Deny;
        }
        reply_rx.recv().unwrap_or(ApprovalOutcome::Deny)
    }
}

/// P5-3's answerable child-approval handler, closing the §2.2 C6 deferred
/// chain — see [`crate::agent::Agent::set_child_approval_handler_factory`]'s
/// doc comment for how this REPLACES (only when installed) the default
/// never-blocking [`crate::subagents::ParentQueueApprovalHandler`]. Also
/// records every request into the shared `queue` (the SAME
/// `Agent::pending_child_approvals` audit trail `ParentQueueApprovalHandler`
/// itself writes to), so [`crate::agent::Agent::pending_child_approvals`]
/// stays a complete audit log regardless of which handler answered a given
/// request.
pub struct TuiChildApprovalHandler {
    child_agent_id: String,
    queue: Arc<Mutex<Vec<QueuedApproval>>>,
    tx: mpsc::Sender<PendingChildApproval>,
}

impl TuiChildApprovalHandler {
    /// Construct a per-child handler — see
    /// `TuiBridge::child_approval_handler_factory` for the usual way one
    /// of these gets built (one per spawn, via
    /// [`crate::agent::Agent::set_child_approval_handler_factory`]).
    pub fn new(
        child_agent_id: String,
        queue: Arc<Mutex<Vec<QueuedApproval>>>,
        tx: mpsc::Sender<PendingChildApproval>,
    ) -> Self {
        TuiChildApprovalHandler {
            child_agent_id,
            queue,
            tx,
        }
    }
}

impl PermissionsApprovalHandler for TuiChildApprovalHandler {
    fn ask(&self, req: &ApprovalRequest) -> ApprovalOutcome {
        // Recorded with no outcome: this handler genuinely blocks, so the
        // entry IS a pending request until the operator answers it.
        let queued = crate::subagents::queue_approval(
            &self.queue,
            QueuedApproval {
                child_agent_id: self.child_agent_id.clone(),
                tool: req.tool.to_string(),
                subject: req.subject.map(String::from),
                queued_at_ms: now_ms(),
                outcome: None,
            },
        );
        let (reply_tx, reply_rx) = mpsc::channel();
        let pending = PendingChildApproval {
            child_agent_id: self.child_agent_id.clone(),
            tool: req.tool.to_string(),
            subject: req.subject.map(String::from),
            raw_args: req.raw_args.clone(),
            reply_tx,
        };
        let outcome = if self.tx.send(pending).is_err() {
            ApprovalOutcome::Deny
        } else {
            reply_rx.recv().unwrap_or(ApprovalOutcome::Deny)
        };
        if let Some(index) = queued {
            crate::subagents::record_queued_outcome(&self.queue, index, outcome.into());
        }
        outcome
    }
}

fn now_ms() -> i64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as i64)
        .unwrap_or(0)
}

/// P5-2's interactive elicitation UI, closing the deferred chain
/// [`crate::mcp::HeadlessElicitationHandler`]'s doc comment names. `handle`
/// is ASYNC (the MCP trait's own shape), so this uses a `tokio::sync::
/// oneshot` reply rather than blocking a thread — awaiting it yields the
/// executor to other work while the modal is up. A dropped reply sender
/// (render loop gone) resolves to [`crate::mcp::ElicitationAction::Cancel`]
/// (the MCP spec's own "dismissed without a decision" outcome — the
/// honest shape for "nobody answered", distinct from an explicit
/// `Decline`).
pub struct TuiElicitationHandler {
    tx: mpsc::Sender<PendingElicitation>,
}

impl TuiElicitationHandler {
    /// Construct a handler that feeds `tx` — the matching `Receiver` half
    /// is [`TuiBridge::elicitation_rx`].
    pub fn new(tx: mpsc::Sender<PendingElicitation>) -> Self {
        TuiElicitationHandler { tx }
    }
}

#[async_trait]
impl McpElicitationHandler for TuiElicitationHandler {
    async fn handle(&self, request: &ElicitationRequest) -> ElicitationResponse {
        let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
        let pending = PendingElicitation {
            message: request.message.clone(),
            requested_schema: request.requested_schema.clone(),
            reply_tx,
        };
        if self.tx.send(pending).is_err() {
            return ElicitationResponse {
                action: crate::mcp::ElicitationAction::Cancel,
                content: None,
            };
        }
        reply_rx.await.unwrap_or(ElicitationResponse {
            action: crate::mcp::ElicitationAction::Cancel,
            content: None,
        })
    }
}

/// The aggregate wiring point a `crates/cli` embedder uses: constructs
/// every channel pair once, installs the SENDING halves onto the `Agent`/
/// `McpClient`s that need them, and hands the RECEIVING halves to the
/// render loop to poll each frame. See [`crate::tui::should_activate`]'s
/// doc comment for why this is only ever built when the TUI is confirmed
/// active — installing these on an agent that no render loop is draining
/// would hang the first `Ask`-tier prompt or elicitation forever.
pub struct TuiBridge {
    approval_tx: mpsc::Sender<PendingApprovalRequest>,
    /// Poll this each render frame (`try_recv`) for a new top-level
    /// approval prompt to show.
    pub approval_rx: mpsc::Receiver<PendingApprovalRequest>,
    child_approval_tx: mpsc::Sender<PendingChildApproval>,
    /// Poll this each render frame for a new child-approval prompt.
    pub child_approval_rx: mpsc::Receiver<PendingChildApproval>,
    elicitation_tx: mpsc::Sender<PendingElicitation>,
    /// Poll this each render frame for a new elicitation prompt.
    pub elicitation_rx: mpsc::Receiver<PendingElicitation>,
    oauth_tx: mpsc::Sender<PendingOAuthDisplay>,
    /// Poll this each render frame for a new OAuth device-code display.
    pub oauth_rx: mpsc::Receiver<PendingOAuthDisplay>,
}

impl Default for TuiBridge {
    fn default() -> Self {
        Self::new()
    }
}

impl TuiBridge {
    /// Build a fresh bridge — four independent channel pairs, all cheap
    /// (unbounded `mpsc`, no background threads spawned here).
    pub fn new() -> Self {
        let (approval_tx, approval_rx) = mpsc::channel();
        let (child_approval_tx, child_approval_rx) = mpsc::channel();
        let (elicitation_tx, elicitation_rx) = mpsc::channel();
        let (oauth_tx, oauth_rx) = mpsc::channel();
        TuiBridge {
            approval_tx,
            approval_rx,
            child_approval_tx,
            child_approval_rx,
            elicitation_tx,
            elicitation_rx,
            oauth_tx,
            oauth_rx,
        }
    }

    /// Install this bridge's top-level-approval and child-approval-factory
    /// handlers onto `agent`. Does NOT touch MCP elicitation — an
    /// `McpClient` needs [`Self::elicitation_handler`] installed on IT
    /// directly (before the client is consumed into tool registration),
    /// which is why that's a separate method the caller invokes per
    /// client, earlier in its own connect sequence.
    pub fn install_on(&self, agent: &mut crate::agent::Agent) {
        agent.set_permissions_approval_handler(TuiApprovalHandler::new(self.approval_tx.clone()));
        // BP-3 (§2 module 6 `tools.question`): the TUI is the other
        // interactive surface §2.1's `tools_question → tui|server` edge
        // names, so `ask_user` asks through the SAME overlay an MCP
        // elicitation already uses here.
        agent.set_user_question_handler(self.elicitation_handler());
        agent.set_child_approval_handler_factory({
            let tx = self.child_approval_tx.clone();
            move |child_id, queue| {
                Arc::new(TuiChildApprovalHandler::new(child_id, queue, tx.clone()))
                    as Arc<dyn PermissionsApprovalHandler>
            }
        });
    }

    /// A fresh [`TuiElicitationHandler`] wired to this bridge — install on
    /// each `McpClient` via
    /// [`crate::mcp::McpClient::set_elicitation_handler`] before that
    /// client is consumed into tool registration.
    pub fn elicitation_handler(&self) -> Arc<dyn McpElicitationHandler> {
        Arc::new(TuiElicitationHandler::new(self.elicitation_tx.clone()))
    }

    /// The sender half for a one-shot OAuth device-code display push (P5-2
    /// device flow's `on_prompt` callback) — see `crates/cli`'s OAuth login
    /// wiring for the call site.
    pub fn oauth_sender(&self) -> mpsc::Sender<PendingOAuthDisplay> {
        self.oauth_tx.clone()
    }
}