adk_computer_use/runtime/
binding.rs1use crate::contracts::{ActionEnvelope, ControlLease, ExecutionReceipt, TargetReservation};
13use crate::error::ComputerUseError;
14
15pub fn validate_envelope_freshness(envelope: &ActionEnvelope) -> Result<(), ComputerUseError> {
27 let proposed_at =
28 chrono::DateTime::parse_from_rfc3339(&envelope.proposed_at).map_err(|error| {
29 ComputerUseError::IdentityMismatch(format!(
30 "action envelope {} has an unreadable proposed_at {:?}: {error}",
31 envelope.action_id, envelope.proposed_at
32 ))
33 })?;
34 let expires_at =
35 chrono::DateTime::parse_from_rfc3339(&envelope.expires_at).map_err(|error| {
36 ComputerUseError::IdentityMismatch(format!(
37 "action envelope {} has an unreadable expires_at {:?}: {error}",
38 envelope.action_id, envelope.expires_at
39 ))
40 })?;
41
42 if expires_at <= proposed_at {
43 return Err(ComputerUseError::IdentityMismatch(format!(
44 "action envelope {} has a non-positive validity window: proposed_at is {} and \
45 expires_at is {}",
46 envelope.action_id, envelope.proposed_at, envelope.expires_at
47 )));
48 }
49 if expires_at <= chrono::Utc::now() {
50 return Err(ComputerUseError::IdentityMismatch(format!(
51 "action envelope {} expired at {}",
52 envelope.action_id, envelope.expires_at
53 )));
54 }
55
56 Ok(())
57}
58
59fn mismatch(object: &str, field: &str, expected: &str, actual: &str) -> ComputerUseError {
61 ComputerUseError::IdentityMismatch(format!(
62 "{object} returned by the computer-use runtime is not bound to this request: {field} \
63 is {actual:?}, expected {expected:?}. The response was rejected rather than stored."
64 ))
65}
66
67fn check_optional(
72 object: &str,
73 field: &str,
74 expected: Option<&str>,
75 actual: Option<&str>,
76) -> Result<(), ComputerUseError> {
77 match (expected, actual) {
78 (Some(expected), Some(actual)) if expected != actual => {
79 Err(mismatch(object, field, expected, actual))
80 }
81 _ => Ok(()),
82 }
83}
84
85pub fn validate_lease(
111 lease: &ControlLease,
112 envelope: &ActionEnvelope,
113) -> Result<(), ComputerUseError> {
114 const OBJECT: &str = "control lease";
115
116 if lease.session_id != envelope.session_id {
117 return Err(mismatch(OBJECT, "session_id", &envelope.session_id, &lease.session_id));
118 }
119 if lease.principal_id != envelope.principal_id {
120 return Err(mismatch(OBJECT, "principal_id", &envelope.principal_id, &lease.principal_id));
121 }
122 check_optional(OBJECT, "agent_id", envelope.agent_id.as_deref(), lease.agent_id.as_deref())?;
123
124 if lease.execution_mode != envelope.requested_mode {
125 return Err(mismatch(
126 OBJECT,
127 "execution_mode",
128 &format!("{:?}", envelope.requested_mode),
129 &format!("{:?}", lease.execution_mode),
130 ));
131 }
132
133 if !lease.state.eq_ignore_ascii_case("active") {
136 return Err(ComputerUseError::IdentityMismatch(format!(
137 "control lease {} is in state {:?}, not active, so it authorizes nothing",
138 lease.lease_id, lease.state
139 )));
140 }
141 if lease.actions_used >= lease.action_budget {
144 return Err(ComputerUseError::IdentityMismatch(format!(
145 "control lease {} has no remaining action budget: {} of {} used",
146 lease.lease_id, lease.actions_used, lease.action_budget
147 )));
148 }
149
150 match chrono::DateTime::parse_from_rfc3339(&lease.expires_at) {
154 Ok(expires_at) => {
155 if expires_at <= chrono::Utc::now() {
156 return Err(ComputerUseError::IdentityMismatch(format!(
157 "control lease {} expired at {}",
158 lease.lease_id, lease.expires_at
159 )));
160 }
161 }
162 Err(e) => {
163 return Err(ComputerUseError::IdentityMismatch(format!(
164 "control lease {} has an unreadable expiry {:?}: {e}",
165 lease.lease_id, lease.expires_at
166 )));
167 }
168 }
169
170 if let Some(target) = &envelope.target {
173 if !lease.boundaries.app_ids.is_empty()
174 && !lease.boundaries.app_ids.contains(&target.app_id)
175 {
176 return Err(mismatch(
177 OBJECT,
178 "boundaries.app_ids",
179 &target.app_id,
180 &format!("{:?}", lease.boundaries.app_ids),
181 ));
182 }
183
184 if let Some(window_id) = &target.window_id
185 && !lease.boundaries.window_ids.is_empty()
186 && !lease.boundaries.window_ids.contains(window_id)
187 {
188 return Err(mismatch(
189 OBJECT,
190 "boundaries.window_ids",
191 &format!("{window_id}"),
192 &format!("{:?}", lease.boundaries.window_ids),
193 ));
194 }
195 }
196
197 Ok(())
198}
199
200pub fn validate_reservation(
208 reservation: &TargetReservation,
209 envelope: &ActionEnvelope,
210) -> Result<(), ComputerUseError> {
211 const OBJECT: &str = "target reservation";
212
213 if reservation.session_id != envelope.session_id {
214 return Err(mismatch(OBJECT, "session_id", &envelope.session_id, &reservation.session_id));
215 }
216 if reservation.principal_id != envelope.principal_id {
217 return Err(mismatch(
218 OBJECT,
219 "principal_id",
220 &envelope.principal_id,
221 &reservation.principal_id,
222 ));
223 }
224 check_optional(
225 OBJECT,
226 "agent_id",
227 envelope.agent_id.as_deref(),
228 reservation.agent_id.as_deref(),
229 )?;
230 check_optional(
231 OBJECT,
232 "execution_group_id",
233 envelope.execution_group_id.as_deref(),
234 reservation.execution_group_id.as_deref(),
235 )?;
236
237 if reservation.intent_id != envelope.action_id {
238 return Err(mismatch(OBJECT, "intent_id", &envelope.action_id, &reservation.intent_id));
239 }
240 if !reservation.state.eq_ignore_ascii_case("active") {
241 return Err(ComputerUseError::IdentityMismatch(format!(
242 "target reservation {} is in state {:?}, not active",
243 reservation.reservation_id, reservation.state
244 )));
245 }
246 match chrono::DateTime::parse_from_rfc3339(&reservation.expires_at) {
247 Ok(expires_at) => {
248 if expires_at <= chrono::Utc::now() {
249 return Err(ComputerUseError::IdentityMismatch(format!(
250 "target reservation {} expired at {}",
251 reservation.reservation_id, reservation.expires_at
252 )));
253 }
254 }
255 Err(error) => {
256 return Err(ComputerUseError::IdentityMismatch(format!(
257 "target reservation {} has an unreadable expiry {:?}: {error}",
258 reservation.reservation_id, reservation.expires_at
259 )));
260 }
261 }
262
263 let target = envelope.target.as_ref().ok_or_else(|| {
264 ComputerUseError::IdentityMismatch(format!(
265 "target reservation {} was returned for action {} without target evidence",
266 reservation.reservation_id, envelope.action_id
267 ))
268 })?;
269 if reservation.scope.app_id != target.app_id {
270 return Err(mismatch(OBJECT, "scope.app_id", &target.app_id, &reservation.scope.app_id));
271 }
272 if reservation.scope.window_id != target.window_id {
273 return Err(mismatch(
274 OBJECT,
275 "scope.window_id",
276 &format!("{:?}", target.window_id),
277 &format!("{:?}", reservation.scope.window_id),
278 ));
279 }
280
281 Ok(())
282}
283
284pub fn validate_receipt(
296 receipt: &ExecutionReceipt,
297 envelope: &ActionEnvelope,
298 expected_digest: &str,
299) -> Result<(), ComputerUseError> {
300 const OBJECT: &str = "execution receipt";
301
302 if receipt.session_id != envelope.session_id {
303 return Err(mismatch(OBJECT, "session_id", &envelope.session_id, &receipt.session_id));
304 }
305 if receipt.action_id != envelope.action_id {
306 return Err(mismatch(OBJECT, "action_id", &envelope.action_id, &receipt.action_id));
307 }
308 if !expected_digest.is_empty() && receipt.action_digest != expected_digest {
309 return Err(mismatch(OBJECT, "action_digest", expected_digest, &receipt.action_digest));
310 }
311
312 Ok(())
313}