use anyhow::{Context, Result};
use polyc_rpc_client::{ApprovalDialer, ApprovalOutcome as ApprovalRpcOutcome};
use serde::Deserialize;
use crate::action::ApprovalDecision;
use crate::components::approvals::{ApprovalOutcome, ApprovalView};
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct ApprovalsResponse {
pub conversation_id: String,
pub approvals: Vec<ApprovalEntry>,
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct ApprovalEntry {
pub turn_id: String,
pub request_id: String,
pub tool_name: String,
pub args_json: String,
pub response: Option<ApprovalResponseEntry>,
#[serde(default)]
pub resolve_token: String,
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct ApprovalResponseEntry {
pub approved: bool,
pub reason: String,
pub signer_pk_hex: String,
pub sig_hex: String,
pub signature_valid: bool,
}
pub(crate) async fn load_approvals(
forensics_base_url: &str,
conversation_id: &str,
) -> Result<Vec<ApprovalView>> {
let url = format!("{forensics_base_url}/api/conversations/{conversation_id}/approvals");
let body: ApprovalsResponse = crate::data::http_client()
.get(&url)
.send()
.await
.with_context(|| format!("GET {url}"))?
.error_for_status()
.with_context(|| format!("approvals projection HTTP error for {conversation_id}"))?
.json()
.await
.with_context(|| format!("decode ApprovalsResponse for {conversation_id}"))?;
let conv_id = if body.conversation_id.is_empty() {
conversation_id
} else {
&body.conversation_id
};
Ok(body
.approvals
.iter()
.map(|entry| view_from_entry(conv_id, entry))
.collect())
}
pub(crate) async fn submit_decision(
agent_addr: &str,
decision: &ApprovalDecision,
) -> Result<ApprovalRpcOutcome> {
let choice = if decision.approved {
polyc_rpc_client::ApprovalChoice::Approve
} else {
polyc_rpc_client::ApprovalChoice::Deny
};
let outcome = ApprovalDialer::new(agent_addr)
.with_context(|| format!("invalid agent address {agent_addr:?}"))?
.respond(
&decision.turn_id,
&decision.request_id,
choice,
&decision.reason,
&decision.conversation_id,
"",
"",
&decision.resolve_token,
None,
)
.await
.with_context(|| format!("ApprovalService.Respond at {agent_addr}"))?;
Ok(outcome)
}
#[must_use]
pub(crate) fn view_from_entry(conversation_id: &str, entry: &ApprovalEntry) -> ApprovalView {
ApprovalView {
turn_id: entry.turn_id.clone(),
conversation_id: conversation_id.to_owned(),
request_id: entry.request_id.clone(),
tool_name: entry.tool_name.clone(),
args_json: entry.args_json.clone(),
resolve_token: entry.resolve_token.clone(),
response: entry.response.as_ref().map(|r| ApprovalOutcome {
approved: r.approved,
reason: r.reason.clone(),
signer_pk_hex: r.signer_pk_hex.clone(),
signature_hex: r.sig_hex.clone(),
signature_valid: r.signature_valid,
}),
}
}