a3s_code_core/capability/
knowledge_surface_binding.rs1use std::fmt;
2
3use sha2::{Digest, Sha256};
4use thiserror::Error;
5
6use super::{CapabilitySetError, Sha256Digest};
7
8pub const KNOWLEDGE_SURFACE_BINDING_SCHEMA: &str = "a3s.code.knowledge-surface-binding.v1";
9pub const MAX_KNOWLEDGE_SURFACE_PROJECTIONS: usize = 256;
10
11const MAX_KNOWLEDGE_PUBLIC_NAME_BYTES: usize = 256;
12const MAX_KNOWLEDGE_FORMAT_VERSION_BYTES: usize = 64;
13const KNOWLEDGE_SURFACE_DIGEST_PREFIX: &[u8] = b"a3s-code-knowledge-surface\0";
14
15#[derive(Clone, Debug, Eq, PartialEq)]
23pub struct KnowledgeSurfaceBindingSpec {
24 pub public_name: String,
25 pub format_version: String,
26 pub content_digest: Sha256Digest,
27 pub projection_digests: Vec<Sha256Digest>,
28}
29
30#[derive(Clone, Eq, PartialEq)]
37pub struct KnowledgeSurfaceBinding {
38 public_name: Box<str>,
39 format_version: Box<str>,
40 content_digest: Sha256Digest,
41 projection_digests: Box<[Sha256Digest]>,
42 surface_digest: Sha256Digest,
43}
44
45impl KnowledgeSurfaceBinding {
46 pub fn new(
47 mut spec: KnowledgeSurfaceBindingSpec,
48 ) -> Result<Self, KnowledgeSurfaceBindingError> {
49 validate_required_text(
50 "public_name",
51 &spec.public_name,
52 MAX_KNOWLEDGE_PUBLIC_NAME_BYTES,
53 )?;
54 validate_required_text(
55 "format_version",
56 &spec.format_version,
57 MAX_KNOWLEDGE_FORMAT_VERSION_BYTES,
58 )?;
59 if spec.projection_digests.is_empty() {
60 return Err(KnowledgeSurfaceBindingError::MissingProjectionEvidence);
61 }
62 if spec.projection_digests.len() > MAX_KNOWLEDGE_SURFACE_PROJECTIONS {
63 return Err(KnowledgeSurfaceBindingError::ProjectionCountExceeded {
64 max: MAX_KNOWLEDGE_SURFACE_PROJECTIONS,
65 });
66 }
67 spec.projection_digests.sort();
68 if spec
69 .projection_digests
70 .windows(2)
71 .any(|pair| pair[0] == pair[1])
72 {
73 return Err(KnowledgeSurfaceBindingError::DuplicateProjectionDigest);
74 }
75 let surface_digest = binding_digest(&spec)?;
76 Ok(Self {
77 public_name: spec.public_name.into_boxed_str(),
78 format_version: spec.format_version.into_boxed_str(),
79 content_digest: spec.content_digest,
80 projection_digests: spec.projection_digests.into_boxed_slice(),
81 surface_digest,
82 })
83 }
84
85 pub fn public_name(&self) -> &str {
86 &self.public_name
87 }
88
89 pub fn format_version(&self) -> &str {
90 &self.format_version
91 }
92
93 pub fn content_digest(&self) -> &Sha256Digest {
94 &self.content_digest
95 }
96
97 pub fn projection_digests(&self) -> &[Sha256Digest] {
98 &self.projection_digests
99 }
100
101 pub fn surface_digest(&self) -> &Sha256Digest {
102 &self.surface_digest
103 }
104}
105
106impl fmt::Debug for KnowledgeSurfaceBinding {
107 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
108 formatter
109 .debug_struct("KnowledgeSurfaceBinding")
110 .field("public_name", &self.public_name)
111 .field("format_version", &self.format_version)
112 .field("content_digest", &self.content_digest)
113 .field("projection_count", &self.projection_digests.len())
114 .field("surface_digest", &self.surface_digest)
115 .finish()
116 }
117}
118
119#[derive(Clone, Debug, Eq, Error, PartialEq)]
120pub enum KnowledgeSurfaceBindingError {
121 #[error("Knowledge surface field '{field}' is invalid: {reason}")]
122 InvalidText {
123 field: &'static str,
124 reason: &'static str,
125 },
126 #[error("Knowledge surface field '{field}' exceeds its byte bound of {max}")]
127 TextTooLarge { field: &'static str, max: usize },
128 #[error("Knowledge surface readiness requires at least one exact projection digest")]
129 MissingProjectionEvidence,
130 #[error("Knowledge surface readiness contains more than {max} projection digests")]
131 ProjectionCountExceeded { max: usize },
132 #[error("Knowledge surface readiness repeats one exact projection digest")]
133 DuplicateProjectionDigest,
134 #[error("Knowledge surface digest construction violated the canonical SHA-256 invariant")]
135 DigestInvariant,
136}
137
138fn validate_required_text(
139 field: &'static str,
140 value: &str,
141 max: usize,
142) -> Result<(), KnowledgeSurfaceBindingError> {
143 if value.is_empty() || value.trim() != value || value.chars().any(char::is_control) {
144 return Err(KnowledgeSurfaceBindingError::InvalidText {
145 field,
146 reason: "it is empty, padded, or contains control characters",
147 });
148 }
149 if value.len() > max {
150 return Err(KnowledgeSurfaceBindingError::TextTooLarge { field, max });
151 }
152 Ok(())
153}
154
155fn binding_digest(
156 spec: &KnowledgeSurfaceBindingSpec,
157) -> Result<Sha256Digest, KnowledgeSurfaceBindingError> {
158 let mut hasher = Sha256::new();
159 hasher.update(KNOWLEDGE_SURFACE_DIGEST_PREFIX);
160 hash_field(&mut hasher, KNOWLEDGE_SURFACE_BINDING_SCHEMA.as_bytes());
161 hash_field(&mut hasher, spec.public_name.as_bytes());
162 hash_field(&mut hasher, spec.format_version.as_bytes());
163 hash_field(&mut hasher, spec.content_digest.as_str().as_bytes());
164 hash_field(
165 &mut hasher,
166 &(spec.projection_digests.len() as u64).to_be_bytes(),
167 );
168 for digest in &spec.projection_digests {
169 hash_field(&mut hasher, digest.as_str().as_bytes());
170 }
171 Sha256Digest::new(format!("sha256:{:x}", hasher.finalize())).map_err(map_digest_error)
172}
173
174fn hash_field(hasher: &mut Sha256, value: &[u8]) {
175 hasher.update((value.len() as u64).to_be_bytes());
176 hasher.update(value);
177}
178
179fn map_digest_error(_error: CapabilitySetError) -> KnowledgeSurfaceBindingError {
180 KnowledgeSurfaceBindingError::DigestInvariant
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 fn digest(byte: char) -> Sha256Digest {
188 Sha256Digest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap()
189 }
190
191 #[test]
192 fn projection_evidence_is_canonical_and_bounded() {
193 let binding = KnowledgeSurfaceBinding::new(KnowledgeSurfaceBindingSpec {
194 public_name: "research:domain".to_owned(),
195 format_version: "0.2".to_owned(),
196 content_digest: digest('a'),
197 projection_digests: vec![digest('c'), digest('b')],
198 })
199 .unwrap();
200 assert_eq!(binding.projection_digests(), &[digest('b'), digest('c')]);
201
202 assert!(matches!(
203 KnowledgeSurfaceBinding::new(KnowledgeSurfaceBindingSpec {
204 public_name: "research:domain".to_owned(),
205 format_version: "0.2".to_owned(),
206 content_digest: digest('a'),
207 projection_digests: vec![digest('b'), digest('b')],
208 }),
209 Err(KnowledgeSurfaceBindingError::DuplicateProjectionDigest)
210 ));
211 assert!(matches!(
212 KnowledgeSurfaceBinding::new(KnowledgeSurfaceBindingSpec {
213 public_name: "research:domain".to_owned(),
214 format_version: "0.2".to_owned(),
215 content_digest: digest('a'),
216 projection_digests: Vec::new(),
217 }),
218 Err(KnowledgeSurfaceBindingError::MissingProjectionEvidence)
219 ));
220 }
221}