a3s_code_core/harness_evidence/
tool_request.rs1use super::{measure, require_digest, HarnessEvidenceError};
2use serde::{Deserialize, Serialize};
3
4pub const TOOL_REQUEST_SNAPSHOT_V1_SCHEMA: &str = "a3s.code.tool-request-snapshot.v1";
5
6const TOOL_REQUEST_SNAPSHOT_DOMAIN: &str = "a3s.code.tool-request-snapshot.v1";
7const TOOL_REQUEST_ID_DOMAIN: &str = "a3s.code.tool-request-id.v1";
8const TOOL_REQUEST_NAME_DOMAIN: &str = "a3s.code.tool-request-name.v1";
9const TOOL_REQUEST_ARGUMENTS_DOMAIN: &str = "a3s.code.tool-request-arguments.v1";
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum ToolRequestOriginV1 {
19 Agent,
20 Nested,
21 HostDirectTrusted,
22 HostDirectGoverned,
23 HostDirectNestedTrusted,
24 HostDirectNestedGoverned,
25}
26
27#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
34#[serde(rename_all = "camelCase", deny_unknown_fields)]
35pub struct ToolRequestSnapshotV1 {
36 pub schema: String,
37 pub origin: ToolRequestOriginV1,
38 pub tool_id_digest: String,
39 pub tool_name_digest: String,
40 pub arguments_bytes: u64,
41 pub arguments_digest: String,
42 pub snapshot_digest: String,
43}
44
45impl ToolRequestSnapshotV1 {
46 pub(crate) fn capture(
47 tool_id: &str,
48 tool_name: &str,
49 arguments: &serde_json::Value,
50 origin: ToolRequestOriginV1,
51 ) -> Result<Self, HarnessEvidenceError> {
52 let arguments = measure(TOOL_REQUEST_ARGUMENTS_DOMAIN, arguments)?;
53 let mut snapshot = Self {
54 schema: TOOL_REQUEST_SNAPSHOT_V1_SCHEMA.to_owned(),
55 origin,
56 tool_id_digest: measure(TOOL_REQUEST_ID_DOMAIN, tool_id)?.digest,
57 tool_name_digest: measure(TOOL_REQUEST_NAME_DOMAIN, tool_name)?.digest,
58 arguments_bytes: arguments.bytes,
59 arguments_digest: arguments.digest,
60 snapshot_digest: String::new(),
61 };
62 snapshot.snapshot_digest = snapshot.expected_digest()?;
63 snapshot.validate()?;
64 Ok(snapshot)
65 }
66
67 pub fn validate(&self) -> Result<(), HarnessEvidenceError> {
68 if self.schema != TOOL_REQUEST_SNAPSHOT_V1_SCHEMA {
69 return Err(HarnessEvidenceError::UnsupportedSchema);
70 }
71 if self.arguments_bytes == 0 {
72 return Err(HarnessEvidenceError::InvalidContents(
73 "serialized Tool request arguments are non-empty",
74 ));
75 }
76 for (field, digest) in [
77 ("tool_id_digest", self.tool_id_digest.as_str()),
78 ("tool_name_digest", self.tool_name_digest.as_str()),
79 ("arguments_digest", self.arguments_digest.as_str()),
80 ("snapshot_digest", self.snapshot_digest.as_str()),
81 ] {
82 require_digest(field, digest)?;
83 }
84 if self.snapshot_digest != self.expected_digest()? {
85 return Err(HarnessEvidenceError::DigestMismatch("snapshot_digest"));
86 }
87 Ok(())
88 }
89
90 pub fn validate_against(
93 &self,
94 tool_id: &str,
95 tool_name: &str,
96 arguments: &serde_json::Value,
97 origin: ToolRequestOriginV1,
98 ) -> Result<(), HarnessEvidenceError> {
99 self.validate()?;
100 if self.origin != origin {
101 return Err(HarnessEvidenceError::InvalidContents(
102 "Tool request origins agree",
103 ));
104 }
105 if self.tool_id_digest != measure(TOOL_REQUEST_ID_DOMAIN, tool_id)?.digest {
106 return Err(HarnessEvidenceError::DigestMismatch("tool_id_digest"));
107 }
108 if self.tool_name_digest != measure(TOOL_REQUEST_NAME_DOMAIN, tool_name)?.digest {
109 return Err(HarnessEvidenceError::DigestMismatch("tool_name_digest"));
110 }
111 let arguments = measure(TOOL_REQUEST_ARGUMENTS_DOMAIN, arguments)?;
112 if self.arguments_digest != arguments.digest {
113 return Err(HarnessEvidenceError::DigestMismatch("arguments_digest"));
114 }
115 if self.arguments_bytes != arguments.bytes {
116 return Err(HarnessEvidenceError::InvalidContents(
117 "Tool request argument byte measurements agree",
118 ));
119 }
120 Ok(())
121 }
122
123 fn expected_digest(&self) -> Result<String, HarnessEvidenceError> {
124 #[derive(Serialize)]
125 struct Identity<'a> {
126 schema: &'a str,
127 origin: ToolRequestOriginV1,
128 tool_id_digest: &'a str,
129 tool_name_digest: &'a str,
130 arguments_bytes: u64,
131 arguments_digest: &'a str,
132 }
133
134 Ok(measure(
135 TOOL_REQUEST_SNAPSHOT_DOMAIN,
136 &Identity {
137 schema: &self.schema,
138 origin: self.origin,
139 tool_id_digest: &self.tool_id_digest,
140 tool_name_digest: &self.tool_name_digest,
141 arguments_bytes: self.arguments_bytes,
142 arguments_digest: &self.arguments_digest,
143 },
144 )?
145 .digest)
146 }
147}