use super::handshake::Version;
use agent_client_protocol_schema::{v1, v2};
use serde_json::{Value, json};
use std::collections::BTreeSet;
#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum Decision {
Executed,
Refused,
Cancelled,
}
pub(super) struct Request {
pub words: Value,
pub act: String,
pub target: Option<String>,
pub arguments: Option<Value>,
options: Vec<Value>,
}
impl Request {
pub fn discard_payload(&mut self) {
self.words = Value::Null;
self.arguments = None;
self.target = None;
for option in &mut self.options {
if let Some(option) = option.as_object_mut() {
option.remove("name");
}
}
}
pub fn decode(
version: Version,
params: Value,
session: &str,
bound: usize,
) -> anyhow::Result<Self> {
let Decoded {
title,
tool,
options,
id,
} = decode_wire(version, params)?;
anyhow::ensure!(id == session, "permission request names a foreign session");
anyhow::ensure!(!title.trim().is_empty(), "permission title is empty");
let options = options
.as_array()
.ok_or_else(|| anyhow::anyhow!("permission options is not an array"))?;
anyhow::ensure!(!options.is_empty(), "permission options is empty");
let mut ids = BTreeSet::new();
let mut offered = Vec::new();
for option in options {
let id = option["optionId"]
.as_str()
.filter(|id| !id.is_empty())
.ok_or_else(|| anyhow::anyhow!("permission option has no id"))?;
anyhow::ensure!(ids.insert(id), "permission option ids are duplicated");
let name = option["name"]
.as_str()
.filter(|name| !name.is_empty())
.ok_or_else(|| anyhow::anyhow!("permission option has no name"))?;
let kind = option["kind"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("permission option has no kind"))?;
offered.push(json!({"optionId":id,"name":name,"kind":kind}));
}
let mut words = json!({"title":title,"options":offered});
anyhow::ensure!(
serde_json::to_vec(&words)?.len() <= bound,
"permission words exceed content_bytes {bound}"
);
let Act {
name: act,
target,
arguments,
} = prepare_act(&tool, bound)?;
if act != "agent_permission" {
words["act"] = json!(act);
words["target"] = json!(target);
words["arguments"] = json!(arguments);
}
anyhow::ensure!(
serde_json::to_vec(&words)?.len() <= bound,
"permission words exceed content_bytes {bound}"
);
Ok(Self {
words,
act,
target,
arguments,
options: offered,
})
}
}
struct Decoded {
title: String,
tool: Value,
options: Value,
id: String,
}
fn decode_wire(version: Version, params: Value) -> anyhow::Result<Decoded> {
let (title, tool, options, id) = match version {
Version::One => {
let request: v1::RequestPermissionRequest = serde_json::from_value(params)
.map_err(|_| anyhow::anyhow!("ACP 1 permission params refused"))?;
(
request
.tool_call
.fields
.title
.clone()
.ok_or_else(|| anyhow::anyhow!("permission tool call has no title"))?,
super::handshake::encoded(request.tool_call)?,
super::handshake::encoded(request.options)?,
request.session_id.to_string(),
)
}
Version::Two => {
let request: v2::RequestPermissionRequest = serde_json::from_value(params)
.map_err(|_| anyhow::anyhow!("ACP 2 permission params refused"))?;
let tool = match request.subject {
Some(v2::RequestPermissionSubject::ToolCall(subject)) => {
super::handshake::encoded(subject.tool_call)?
}
_ => Value::Null,
};
(
request.title,
tool,
super::handshake::encoded(request.options)?,
request.session_id.to_string(),
)
}
};
Ok(Decoded {
title,
tool,
options,
id,
})
}
struct Act {
name: String,
target: Option<String>,
arguments: Option<Value>,
}
fn prepare_act(tool: &Value, bound: usize) -> anyhow::Result<Act> {
let descriptor = tool.get("rawInput");
let act = descriptor
.and_then(|value| value.get("act"))
.and_then(Value::as_str);
let deliberate = act.filter(|act| {
[
"deploy_service",
"create_agent",
"start",
"stop",
"create_site",
"bind_builder",
"unbind",
"propose_template",
"complete",
"grant",
]
.contains(act)
});
let (act, target, arguments) = if let Some(act) = deliberate {
let descriptor =
descriptor.ok_or_else(|| anyhow::anyhow!("deliberate descriptor absent"))?;
let target = descriptor["target"]
.as_str()
.filter(|target| !target.is_empty())
.ok_or_else(|| anyhow::anyhow!("deliberate permission has no target"))?;
let arguments = descriptor
.get("arguments")
.filter(|value| value.is_object())
.ok_or_else(|| anyhow::anyhow!("deliberate permission has no arguments object"))?;
anyhow::ensure!(
serde_json::to_vec(descriptor)?.len() <= bound,
"deliberate permission exceeds content_bytes {bound}"
);
(
act.to_owned(),
Some(target.to_owned()),
Some(arguments.clone()),
)
} else {
("agent_permission".to_owned(), None, None)
};
Ok(Act {
name: act,
target,
arguments,
})
}
pub(super) fn decide(request: &Request, decision: Decision) -> Value {
let kind = match decision {
Decision::Executed => "allow_once",
Decision::Refused => "reject_once",
Decision::Cancelled => return json!({"outcome":{"outcome":"cancelled"}}),
};
request
.options
.iter()
.find(|option| option["kind"] == kind)
.map_or_else(
|| json!({"outcome":{"outcome":"cancelled"}}),
|option| json!({"outcome":{"outcome":"selected","optionId":option["optionId"]}}),
)
}
pub(super) fn operation() -> anyhow::Result<String> {
use std::io::Read as _;
let mut bytes = [0_u8; 16];
std::fs::File::open("/dev/urandom")?.read_exact(&mut bytes)?;
Ok(format!("acp-{}", hex::encode(bytes)))
}
pub(super) fn held(answer: anyhow::Result<Value>) -> anyhow::Result<Value> {
let answer = answer?;
anyhow::ensure!(
answer["state"] == "held",
"agents.permission did not return a held draft"
);
Ok(answer)
}