polyc-agent 2026.8.3

The agent turn loop: provider + tool-call routing, shared by the control plane and harness.
//! Resolving an approver's in-flight edit onto the call that executes.
//!
//! A human-in-the-loop approver may approve a gated tool call *with edited
//! arguments* (`#67`). The signed `approval_response` carries the approver's
//! edit as `modified_args_json` — empty when they approved as-is. This module
//! owns the one rule that turns "proposed args + optional edit" into "the args
//! that actually execute", so the harness resume path and any in-process caller
//! apply an approval identically.
//!
//! The split is deliberate: the model's *proposed* args remain the identity the
//! approval is bound to (matched byte-for-byte on resume, so a re-emitted call
//! with different args cannot inherit the approval — see
//! `polyc_crypto::approval::VerifiedResponse::authorizes_call`), while the
//! *edit* is a separate signed field this resolver substitutes at execution.

/// The approver's in-flight edit to a specific approved call.
///
/// Carried alongside the signed approval and keyed by the same `(request_id,
/// tool_name, args_json)` identity. An absent override means "execute the
/// proposed call unchanged", so the common approve-as-is path needs no entry.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ApprovalOverride {
    /// The approver's replacement arguments, or empty when they did not edit
    /// (execute the model's proposed args unchanged). Sourced from the signed
    /// `approval_response`, so the edit is unforgeable and auditable.
    pub modified_args_json: String,
    /// Context the approver attached to inject before the tool runs (`#67`), or
    /// empty for none — prepended as an internal-only message the model sees
    /// ahead of the tool result. Signed as part of the `approval_response`.
    pub injected_context: String,
}

/// The effective call to execute after applying an approver's edit.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedCall {
    /// The arguments to pass to `ToolExecutor::execute` — the approver's edit
    /// when they changed it, else the model's proposed args unchanged.
    pub args_json: String,
    /// Context to prepend as an internal-only message before the tool result, or
    /// `None` when the approver injected none.
    pub injected_context: Option<String>,
}

/// Resolve the effective execution call from the model's PROPOSED args and the
/// approver's optional in-flight edit.
///
/// This is the single home of the "empty edit ⇒ run the proposed args"
/// defaulting. An override whose `modified_args_json` is blank (or all
/// whitespace) is treated as "no edit" — the proposed args execute — so an
/// approver clicking plain Approve and an approver submitting an empty edit box
/// resolve to the same behavior. A blank `injected_context` likewise resolves to
/// `None` (no message injected).
#[must_use]
pub fn resolve_approved_call(
    proposed_args_json: &str,
    over: Option<&ApprovalOverride>,
) -> ResolvedCall {
    let args_json = match over {
        Some(o) if !o.modified_args_json.trim().is_empty() => o.modified_args_json.clone(),
        _ => proposed_args_json.to_owned(),
    };
    let injected_context = over
        .map(|o| o.injected_context.trim())
        .filter(|c| !c.is_empty())
        .map(str::to_owned);
    ResolvedCall {
        args_json,
        injected_context,
    }
}

#[cfg(test)]
mod tests {
    #![allow(clippy::pedantic, clippy::nursery, missing_docs)]

    use super::*;

    /// No override ⇒ the proposed args execute unchanged (the common approve
    /// path).
    #[test]
    fn no_override_runs_proposed() {
        let r = resolve_approved_call(r#"{"path":"/etc/hosts"}"#, None);
        assert_eq!(r.args_json, r#"{"path":"/etc/hosts"}"#);
    }

    /// An empty edit ⇒ still the proposed args (approver approved as-is via a
    /// blank edit box; must not execute empty/blank args).
    #[test]
    fn empty_edit_runs_proposed() {
        for blank in ["", "   ", "\n\t "] {
            let over = ApprovalOverride {
                modified_args_json: blank.to_owned(),
                injected_context: String::new(),
            };
            let r = resolve_approved_call(r#"{"path":"/etc/hosts"}"#, Some(&over));
            assert_eq!(
                r.args_json, r#"{"path":"/etc/hosts"}"#,
                "blank edit {blank:?} must fall back to the proposed args"
            );
        }
    }

    /// A non-empty edit ⇒ the edited args execute in place of the proposal.
    #[test]
    fn non_empty_edit_runs_edited() {
        let over = ApprovalOverride {
            modified_args_json: r#"{"path":"/etc/hostname"}"#.to_owned(),
            injected_context: String::new(),
        };
        let r = resolve_approved_call(r#"{"path":"/etc/shadow"}"#, Some(&over));
        assert_eq!(
            r.args_json, r#"{"path":"/etc/hostname"}"#,
            "the approver's edit is what executes"
        );
        assert_eq!(r.injected_context, None);
    }

    /// #67: injected context resolves to `Some` when set, `None` when blank, and
    /// is independent of whether the args were edited.
    #[test]
    fn injected_context_resolves_independently_of_args() {
        let over = ApprovalOverride {
            modified_args_json: String::new(),
            injected_context: "only touch files under src/".to_owned(),
        };
        let r = resolve_approved_call(r#"{"path":"a"}"#, Some(&over));
        // No args edit ⇒ proposed args run, but the context is still injected.
        assert_eq!(r.args_json, r#"{"path":"a"}"#);
        assert_eq!(
            r.injected_context.as_deref(),
            Some("only touch files under src/")
        );

        // A whitespace-only context is treated as none.
        let blank = ApprovalOverride {
            modified_args_json: String::new(),
            injected_context: "  \n".to_owned(),
        };
        assert_eq!(
            resolve_approved_call("{}", Some(&blank)).injected_context,
            None
        );
    }
}