use serde::Serialize;
use crate::session::SessionSource;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum Attested {
LocalUid { uid: u32 },
Remote,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum Declared {
Unknown,
Human,
Agent { label: Option<String> },
Reconciler { label: Option<String> },
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct Shutai {
attested: Attested,
declared: Declared,
}
impl Shutai {
#[must_use]
pub const fn from_peer_uid(uid: u32) -> Self {
Self {
attested: Attested::LocalUid { uid },
declared: Declared::Unknown,
}
}
#[must_use]
pub const fn remote() -> Self {
Self {
attested: Attested::Remote,
declared: Declared::Unknown,
}
}
#[must_use]
pub fn declaring(self, declared: Declared) -> Self {
Self { declared, ..self }
}
#[must_use]
pub const fn attested(&self) -> &Attested {
&self.attested
}
#[must_use]
pub const fn declared(&self) -> &Declared {
&self.declared
}
#[must_use]
pub const fn is_automation(&self) -> bool {
matches!(
self.declared,
Declared::Agent { .. } | Declared::Reconciler { .. }
)
}
#[must_use]
pub fn session_source(&self) -> SessionSource {
match &self.declared {
Declared::Agent { label: Some(l) } | Declared::Reconciler { label: Some(l) } => {
SessionSource::Named(l.clone())
}
Declared::Agent { label: None } | Declared::Reconciler { label: None } => {
SessionSource::Agent
}
Declared::Unknown | Declared::Human => SessionSource::Human,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_fresh_local_shutai_declares_nothing() {
let s = Shutai::from_peer_uid(501);
assert_eq!(*s.attested(), Attested::LocalUid { uid: 501 });
assert_eq!(*s.declared(), Declared::Unknown);
assert!(
!s.is_automation(),
"an undeclared connection must not be assumed to be an agent"
);
}
#[test]
fn declaring_does_not_touch_the_attested_half() {
let s = Shutai::from_peer_uid(501).declaring(Declared::Agent {
label: Some("claude-code".into()),
});
assert_eq!(
*s.attested(),
Attested::LocalUid { uid: 501 },
"a declaration must never be able to rewrite what the kernel said"
);
assert!(s.is_automation());
}
#[test]
fn session_source_is_derived_rather_than_declared() {
assert_eq!(
Shutai::from_peer_uid(1).session_source(),
SessionSource::Human,
"undeclared stays Human so pre-shutai sessions keep their meaning"
);
assert_eq!(
Shutai::from_peer_uid(1)
.declaring(Declared::Agent { label: None })
.session_source(),
SessionSource::Agent
);
assert_eq!(
Shutai::from_peer_uid(1)
.declaring(Declared::Agent {
label: Some("pleme-ci".into())
})
.session_source(),
SessionSource::Named("pleme-ci".into())
);
}
#[test]
fn a_reconciler_is_automation_but_keeps_its_own_arm() {
let s = Shutai::from_peer_uid(1).declaring(Declared::Reconciler {
label: Some("ghost-session-sweeper".into()),
});
assert!(s.is_automation());
assert!(matches!(s.declared(), Declared::Reconciler { .. }));
}
#[test]
fn shutai_never_becomes_deserializable() {
let src = include_str!("shutai.rs");
let code: String = src
.lines()
.map(str::trim_start)
.filter(|l| !l.starts_with("//"))
.collect::<Vec<_>>()
.join("\n");
let code = code.split("mod tests").next().unwrap_or(&code);
assert!(
!code.contains("Deserialize"),
"`Deserialize` appeared in shutai.rs. A peer would then be able \
to SEND a Shutai, which is exactly the payload-supplied identity \
this type replaces. If this is deliberate, the module docs must \
be re-graded in the same commit."
);
assert!(
code.contains("pub struct Shutai"),
"the scan lost sight of Shutai — fix the scan, not the assert"
);
assert!(
code.contains("Serialize"),
"Shutai must still serialise OUTWARD (audit, list, MCP reads)"
);
}
}