#[cfg(feature = "alloc")]
use alloc::string::{String, ToString};
use crate::Harness;
use crate::vocabulary::{RunStatus, Vocabulary};
use super::{
AgentId, AgentType, CapabilityReadiness, DispatchRecord, DispatchState, LaneId, ProjectId,
Role, RunId, SessionId,
};
pub const ROOT_SESSION_SCHEMA: &str = "shepherd.root-session/1";
#[derive(
Clone,
Copy,
Debug,
Default,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
serde::Deserialize,
serde::Serialize,
strum::AsRefStr,
strum::Display,
strum::EnumCount,
strum::EnumIs,
strum::EnumString,
strum::IntoStaticStr,
strum::VariantArray,
strum::VariantNames,
)]
#[serde(rename_all = "snake_case")]
#[strum(ascii_case_insensitive, serialize_all = "snake_case")]
pub enum RootMode {
#[default]
#[serde(rename = "planting", alias = "planning")]
#[strum(to_string = "planting", serialize = "planning")]
Planting,
Execution,
}
impl RootMode {
#[must_use]
pub fn admits_run_status(self, status: &Vocabulary<RunStatus>) -> bool {
match (self, status.known()) {
(Self::Planting, Some(RunStatus::Planted | RunStatus::Planned)) => true,
(Self::Execution, Some(RunStatus::Executing)) => true,
_ => false,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(deny_unknown_fields)]
pub struct RootSessionBinding {
pub schema: String,
pub project_id: ProjectId,
pub run: RunId,
pub harness: Harness,
pub session_id: SessionId,
pub role: Role,
pub mode: RootMode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project_filesystem_id: Option<String>,
pub bound_at: i64,
pub expires_at: i64,
}
impl RootSessionBinding {
pub fn validate(&self) -> core::result::Result<(), IdentityError> {
if self.schema != ROOT_SESSION_SCHEMA {
return Err(IdentityError::InvalidRootBinding(self.schema.clone()));
}
if !matches!(self.role, Role::Shepherd | Role::Planter) {
return Err(IdentityError::InvalidRootBinding(self.role.to_string()));
}
if self.bound_at < 0
|| self.expires_at <= self.bound_at
|| self
.project_filesystem_id
.as_deref()
.is_some_and(|identity| !valid_project_filesystem_id(identity))
{
return Err(IdentityError::InvalidRootBinding(
"binding time range is invalid".into(),
));
}
Ok(())
}
}
fn valid_project_filesystem_id(identity: &str) -> bool {
if identity.len() == 64
&& identity
.bytes()
.all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
{
return true;
}
let mut parts = identity.split(':');
parts.next() == Some("unix")
&& parts.next().is_some_and(|part| {
!part.is_empty()
&& part.len() <= 16
&& part.bytes().all(|byte| byte.is_ascii_hexdigit())
})
&& parts.next().is_some_and(|part| {
!part.is_empty()
&& part.len() <= 16
&& part.bytes().all(|byte| byte.is_ascii_hexdigit())
})
&& parts.next().is_none()
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NativeIdentity {
pub harness: Harness,
pub project_id: ProjectId,
pub run: RunId,
pub lane: Option<LaneId>,
pub session_id: SessionId,
pub agent_id: Option<AgentId>,
pub agent_type: Option<AgentType>,
pub role: Option<Role>,
pub tool_call_id: Option<String>,
pub now: i64,
pub root_binding: Option<RootSessionBinding>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum IdentityResolution {
Root { role: Role, mode: RootMode },
Agent { agent_id: AgentId, role: Role },
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum IdentityError {
#[error("native identity fields are incomplete")]
IncompleteNativeIdentity,
#[error("no explicit root session binding exists")]
MissingRootBinding,
#[error("dispatch record missing for native agent `{agent_id}`")]
MissingRecord { agent_id: AgentId },
#[error("dispatch agent id mismatch: native `{native}`, record `{record}`")]
AgentIdMismatch { native: AgentId, record: AgentId },
#[error("dispatch agent type mismatch: native `{native}`, record `{record}`")]
AgentTypeMismatch {
native: AgentType,
record: AgentType,
},
#[error("dispatch role mismatch: native `{native}`, record `{record}`")]
RoleMismatch { native: Role, record: Role },
#[error("dispatch harness mismatch")]
HarnessMismatch,
#[error("dispatch project mismatch")]
ProjectMismatch,
#[error("wrong active run: native `{native}`, record `{record}`")]
WrongRun { native: RunId, record: RunId },
#[error("wrong lane: native `{native:?}`, record `{record:?}`")]
WrongLane {
native: Option<LaneId>,
record: Option<LaneId>,
},
#[error("dispatch session mismatch")]
SessionMismatch,
#[error("dispatch record is stale since {expired_at}")]
Stale { expired_at: i64 },
#[error("dispatch record is terminal: {state}")]
Terminal { state: DispatchState },
#[error("dispatch capability contract blocks work")]
CapabilityBlocked,
#[error("root session binding is invalid: {0}")]
InvalidRootBinding(String),
}
pub fn resolve_native_identity(
record: Option<&DispatchRecord>,
native: &NativeIdentity,
) -> core::result::Result<IdentityResolution, IdentityError> {
let Some(agent_id) = &native.agent_id else {
if native.agent_type.is_some() || native.role.is_some() {
return Err(IdentityError::IncompleteNativeIdentity);
}
return resolve_root(native);
};
let record = record.ok_or_else(|| IdentityError::MissingRecord {
agent_id: agent_id.clone(),
})?;
if agent_id != &record.agent_id {
return Err(IdentityError::AgentIdMismatch {
native: agent_id.clone(),
record: record.agent_id.clone(),
});
}
if let Some(agent_type) = &native.agent_type
&& agent_type != &record.agent_type
{
return Err(IdentityError::AgentTypeMismatch {
native: agent_type.clone(),
record: record.agent_type.clone(),
});
}
if let Some(role) = native.role
&& role != record.role
{
return Err(IdentityError::RoleMismatch {
native: role,
record: record.role,
});
}
if native.harness != record.harness {
return Err(IdentityError::HarnessMismatch);
}
if native.project_id != record.project_id {
return Err(IdentityError::ProjectMismatch);
}
if native.run != record.run {
return Err(IdentityError::WrongRun {
native: native.run.clone(),
record: record.run.clone(),
});
}
if native.lane.is_some() && native.lane != record.lane {
return Err(IdentityError::WrongLane {
native: native.lane.clone(),
record: record.lane.clone(),
});
}
if native.session_id != record.session_id {
return Err(IdentityError::SessionMismatch);
}
if record.state != DispatchState::Active {
return Err(IdentityError::Terminal {
state: record.state,
});
}
if native.now >= record.lease_expires_at {
return Err(IdentityError::Stale {
expired_at: record.lease_expires_at,
});
}
if record.capabilities.readiness() == CapabilityReadiness::Blocked {
return Err(IdentityError::CapabilityBlocked);
}
Ok(IdentityResolution::Agent {
agent_id: agent_id.clone(),
role: record.role,
})
}
fn resolve_root(
native: &NativeIdentity,
) -> core::result::Result<IdentityResolution, IdentityError> {
let binding = native
.root_binding
.as_ref()
.ok_or(IdentityError::MissingRootBinding)?;
binding.validate()?;
if binding.bound_at > native.now {
return Err(IdentityError::InvalidRootBinding(
"binding begins in the future".into(),
));
}
if binding.harness != native.harness {
return Err(IdentityError::HarnessMismatch);
}
if binding.project_id != native.project_id {
return Err(IdentityError::ProjectMismatch);
}
if binding.run != native.run {
return Err(IdentityError::WrongRun {
native: native.run.clone(),
record: binding.run.clone(),
});
}
if binding.session_id != native.session_id {
return Err(IdentityError::SessionMismatch);
}
if native.now >= binding.expires_at {
return Err(IdentityError::Stale {
expired_at: binding.expires_at,
});
}
Ok(IdentityResolution::Root {
role: binding.role,
mode: binding.mode,
})
}