//! Typed metadata records and zero-allocation validation (`SMH-SPEC-SPEC0001`). use serde::de::value::{Error as ValueError, StrDeserializer}; use serde::{Deserialize, Serialize}; use smith::error::MutationState; use smith::id::ToolCallId; use smith::structured::{AccessError, Kind, StructuredData, StructuredValue}; use crate::session::{COMPACTION_KIND, PROVIDER_HTTP_KIND, TOOL_MUTATION_KIND}; /// One tool completion's durable mutation state. #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ToolMutationMetadata { /// Identifier of the completed tool call. pub call_id: ToolCallId, /// Best-known host mutation outcome. pub mutation_state: MutationState, } impl ToolMutationMetadata { /// A typed mutation record. #[must_use] pub const fn new(call_id: ToolCallId, mutation_state: MutationState) -> Self { Self { call_id, mutation_state, } } } /// A persisted compaction summary. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CompactionMetadata { /// Summary text. pub summary: String, /// Whether the producer knew the summary was complete. #[serde( default, deserialize_with = "deserialize_complete", skip_serializing_if = "Option::is_none" )] pub complete: Option, } fn deserialize_complete<'de, D: serde::Deserializer<'de>>( deserializer: D, ) -> Result, D::Error> { bool::deserialize(deserializer).map(Some) } /// Encode a typed metadata record as structured data. /// /// # Errors /// /// Returns `ENTRY_DECODE` when the record cannot be represented as structured data. pub fn encode(record: &Record) -> smith::Result { StructuredData::from_serializable(record).map_err(|error| smith::SmithError::Session { code: smith::error::SessionCode::EntryDecode, message: format!("cannot encode metadata: {error}"), }) } /// Validate a reserved metadata record without materializing its structured tree. /// /// Unknown extension kinds remain unrestricted structured data. /// /// # Errors /// /// Returns [`AccessError::Malformed`] when a reserved kind has an invalid shape. pub fn validate<'a, Value: StructuredValue<'a>>( kind: &str, value: Value, ) -> Result<(), AccessError> { match kind { COMPACTION_KIND => compaction(value), TOOL_MUTATION_KIND => tool_mutation(value), PROVIDER_HTTP_KIND => provider_effect(value), _ => Ok(()), } } fn fields<'a, Value: StructuredValue<'a>>(value: Value) -> Result { value.fields()?.ok_or(AccessError::Malformed) } fn string<'a, Value: StructuredValue<'a>>(value: Value) -> Result<&'a str, AccessError> { match value.kind()? { Kind::String(value) => Ok(value), _ => Err(AccessError::Malformed), } } fn boolean<'a, Value: StructuredValue<'a>>(value: Value) -> Result { match value.kind()? { Kind::Bool(value) => Ok(value), _ => Err(AccessError::Malformed), } } fn unsigned<'a, Value: StructuredValue<'a>>(value: Value) -> Result { match value.kind()? { Kind::Unsigned(value) => Ok(value), Kind::Signed(value) if value >= 0 => Ok(value.cast_unsigned()), _ => Err(AccessError::Malformed), } } fn scalar<'a, Record: Deserialize<'a>>(value: &str) -> Result { Record::deserialize(StrDeserializer::::new(value)) .map_err(|_| AccessError::Malformed) } fn compaction<'a, Value: StructuredValue<'a>>(value: Value) -> Result<(), AccessError> { let mut summary = false; for field in fields(value)? { let (name, value) = field?; match name { "summary" => { string(value)?; summary = true; } "complete" => { boolean(value)?; } _ => {} } } summary.then_some(()).ok_or(AccessError::Malformed) } fn tool_mutation<'a, Value: StructuredValue<'a>>(value: Value) -> Result<(), AccessError> { let mut call_id = false; let mut mutation_state = false; for field in fields(value)? { let (name, value) = field?; match name { "call_id" if !call_id => { let _: ToolCallId = scalar(string(value)?)?; call_id = true; } "mutation_state" if !mutation_state => { let _: MutationState = scalar(string(value)?)?; mutation_state = true; } _ => return Err(AccessError::Malformed), } } (call_id && mutation_state) .then_some(()) .ok_or(AccessError::Malformed) } fn required<'a, Value: StructuredValue<'a>>( value: Value, name: &str, ) -> Result { value.field(name)?.ok_or(AccessError::Malformed) } fn exact_fields<'a, Value: StructuredValue<'a>>( value: Value, expected: &[&str], ) -> Result<(), AccessError> { let fields = fields(value)?; if fields.len() != expected.len() { return Err(AccessError::Malformed); } for field in fields { let (name, _) = field?; if !expected.contains(&name) { return Err(AccessError::Malformed); } } Ok(()) } fn provider_effect<'a, Value: StructuredValue<'a>>(value: Value) -> Result<(), AccessError> { match string(required(value, "phase")?)? { "intent" => { exact_fields( value, &["phase", "method", "url", "headers", "request_bytes"], )?; if string(required(value, "method")?)? != "POST" { return Err(AccessError::Malformed); } string(required(value, "url")?)?; usize::try_from(unsigned(required(value, "request_bytes")?)?) .map_err(|_| AccessError::Malformed)?; let headers = required(value, "headers")? .elements()? .ok_or(AccessError::Malformed)?; for header in headers { header_fields(header?)?; } } "outcome" => { exact_fields(value, &["phase", "status", "response_bytes", "outcome"])?; u16::try_from(unsigned(required(value, "status")?)?) .map_err(|_| AccessError::Malformed)?; unsigned(required(value, "response_bytes")?)?; let _: smith::provider::ProviderEffectOutcome = scalar(string(required(value, "outcome")?)?)?; } _ => return Err(AccessError::Malformed), } Ok(()) } fn header_fields<'a, Value: StructuredValue<'a>>(value: Value) -> Result<(), AccessError> { let mut name = false; let mut header_value = false; for field in fields(value)? { let (field_name, field_value) = field?; match field_name { "name" if !name => { string(field_value)?; name = true; } "value" if !header_value => { string(field_value)?; header_value = true; } "name" | "value" => return Err(AccessError::Malformed), _ => {} } } (name && header_value) .then_some(()) .ok_or(AccessError::Malformed) } #[cfg(test)] mod tests { //! SMH-SPEC-SPEC0001: Shared structured data. use super::*; #[cfg(target_arch = "x86_64")] #[test] fn metadata_layouts_are_exact() { assert_eq!(std::mem::size_of::(), 17); assert_eq!(std::mem::size_of::(), 32); } #[test] fn compaction_record_deserialization_matches_present_boolean_shape() { let legacy: CompactionMetadata = serde_json::from_str(r#"{"summary":"x"}"#).unwrap(); assert_eq!(legacy.complete, None); assert!( serde_json::from_str::(r#"{"summary":"x","complete":null}"#) .is_err() ); let complete: CompactionMetadata = serde_json::from_str(r#"{"summary":"x","complete":true}"#).unwrap(); assert_eq!(complete.complete, Some(true)); } #[test] fn known_metadata_accepts_owned_shapes() { let call_id = ToolCallId::from_u128(1); for (kind, detail) in [ ( COMPACTION_KIND, StructuredData::from_json(r#"{"summary":"legacy"}"#).unwrap(), ), ( TOOL_MUTATION_KIND, encode(&ToolMutationMetadata::new(call_id, MutationState::Applied)) .unwrap(), ), ( PROVIDER_HTTP_KIND, StructuredData::from_json(r#"{"phase":"intent","method":"POST","url":"","headers":[{"name":"n","value":"v","extension":null}],"request_bytes":0}"#).unwrap(), ), ( PROVIDER_HTTP_KIND, StructuredData::from_json(r#"{"phase":"outcome","status":0,"response_bytes":0,"outcome":"cancelled"}"#).unwrap(), ), ("extension", StructuredData::from_json(r#"[false,{"any":"shape"}]"#).unwrap()), ] { assert!(validate(kind, detail.view()).is_ok(), "rejected {kind}"); } } #[test] fn known_metadata_rejects_malformed_owned_shapes() { let call_id = ToolCallId::from_u128(1); let valid_call_id = serde_json::to_string(&call_id).unwrap(); let invalid = [ (COMPACTION_KIND, r#"{}"#.to_string()), (COMPACTION_KIND, r#"{"summary":null}"#.to_string()), (COMPACTION_KIND, r#"{"summary":"x","complete":null}"#.to_string()), (TOOL_MUTATION_KIND, r#"{}"#.to_string()), (TOOL_MUTATION_KIND, format!(r#"{{"call_id":{valid_call_id}}}"#)), (TOOL_MUTATION_KIND, r#"{"mutation_state":"applied"}"#.to_string()), (TOOL_MUTATION_KIND, format!(r#"{{"call_id":{valid_call_id},"mutation_state":"applied","extra":true}}"#)), (TOOL_MUTATION_KIND, r#"{"call_id":"bad","mutation_state":"applied"}"#.to_string()), (TOOL_MUTATION_KIND, format!(r#"{{"call_id":{valid_call_id},"mutation_state":"wrong"}}"#)), (PROVIDER_HTTP_KIND, r#"{"method":"POST","url":"x","headers":[],"request_bytes":0}"#.to_string()), (PROVIDER_HTTP_KIND, r#"{"phase":"intent","method":"GET","url":"x","headers":[],"request_bytes":0}"#.to_string()), (PROVIDER_HTTP_KIND, r#"{"phase":"intent","method":"POST","url":"x","headers":[null],"request_bytes":0}"#.to_string()), (PROVIDER_HTTP_KIND, r#"{"phase":"intent","method":"POST","url":"x","headers":[{"name":false,"value":"v"}],"request_bytes":0}"#.to_string()), (PROVIDER_HTTP_KIND, r#"{"phase":"intent","method":"POST","url":"x","headers":[],"request_bytes":18446744073709551615,"extra":true}"#.to_string()), (PROVIDER_HTTP_KIND, r#"{"phase":"outcome","status":65536,"response_bytes":0,"outcome":"completed"}"#.to_string()), (PROVIDER_HTTP_KIND, r#"{"phase":"outcome","status":0,"response_bytes":-1,"outcome":"completed"}"#.to_string()), (PROVIDER_HTTP_KIND, r#"{"phase":"outcome","status":0,"response_bytes":0,"outcome":"completed","extra":true}"#.to_string()), ]; for (kind, detail) in invalid { let detail = StructuredData::from_json(&detail).unwrap(); assert!( validate(kind, detail.view()).is_err(), "accepted {kind}: {detail:?}" ); } } }