1use buffa::Message as _;
15use polyc_proto::proto::polychrome::agent::v1::{ToolCallContent, ToolResultContent};
16
17use crate::{Signer, verify};
18
19fn tool_call_canonical_bytes(call: &ToolCallContent) -> Vec<u8> {
22 let mut canonical = call.clone();
23 canonical.signature.clear();
24 canonical.encode_to_vec()
25}
26
27fn tool_result_canonical_bytes(result: &ToolResultContent) -> Vec<u8> {
30 let mut canonical = result.clone();
31 canonical.signature.clear();
32 canonical.encode_to_vec()
33}
34
35#[must_use]
38pub fn sign_tool_call(signer: &Signer, call: &ToolCallContent) -> Vec<u8> {
39 signer.sign(&tool_call_canonical_bytes(call))
40}
41
42pub fn sign_tool_call_into(signer: &Signer, call: &mut ToolCallContent) {
44 call.signature = sign_tool_call(signer, call);
45}
46
47#[must_use]
52pub fn verify_tool_call(public_key: &[u8], call: &ToolCallContent) -> bool {
53 verify(
54 public_key,
55 &tool_call_canonical_bytes(call),
56 &call.signature,
57 )
58}
59
60#[must_use]
63pub fn sign_tool_result(signer: &Signer, result: &ToolResultContent) -> Vec<u8> {
64 signer.sign(&tool_result_canonical_bytes(result))
65}
66
67pub fn sign_tool_result_into(signer: &Signer, result: &mut ToolResultContent) {
69 result.signature = sign_tool_result(signer, result);
70}
71
72#[must_use]
77pub fn verify_tool_result(public_key: &[u8], result: &ToolResultContent) -> bool {
78 verify(
79 public_key,
80 &tool_result_canonical_bytes(result),
81 &result.signature,
82 )
83}
84
85#[cfg(test)]
86mod tests {
87 #![allow(clippy::pedantic, clippy::nursery, missing_docs)]
88
89 use polyc_proto::proto::polychrome::agent::v1::{
90 FunctionCallContent, FunctionResultContent, ToolCallContent, ToolResultContent,
91 };
92
93 use super::*;
94
95 fn function_call(name: &str) -> FunctionCallContent {
96 FunctionCallContent {
97 name: name.to_string(),
98 ..Default::default()
99 }
100 }
101
102 fn sample_call() -> ToolCallContent {
103 ToolCallContent {
104 id: "call-1".to_string(),
105 r#type: Some(function_call("web_search").into()),
106 ..Default::default()
107 }
108 }
109
110 fn sample_result() -> ToolResultContent {
111 ToolResultContent {
112 call_id: "call-1".to_string(),
113 r#type: Some(
114 FunctionResultContent {
115 name: "web_search".to_string(),
116 ..Default::default()
117 }
118 .into(),
119 ),
120 ..Default::default()
121 }
122 }
123
124 #[test]
125 fn tool_call_round_trips() {
126 let signer = Signer::from_seed(7);
127 let pk = signer.public_key_bytes();
128 let mut call = sample_call();
129 sign_tool_call_into(&signer, &mut call);
130 assert!(!call.signature.is_empty());
131 assert!(verify_tool_call(&pk, &call));
132 }
133
134 #[test]
135 fn tool_call_tampered_id_fails() {
136 let signer = Signer::from_seed(7);
137 let pk = signer.public_key_bytes();
138 let mut call = sample_call();
139 sign_tool_call_into(&signer, &mut call);
140 call.id = "call-2".to_string();
141 assert!(!verify_tool_call(&pk, &call));
142 }
143
144 #[test]
145 fn tool_call_tampered_args_fails() {
146 let signer = Signer::from_seed(7);
147 let pk = signer.public_key_bytes();
148 let mut call = sample_call();
149 sign_tool_call_into(&signer, &mut call);
150 call.r#type = Some(function_call("rm_rf").into());
151 assert!(!verify_tool_call(&pk, &call));
152 }
153
154 #[test]
155 fn tool_call_wrong_key_fails() {
156 let signer = Signer::from_seed(7);
157 let other = Signer::from_seed(8);
158 let mut call = sample_call();
159 sign_tool_call_into(&signer, &mut call);
160 assert!(!verify_tool_call(&other.public_key_bytes(), &call));
161 }
162
163 #[test]
164 fn tool_call_unsigned_fails() {
165 let signer = Signer::from_seed(7);
166 let call = sample_call();
167 assert!(!verify_tool_call(&signer.public_key_bytes(), &call));
168 }
169
170 #[test]
171 fn tool_result_round_trips() {
172 let signer = Signer::from_seed(9);
173 let pk = signer.public_key_bytes();
174 let mut result = sample_result();
175 sign_tool_result_into(&signer, &mut result);
176 assert!(!result.signature.is_empty());
177 assert!(verify_tool_result(&pk, &result));
178 }
179
180 #[test]
181 fn tool_result_tampered_call_id_fails() {
182 let signer = Signer::from_seed(9);
183 let pk = signer.public_key_bytes();
184 let mut result = sample_result();
185 sign_tool_result_into(&signer, &mut result);
186 result.call_id = "call-99".to_string();
187 assert!(!verify_tool_result(&pk, &result));
188 }
189
190 #[test]
191 fn tool_result_wrong_key_fails() {
192 let signer = Signer::from_seed(9);
193 let other = Signer::from_seed(10);
194 let mut result = sample_result();
195 sign_tool_result_into(&signer, &mut result);
196 assert!(!verify_tool_result(&other.public_key_bytes(), &result));
197 }
198}