diff --git a/crates/didbot-agentd/src/protocol.rs b/crates/didbot-agentd/src/protocol.rs index 1d3007fe..1147f9ab 100644 --- a/crates/didbot-agentd/src/protocol.rs +++ b/crates/didbot-agentd/src/protocol.rs @@ -20,7 +20,12 @@ use serde::{Deserialize, Serialize}; pub const VERSION: u32 = 1; /// What each caller wants. -#[derive(Debug, Clone, Serialize, Deserialize)] +/// +/// Serialized with an `asks` tag. Read without needing one: a sender that +/// predates the tag is asking the only thing there was to ask, and refusing +/// it would make every adapter update a flag day. That is the skew [`VERSION`] +/// exists to survive, and a missing tag is the cheapest kind. +#[derive(Debug, Clone, Serialize)] #[serde(tag = "asks", rename_all = "snake_case")] pub enum Message { /// A hook saying what the harness just did. @@ -29,6 +34,24 @@ pub enum Message { Confirm(Confirm), } +impl<'de> Deserialize<'de> for Message { + fn deserialize>(deserializer: D) -> Result { + use serde::de::Error; + let value = serde_json::Value::deserialize(deserializer)?; + match value.get("asks").and_then(serde_json::Value::as_str) { + None | Some("report") => serde_json::from_value(value) + .map(Message::Report) + .map_err(D::Error::custom), + Some("confirm") => serde_json::from_value(value) + .map(Message::Confirm) + .map_err(D::Error::custom), + Some(other) => Err(D::Error::custom(format!( + "this daemon does not know how to `{other}`" + ))), + } + } +} + /// What the adapter observed, in the vocabulary the daemon works in. /// /// Deliberately not the harness's own event names: the daemon has no opinion @@ -198,6 +221,26 @@ mod tests { ); } + #[test] + fn a_sender_that_predates_the_tag_is_still_read() { + // What every adapter sent before there was more than one thing to ask. + let line = r#"{"version":1,"observed":"acted","session":"s1","context":"a1"}"#; + let message: Message = serde_json::from_str(line).unwrap(); + let Message::Report(report) = message else { + panic!("a report"); + }; + assert_eq!(report.observed, Observed::Acted); + } + + #[test] + fn something_this_daemon_cannot_do_is_named_rather_than_guessed() { + let line = r#"{"asks":"revoke","version":1}"#; + let err = serde_json::from_str::(line) + .unwrap_err() + .to_string(); + assert!(err.contains("revoke"), "{err}"); + } + #[test] fn an_unknown_observation_is_refused_rather_than_guessed() { let line = r#"{"asks":"report","version":1,"observed":"vanished","session":"s1"}"#;