adk_computer_use/contracts/
approval.rs1use super::action::{ActionClass, ExecutionMode};
4use serde::{Deserialize, Serialize};
5use std::collections::HashSet;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum ApprovalGrantScope {
11 ExactAction,
13 SessionOperation,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(try_from = "RawApprovalGrant", into = "RawApprovalGrant")]
26pub struct ApprovalGrant {
27 grant_id: String,
28 scope: ApprovalGrantScope,
29 principal_id: String,
30 session_id: String,
31 action_digest: String,
32 scope_digest: String,
33 policy_digest: String,
34 tool: String,
35 operation: String,
36 action_class: ActionClass,
37 mode: ExecutionMode,
38 issued_at: String,
39 expires_at: String,
40 remaining_uses: u32,
41 consumed_by_action_ids: Vec<String>,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "camelCase", deny_unknown_fields)]
46struct RawApprovalGrant {
47 grant_id: String,
48 scope: ApprovalGrantScope,
49 principal_id: String,
50 session_id: String,
51 action_digest: String,
52 scope_digest: String,
53 policy_digest: String,
54 tool: String,
55 operation: String,
56 action_class: ActionClass,
57 mode: ExecutionMode,
58 issued_at: String,
59 expires_at: String,
60 remaining_uses: u32,
61 consumed_by_action_ids: Vec<String>,
62}
63
64fn is_sha256_hex(value: &str) -> bool {
65 value.len() == 64
66 && value.bytes().all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
67}
68
69impl TryFrom<RawApprovalGrant> for ApprovalGrant {
70 type Error = String;
71
72 fn try_from(raw: RawApprovalGrant) -> Result<Self, Self::Error> {
73 if raw.grant_id.is_empty()
74 || raw.principal_id.is_empty()
75 || raw.session_id.is_empty()
76 || raw.policy_digest.is_empty()
77 || raw.issued_at.is_empty()
78 || raw.expires_at.is_empty()
79 {
80 return Err("approval grant identity, policy, and timestamps must not be empty".into());
81 }
82 if !is_sha256_hex(&raw.action_digest) || !is_sha256_hex(&raw.scope_digest) {
83 return Err("approval grant digests must be lowercase SHA-256 hex".into());
84 }
85 if raw.remaining_uses > 100 || raw.consumed_by_action_ids.len() > 100 {
86 return Err("approval grant use accounting exceeds the wire limit".into());
87 }
88 let unique: HashSet<&str> = raw.consumed_by_action_ids.iter().map(String::as_str).collect();
89 if unique.len() != raw.consumed_by_action_ids.len()
90 || raw.consumed_by_action_ids.iter().any(String::is_empty)
91 {
92 return Err("approval grant consumed action IDs must be nonempty and unique".into());
93 }
94 match raw.scope {
95 ApprovalGrantScope::ExactAction => {
96 if raw.scope_digest != raw.action_digest
97 || raw.remaining_uses > 1
98 || raw.consumed_by_action_ids.len() > 1
99 {
100 return Err("exact-action approval must bind one action digest and use".into());
101 }
102 }
103 ApprovalGrantScope::SessionOperation => {
104 if !matches!(raw.tool.as_str(), "set_value" | "fill_form")
105 || raw.operation.is_empty()
106 || raw.action_class != ActionClass::EditReversible
107 || raw.remaining_uses + raw.consumed_by_action_ids.len() as u32 > 20
108 {
109 return Err(
110 "session-operation approval must be a bounded reversible semantic edit"
111 .into(),
112 );
113 }
114 }
115 }
116 Ok(Self {
117 grant_id: raw.grant_id,
118 scope: raw.scope,
119 principal_id: raw.principal_id,
120 session_id: raw.session_id,
121 action_digest: raw.action_digest,
122 scope_digest: raw.scope_digest,
123 policy_digest: raw.policy_digest,
124 tool: raw.tool,
125 operation: raw.operation,
126 action_class: raw.action_class,
127 mode: raw.mode,
128 issued_at: raw.issued_at,
129 expires_at: raw.expires_at,
130 remaining_uses: raw.remaining_uses,
131 consumed_by_action_ids: raw.consumed_by_action_ids,
132 })
133 }
134}
135
136impl From<ApprovalGrant> for RawApprovalGrant {
137 fn from(value: ApprovalGrant) -> Self {
138 Self {
139 grant_id: value.grant_id,
140 scope: value.scope,
141 principal_id: value.principal_id,
142 session_id: value.session_id,
143 action_digest: value.action_digest,
144 scope_digest: value.scope_digest,
145 policy_digest: value.policy_digest,
146 tool: value.tool,
147 operation: value.operation,
148 action_class: value.action_class,
149 mode: value.mode,
150 issued_at: value.issued_at,
151 expires_at: value.expires_at,
152 remaining_uses: value.remaining_uses,
153 consumed_by_action_ids: value.consumed_by_action_ids,
154 }
155 }
156}
157
158impl ApprovalGrant {
159 pub fn grant_id(&self) -> &str {
161 &self.grant_id
162 }
163
164 pub fn scope(&self) -> ApprovalGrantScope {
166 self.scope
167 }
168
169 pub fn scope_digest(&self) -> &str {
171 &self.scope_digest
172 }
173
174 pub fn remaining_uses(&self) -> u32 {
176 self.remaining_uses
177 }
178}