#[cfg(feature = "alloc")]
use alloc::{
format,
string::{String, ToString},
};
use crate::Harness;
use super::{
AgentId, DispatchError, DispatchResult, ProfileLease, ProfileLeaseState, ProjectId, Role,
RootSessionBinding, RunId, SessionId, constant_time_digest_eq,
};
pub const SKILL_USE_CHALLENGE_SCHEMA: &str = "shepherd.skill-use-challenge/1";
pub const LOADED_SKILL_SCHEMA: &str = "shepherd.loaded-skill/1";
#[derive(
Clone,
Copy,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
serde::Deserialize,
serde::Serialize,
strum::AsRefStr,
strum::Display,
strum::EnumCount,
strum::EnumIs,
strum::EnumString,
strum::IntoStaticStr,
strum::VariantNames,
)]
#[serde(rename_all = "kebab-case")]
#[strum(ascii_case_insensitive, serialize_all = "snake_case")]
pub enum SkillUseStage {
DuringWork,
Completion,
}
#[derive(
Clone,
Copy,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
serde::Deserialize,
serde::Serialize,
strum::AsRefStr,
strum::Display,
strum::EnumCount,
strum::EnumIs,
strum::EnumString,
strum::IntoStaticStr,
strum::VariantNames,
)]
#[serde(rename_all = "kebab-case")]
#[strum(ascii_case_insensitive, serialize_all = "snake_case")]
pub enum SkillUseState {
Pending,
Attested,
Expired,
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(deny_unknown_fields)]
pub struct SkillUseRootAuthority {
pub binding: RootSessionBinding,
pub profile_lease: Option<ProfileLease>,
}
impl SkillUseRootAuthority {
pub fn validate(&self) -> DispatchResult<()> {
self.binding
.validate()
.map_err(|error| DispatchError::InvalidSkillUse(error.to_string()))?;
if self.binding.role != Role::Shepherd
|| !matches!(
self.binding.harness,
Harness::ClaudeCode | Harness::Codex | Harness::Pi
)
{
return Err(DispatchError::InvalidSkillUse(
"root skill use requires an actual Shepherd root binding".into(),
));
}
if let Some(lease) = &self.profile_lease {
lease.validate()?;
if lease.project_id != self.binding.project_id
|| lease.run != self.binding.run
|| lease.root_session_id != self.binding.session_id
|| lease.harness != self.binding.harness
|| self.binding.project_filesystem_id.as_deref()
!= Some(lease.project_filesystem_id.as_str())
|| lease.state == ProfileLeaseState::Entered
{
return Err(DispatchError::InvalidSkillUse(
"root skill use has an unactivated or foreign profile lease".into(),
));
}
}
Ok(())
}
#[must_use]
pub fn role(&self) -> Role {
if self
.profile_lease
.as_ref()
.is_some_and(|lease| lease.state == ProfileLeaseState::Active)
{
Role::Planter
} else {
Role::Shepherd
}
}
pub fn validate_live(&self, now: i64) -> DispatchResult<()> {
self.validate()?;
if now < self.binding.bound_at || now >= self.binding.expires_at {
return Err(DispatchError::InvalidSkillUse(
"root skill-use authority is not live".into(),
));
}
if self
.profile_lease
.as_ref()
.is_some_and(|lease| now < lease.entered_at)
{
return Err(DispatchError::InvalidSkillUse(
"root skill-use authority predates its retained profile lease".into(),
));
}
if let Some(lease) = &self.profile_lease
&& lease.state == ProfileLeaseState::Active
{
lease.validate_for_root(&self.binding, now)?;
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SkillUsePrepare {
pub project_id: ProjectId,
pub run: RunId,
pub dispatch_id: Option<AgentId>,
pub root_authority: Option<SkillUseRootAuthority>,
pub session_id: SessionId,
pub target: Harness,
pub role: Role,
pub startup_skill: String,
pub skill: String,
pub stage: SkillUseStage,
pub installed_carrier_path: String,
pub candidate_sha256: [u8; 32],
pub carrier_sha256: [u8; 32],
pub compiler_tree_sha256: [u8; 32],
pub skill_bundle_sha256: [u8; 32],
pub nonce_sha256: [u8; 32],
pub prepared_at: i64,
pub expires_at: i64,
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(deny_unknown_fields)]
pub struct SkillUseChallenge {
pub schema: String,
pub project_id: ProjectId,
pub run: RunId,
pub dispatch_id: Option<AgentId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub root_authority: Option<SkillUseRootAuthority>,
pub session_id: SessionId,
pub target: Harness,
pub role: Role,
pub startup_skill: String,
pub skill: String,
pub stage: SkillUseStage,
pub installed_carrier_path: String,
#[serde(with = "super::pending::digest_serde")]
pub candidate_sha256: [u8; 32],
#[serde(with = "super::pending::digest_serde")]
pub carrier_sha256: [u8; 32],
#[serde(with = "super::pending::digest_serde")]
pub compiler_tree_sha256: [u8; 32],
#[serde(with = "super::pending::digest_serde")]
pub skill_bundle_sha256: [u8; 32],
#[serde(with = "super::pending::digest_serde")]
pub nonce_sha256: [u8; 32],
pub prepared_at: i64,
pub expires_at: i64,
pub state: SkillUseState,
pub attested_at: Option<i64>,
}
impl SkillUseChallenge {
pub fn prepare(input: SkillUsePrepare) -> DispatchResult<Self> {
if !allowed_skill_use(input.role, &input.startup_skill, &input.skill, input.stage) {
return Err(DispatchError::InvalidSkillUse(format!(
"role `{}` with startup `{}` cannot load `{}` at {:?}",
input.role, input.startup_skill, input.skill, input.stage
)));
}
if input.prepared_at < 0
|| input.expires_at <= input.prepared_at
|| !valid_installed_path(&input.installed_carrier_path)
|| input.candidate_sha256 == [0; 32]
|| input.carrier_sha256 == [0; 32]
|| input.compiler_tree_sha256 == [0; 32]
|| input.skill_bundle_sha256 == [0; 32]
|| input.nonce_sha256 == [0; 32]
{
return Err(DispatchError::InvalidSkillUse(
"skill-use identity, digest, path, or lease is invalid".into(),
));
}
let value = Self {
schema: SKILL_USE_CHALLENGE_SCHEMA.into(),
project_id: input.project_id,
run: input.run,
dispatch_id: input.dispatch_id,
root_authority: input.root_authority,
session_id: input.session_id,
target: input.target,
role: input.role,
startup_skill: input.startup_skill,
skill: input.skill,
stage: input.stage,
installed_carrier_path: input.installed_carrier_path,
candidate_sha256: input.candidate_sha256,
carrier_sha256: input.carrier_sha256,
compiler_tree_sha256: input.compiler_tree_sha256,
skill_bundle_sha256: input.skill_bundle_sha256,
nonce_sha256: input.nonce_sha256,
prepared_at: input.prepared_at,
expires_at: input.expires_at,
state: SkillUseState::Pending,
attested_at: None,
};
value.validate()?;
Ok(value)
}
pub fn validate(&self) -> DispatchResult<()> {
if self.schema != SKILL_USE_CHALLENGE_SCHEMA
|| !allowed_skill_use(self.role, &self.startup_skill, &self.skill, self.stage)
|| self.prepared_at < 0
|| self.expires_at <= self.prepared_at
|| !valid_installed_path(&self.installed_carrier_path)
|| self.candidate_sha256 == [0; 32]
|| self.carrier_sha256 == [0; 32]
|| self.compiler_tree_sha256 == [0; 32]
|| self.skill_bundle_sha256 == [0; 32]
|| self.nonce_sha256 == [0; 32]
|| match self.state {
SkillUseState::Pending | SkillUseState::Expired => self.attested_at.is_some(),
SkillUseState::Attested => self
.attested_at
.is_none_or(|value| value < self.prepared_at || value >= self.expires_at),
}
{
return Err(DispatchError::InvalidSkillUse(
"persisted skill-use challenge is inconsistent".into(),
));
}
match (&self.dispatch_id, &self.root_authority) {
(Some(_), None) if !matches!(self.role, Role::Shepherd | Role::Planter) => {}
(None, Some(authority)) => {
authority.validate_live(self.prepared_at)?;
if self.project_id != authority.binding.project_id
|| self.run != authority.binding.run
|| self.session_id != authority.binding.session_id
|| self.target != authority.binding.harness
|| self.role != authority.role()
|| self.expires_at > authority.binding.expires_at
|| authority.profile_lease.as_ref().is_some_and(|lease| {
lease.state == ProfileLeaseState::Active
&& (self.expires_at > lease.expires_at
|| !constant_time_digest_eq(
&self.candidate_sha256,
&lease.expected_attachment.candidate_sha256,
))
})
{
return Err(DispatchError::InvalidSkillUse(
"root challenge differs from its exact root/profile authority".into(),
));
}
}
_ => {
return Err(DispatchError::InvalidSkillUse(
"skill use requires exactly one child or Native root authority".into(),
));
}
}
Ok(())
}
pub fn attest(&mut self, attestation: &LoadedSkillAttestation, now: i64) -> DispatchResult<()> {
self.validate()?;
if self.state != SkillUseState::Pending {
return Err(DispatchError::SkillUseReplay);
}
if now >= self.expires_at {
self.state = SkillUseState::Expired;
return Err(DispatchError::SkillUseExpired {
expires_at: self.expires_at,
});
}
attestation.validate_against(self, now)?;
self.state = SkillUseState::Attested;
self.attested_at = Some(now);
self.validate()
}
pub fn expire(&mut self, now: i64) -> DispatchResult<()> {
self.validate()?;
if self.state != SkillUseState::Pending {
return Err(DispatchError::SkillUseReplay);
}
if now < self.expires_at {
return Err(DispatchError::InvalidSkillUse(
"skill-use challenge has not reached its expiry".into(),
));
}
self.state = SkillUseState::Expired;
self.validate()
}
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(deny_unknown_fields)]
pub struct LoadedSkillAttestation {
pub schema: String,
pub project_id: ProjectId,
pub run: RunId,
pub dispatch_id: Option<AgentId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub root_authority: Option<SkillUseRootAuthority>,
pub session_id: SessionId,
pub target: Harness,
pub role: Role,
pub startup_skill: String,
pub skill: String,
pub stage: SkillUseStage,
pub installed_carrier_path: String,
#[serde(with = "super::pending::digest_serde")]
pub candidate_sha256: [u8; 32],
#[serde(with = "super::pending::digest_serde")]
pub carrier_sha256: [u8; 32],
#[serde(with = "super::pending::digest_serde")]
pub compiler_tree_sha256: [u8; 32],
#[serde(with = "super::pending::digest_serde")]
pub skill_bundle_sha256: [u8; 32],
#[serde(with = "super::pending::digest_serde")]
pub nonce_sha256: [u8; 32],
pub loaded_at: i64,
}
impl LoadedSkillAttestation {
#[must_use]
pub fn from_challenge(challenge: &SkillUseChallenge, loaded_at: i64) -> Self {
Self {
schema: LOADED_SKILL_SCHEMA.into(),
project_id: challenge.project_id.clone(),
run: challenge.run.clone(),
dispatch_id: challenge.dispatch_id.clone(),
root_authority: challenge.root_authority.clone(),
session_id: challenge.session_id.clone(),
target: challenge.target,
role: challenge.role,
startup_skill: challenge.startup_skill.clone(),
skill: challenge.skill.clone(),
stage: challenge.stage,
installed_carrier_path: challenge.installed_carrier_path.clone(),
candidate_sha256: challenge.candidate_sha256,
carrier_sha256: challenge.carrier_sha256,
compiler_tree_sha256: challenge.compiler_tree_sha256,
skill_bundle_sha256: challenge.skill_bundle_sha256,
nonce_sha256: challenge.nonce_sha256,
loaded_at,
}
}
pub fn validate_against(&self, challenge: &SkillUseChallenge, now: i64) -> DispatchResult<()> {
challenge.validate()?;
if self.schema != LOADED_SKILL_SCHEMA
|| self.loaded_at != now
|| self.loaded_at < challenge.prepared_at
|| self.loaded_at >= challenge.expires_at
|| self.project_id != challenge.project_id
|| self.run != challenge.run
|| self.dispatch_id != challenge.dispatch_id
|| self.root_authority != challenge.root_authority
|| self.session_id != challenge.session_id
|| self.target != challenge.target
|| self.role != challenge.role
|| self.startup_skill != challenge.startup_skill
|| self.skill != challenge.skill
|| self.stage != challenge.stage
|| self.installed_carrier_path != challenge.installed_carrier_path
|| !constant_time_digest_eq(&self.candidate_sha256, &challenge.candidate_sha256)
|| !constant_time_digest_eq(&self.carrier_sha256, &challenge.carrier_sha256)
|| !constant_time_digest_eq(&self.compiler_tree_sha256, &challenge.compiler_tree_sha256)
|| !constant_time_digest_eq(&self.skill_bundle_sha256, &challenge.skill_bundle_sha256)
|| !constant_time_digest_eq(&self.nonce_sha256, &challenge.nonce_sha256)
{
return Err(DispatchError::AttachmentMismatch(
"loaded skill does not match the Native single-use challenge".into(),
));
}
Ok(())
}
}
#[must_use]
pub fn allowed_skill_use(
role: Role,
startup_skill: &str,
skill: &str,
stage: SkillUseStage,
) -> bool {
if skill == "debugging" {
return role == Role::Coder
&& startup_skill == "implementing"
&& stage == SkillUseStage::DuringWork;
}
if skill != "verification" || stage != SkillUseStage::Completion {
return false;
}
matches!(
(role, startup_skill),
(Role::Shepherd, "shepherd")
| (Role::Planter, "planting")
| (Role::Engineer, "planning")
| (Role::Conductor, "lane-execution")
| (Role::Coder, "implementing")
| (Role::Worker, "artifact-work")
| (Role::Auditor | Role::Critic, "reviewing")
| (Role::Discovery, "researching")
)
}
fn valid_installed_path(value: &str) -> bool {
let bytes = value.as_bytes();
!value.is_empty()
&& value.len() <= 4_096
&& !value.contains(['\0', '\n', '\r'])
&& (value.starts_with('/')
|| (bytes.len() >= 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& matches!(bytes[2], b'/' | b'\\')))
}