#[cfg(feature = "alloc")]
use alloc::{
collections::BTreeSet,
string::{String, ToString},
};
use super::{DispatchError, DispatchResult};
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(deny_unknown_fields)]
pub struct CapabilityContract {
pub required: BTreeSet<String>,
pub optional: BTreeSet<String>,
pub forbidden: BTreeSet<String>,
}
impl CapabilityContract {
pub fn new<R, O, F, RS, OS, FS>(required: R, optional: O, forbidden: F) -> DispatchResult<Self>
where
R: IntoIterator<Item = RS>,
O: IntoIterator<Item = OS>,
F: IntoIterator<Item = FS>,
RS: AsRef<str>,
OS: AsRef<str>,
FS: AsRef<str>,
{
let contract = Self {
required: normalized_capabilities(required)?,
optional: normalized_capabilities(optional)?,
forbidden: normalized_capabilities(forbidden)?,
};
contract.validate()?;
Ok(contract)
}
pub fn validate(&self) -> DispatchResult<()> {
validate_capability_set(&self.required)?;
validate_capability_set(&self.optional)?;
validate_capability_set(&self.forbidden)?;
self.validate_disjoint()
}
#[must_use]
pub fn evaluate(&self, probe: CapabilityProbe) -> CapabilityReport {
let declared: BTreeSet<String> = self.required.union(&self.optional).cloned().collect();
let present = declared.intersection(&probe.observed).cloned().collect();
let missing_required = self.required.difference(&probe.observed).cloned().collect();
let missing_optional = self.optional.difference(&probe.observed).cloned().collect();
let missing = declared.difference(&probe.observed).cloned().collect();
let extra = probe.observed.difference(&declared).cloned().collect();
let forbidden_extra = probe
.observed
.intersection(&self.forbidden)
.cloned()
.collect();
CapabilityReport {
declared,
observed: probe.observed,
present,
missing,
missing_required,
missing_optional,
extra,
forbidden_extra,
source: probe.source,
harness_version: probe.harness_version,
provider_version: probe.provider_version,
probe_id: probe.probe_id,
observed_events: probe.observed_events,
binary_sha256: probe.binary_sha256,
package_sha256: probe.package_sha256,
probed_at: probe.probed_at,
}
}
fn validate_disjoint(&self) -> DispatchResult<()> {
for (left_name, left, right_name, right) in [
("required", &self.required, "optional", &self.optional),
("required", &self.required, "forbidden", &self.forbidden),
("optional", &self.optional, "forbidden", &self.forbidden),
] {
if let Some(capability) = left.intersection(right).next() {
return Err(DispatchError::CapabilityOverlap {
capability: capability.clone(),
left: left_name,
right: right_name,
});
}
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(deny_unknown_fields)]
pub struct CapabilityProbe {
pub probe_id: String,
pub observed: BTreeSet<String>,
pub observed_events: BTreeSet<String>,
pub source: String,
pub harness_version: String,
pub provider_version: Option<String>,
pub binary_sha256: String,
pub package_sha256: Option<String>,
pub probed_at: i64,
}
impl CapabilityProbe {
pub fn new<I, S>(
observed: I,
source: impl Into<String>,
harness_version: impl Into<String>,
provider_version: Option<&str>,
probed_at: i64,
) -> DispatchResult<Self>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let source = source.into();
let harness_version = harness_version.into();
let probe = Self {
probe_id: "legacy-probe".into(),
observed: normalized_capabilities(observed)?,
observed_events: BTreeSet::new(),
source,
harness_version,
provider_version: provider_version.map(ToString::to_string),
binary_sha256: "0000000000000000000000000000000000000000000000000000000000000000"
.into(),
package_sha256: None,
probed_at,
};
probe.validate()?;
Ok(probe)
}
pub fn validate(&self) -> DispatchResult<()> {
validate_capability_set(&self.observed)?;
if !valid_metadata(&self.source, 256) {
return Err(DispatchError::InvalidCapability(self.source.clone()));
}
if !valid_metadata(&self.harness_version, 128) {
return Err(DispatchError::InvalidCapability(
self.harness_version.clone(),
));
}
if let Some(provider_version) = &self.provider_version
&& !valid_metadata(provider_version, 128)
{
return Err(DispatchError::InvalidCapability(provider_version.clone()));
}
if self.probed_at < 0 {
return Err(DispatchError::InvalidTime(
"capability probe time cannot be negative".into(),
));
}
if self.source == "native-authenticated-probe"
&& !valid_native_evidence(
&self.probe_id,
&self.observed_events,
&self.binary_sha256,
self.package_sha256.as_deref(),
)
{
return Err(DispatchError::InvalidCapability(
"native capability probe evidence is incomplete".into(),
));
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(deny_unknown_fields)]
pub struct HarnessCapabilities {
pub schema: String,
pub harness: crate::Harness,
pub probe_id: String,
pub binary_path: String,
pub binary_sha256: String,
pub package_sha256: Option<String>,
pub harness_version: String,
pub provider_version: Option<String>,
pub observed_events: BTreeSet<String>,
pub capabilities: BTreeSet<String>,
}
impl HarnessCapabilities {
pub const SCHEMA: &'static str = "shepherd.harness-capabilities/1";
#[allow(clippy::too_many_arguments)]
pub fn authenticated(
harness: crate::Harness,
probe_id: impl Into<String>,
binary_path: impl Into<String>,
binary_sha256: impl Into<String>,
package_sha256: Option<String>,
harness_version: impl Into<String>,
provider_version: Option<String>,
observed_events: impl IntoIterator<Item = String>,
capabilities: impl IntoIterator<Item = String>,
) -> DispatchResult<Self> {
let value = Self {
schema: Self::SCHEMA.into(),
harness,
probe_id: probe_id.into(),
binary_path: binary_path.into(),
binary_sha256: binary_sha256.into(),
package_sha256,
harness_version: harness_version.into(),
provider_version,
observed_events: observed_events.into_iter().collect(),
capabilities: normalized_capabilities(capabilities)?,
};
value.validate()?;
Ok(value)
}
pub fn validate(&self) -> DispatchResult<()> {
if self.schema != Self::SCHEMA
|| !valid_metadata(&self.probe_id, 256)
|| self.probe_id == "legacy-probe"
|| !valid_absolute_path(&self.binary_path)
|| !valid_hash(&self.binary_sha256)
|| self
.package_sha256
.as_ref()
.is_some_and(|hash| !valid_hash(hash))
|| !valid_fixed_version(&self.harness_version)
|| self
.provider_version
.as_ref()
.is_some_and(|version| !valid_fixed_version(version))
|| !self.observed_events.contains("SessionStart")
|| !self.observed_events.contains("PreToolUse")
|| !self.observed_events.contains("SubagentStart")
|| !self.observed_events.contains("SubagentStop")
|| !self.capabilities.contains("skill-load")
|| !self.capabilities.contains("subagent-provider")
{
return Err(DispatchError::InvalidCapability(
"native harness capability evidence is incomplete or unauthenticated".into(),
));
}
for event in &self.observed_events {
if !valid_metadata(event, 64) {
return Err(DispatchError::InvalidEvent(event.clone()));
}
}
Ok(())
}
#[must_use]
pub fn probe(&self, probed_at: i64) -> CapabilityProbe {
CapabilityProbe {
probe_id: self.probe_id.clone(),
observed: self.capabilities.clone(),
observed_events: self.observed_events.clone(),
source: "native-authenticated-probe".into(),
harness_version: self.harness_version.clone(),
provider_version: self.provider_version.clone(),
binary_sha256: self.binary_sha256.clone(),
package_sha256: self.package_sha256.clone(),
probed_at,
}
}
}
#[derive(
Clone,
Copy,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
strum::AsRefStr,
strum::Display,
strum::EnumCount,
strum::EnumIs,
strum::EnumString,
strum::IntoStaticStr,
strum::VariantNames,
)]
#[strum(ascii_case_insensitive, serialize_all = "snake_case")]
pub enum CapabilityReadiness {
Ready,
Degraded,
Blocked,
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(deny_unknown_fields)]
pub struct CapabilityReport {
pub declared: BTreeSet<String>,
pub observed: BTreeSet<String>,
pub present: BTreeSet<String>,
pub missing: BTreeSet<String>,
pub missing_required: BTreeSet<String>,
pub missing_optional: BTreeSet<String>,
pub extra: BTreeSet<String>,
pub forbidden_extra: BTreeSet<String>,
pub source: String,
pub harness_version: String,
pub provider_version: Option<String>,
pub probe_id: String,
pub observed_events: BTreeSet<String>,
pub binary_sha256: String,
pub package_sha256: Option<String>,
pub probed_at: i64,
}
impl CapabilityReport {
#[must_use]
pub fn readiness(&self) -> CapabilityReadiness {
if !self.missing_required.is_empty() || !self.forbidden_extra.is_empty() {
CapabilityReadiness::Blocked
} else if !self.missing_optional.is_empty() {
CapabilityReadiness::Degraded
} else {
CapabilityReadiness::Ready
}
}
pub fn validate(&self) -> DispatchResult<()> {
for values in [
&self.declared,
&self.observed,
&self.present,
&self.missing,
&self.missing_required,
&self.missing_optional,
&self.extra,
&self.forbidden_extra,
] {
validate_capability_set(values)?;
}
let expected_present = self
.declared
.intersection(&self.observed)
.cloned()
.collect::<BTreeSet<_>>();
let expected_missing = self
.declared
.difference(&self.observed)
.cloned()
.collect::<BTreeSet<_>>();
let expected_extra = self
.observed
.difference(&self.declared)
.cloned()
.collect::<BTreeSet<_>>();
let partitioned_missing = self
.missing_required
.union(&self.missing_optional)
.cloned()
.collect::<BTreeSet<_>>();
let valid = self.present == expected_present
&& self.missing == expected_missing
&& self.extra == expected_extra
&& partitioned_missing == self.missing
&& self.missing_required.is_disjoint(&self.missing_optional)
&& self.forbidden_extra.is_subset(&self.extra)
&& valid_metadata(&self.source, 256)
&& valid_metadata(&self.harness_version, 128)
&& self
.provider_version
.as_ref()
.is_none_or(|version| valid_metadata(version, 128))
&& valid_metadata(&self.probe_id, 256)
&& self
.observed_events
.iter()
.all(|event| valid_metadata(event, 64))
&& valid_hash(&self.binary_sha256)
&& self
.package_sha256
.as_ref()
.is_none_or(|hash| valid_hash(hash))
&& self.probed_at >= 0
&& (self.source != "native-authenticated-probe"
|| valid_native_evidence(
&self.probe_id,
&self.observed_events,
&self.binary_sha256,
self.package_sha256.as_deref(),
));
if valid {
Ok(())
} else {
Err(DispatchError::InvalidRecord(
"capability diff is inconsistent".into(),
))
}
}
}
fn valid_metadata(value: &str, max: usize) -> bool {
(1..=max).contains(&value.len()) && !value.chars().any(char::is_control)
}
fn valid_hash(value: &str) -> bool {
value.len() == 64
&& value
.bytes()
.all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
}
fn valid_absolute_path(value: &str) -> bool {
value.starts_with('/')
|| (value.len() >= 3 && value.as_bytes()[1] == b':' && value.as_bytes()[2] == b'\\')
}
fn valid_fixed_version(value: &str) -> bool {
valid_metadata(value, 128)
&& !matches!(value, "unknown" | "ready" | "fake-ready" | "unavailable")
}
fn valid_native_evidence(
probe_id: &str,
observed_events: &BTreeSet<String>,
binary_sha256: &str,
package_sha256: Option<&str>,
) -> bool {
valid_metadata(probe_id, 256)
&& probe_id != "legacy-probe"
&& valid_hash(binary_sha256)
&& package_sha256.is_none_or(valid_hash)
&& [
"SessionStart",
"PreToolUse",
"SubagentStart",
"SubagentStop",
]
.into_iter()
.all(|event| observed_events.contains(event))
&& observed_events
.iter()
.all(|event| valid_metadata(event, 64))
}
fn normalized_capabilities<I, S>(values: I) -> DispatchResult<BTreeSet<String>>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
values
.into_iter()
.map(|value| {
let value = value.as_ref();
let bytes = value.as_bytes();
let valid = (1..=128).contains(&bytes.len())
&& bytes[0].is_ascii_lowercase()
&& bytes.iter().all(|byte| {
byte.is_ascii_lowercase()
|| byte.is_ascii_digit()
|| matches!(*byte, b'.' | b'_' | b':' | b'-')
});
if valid {
Ok(value.to_string())
} else {
Err(DispatchError::InvalidCapability(value.to_string()))
}
})
.collect()
}
fn validate_capability_set(values: &BTreeSet<String>) -> DispatchResult<()> {
for value in values {
let normalized = normalized_capabilities([value.as_str()])?;
if normalized.len() != 1 {
return Err(DispatchError::InvalidCapability(value.clone()));
}
}
Ok(())
}