use std::fmt;
use chio_weights::card::{ModelCard, StringSet};
use chio_weights::error::WeightsError;
pub type WeightsBindingError = WeightsError;
#[derive(Debug, Clone)]
pub struct WeightsBindingRequest<'a> {
pub loaded_weights_hash: &'a str,
pub requested_scopes: &'a StringSet,
pub requested_tools: &'a StringSet,
}
pub fn evaluate_weights_binding(
card: &ModelCard,
request: &WeightsBindingRequest<'_>,
) -> Result<(), WeightsBindingError> {
if card.weights_hash != request.loaded_weights_hash {
return Err(WeightsError::CardMismatch {
expected: card.weights_hash.clone(),
found: request.loaded_weights_hash.to_string(),
});
}
for scope in request.requested_scopes.iter() {
if !card.allowed_capability_set.contains(scope) {
return Err(WeightsError::ScopeNotSubset {
scope: scope.to_string(),
});
}
}
for tool in request.requested_tools.iter() {
if card.banned_tools.contains(tool) {
return Err(WeightsError::ToolBanned {
tool: tool.to_string(),
});
}
}
Ok(())
}
pub fn evaluate_weights_binding_with_loaded_hash<H, E>(
card: &ModelCard,
loaded_weights_hash: Result<H, E>,
requested_scopes: &StringSet,
requested_tools: &StringSet,
) -> Result<(), WeightsBindingError>
where
H: AsRef<str>,
E: fmt::Display,
{
let loaded_weights_hash = loaded_weights_hash.map_err(|error| {
WeightsError::SchemaRejected(format!("loaded weights unavailable: {error}"))
})?;
let request = WeightsBindingRequest {
loaded_weights_hash: loaded_weights_hash.as_ref(),
requested_scopes,
requested_tools,
};
evaluate_weights_binding(card, &request)
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{TimeZone, Utc};
fn fixed_issued_at() -> chrono::DateTime<Utc> {
match Utc.with_ymd_and_hms(2026, 4, 30, 12, 0, 0) {
chrono::LocalResult::Single(t) => t,
_ => panic!("fixed_issued_at fixture must construct"),
}
}
fn good_card() -> ModelCard {
let issued = fixed_issued_at();
match ModelCard::new(
"0000000000000000000000000000000000000000000000000000000000000001",
StringSet::new(["tool:read", "tool:write"]),
StringSet::new(["tool:exec"]),
"public-internet",
"https://example.com/issuer",
issued,
issued + chrono::Duration::days(30),
) {
Ok(c) => c,
Err(e) => panic!("good_card must construct: {e}"),
}
}
#[test]
fn binding_succeeds_when_all_gates_pass() {
let card = good_card();
let scopes = StringSet::new(["tool:read"]);
let tools = StringSet::new(["tool:read"]);
let req = WeightsBindingRequest {
loaded_weights_hash: "0000000000000000000000000000000000000000000000000000000000000001",
requested_scopes: &scopes,
requested_tools: &tools,
};
assert!(evaluate_weights_binding(&card, &req).is_ok());
}
#[test]
fn rejects_card_mismatch() {
let card = good_card();
let scopes = StringSet::new(["tool:read"]);
let tools = StringSet::new(["tool:read"]);
let req = WeightsBindingRequest {
loaded_weights_hash: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
requested_scopes: &scopes,
requested_tools: &tools,
};
let err = match evaluate_weights_binding(&card, &req) {
Err(e) => e,
Ok(()) => panic!("must reject"),
};
assert_eq!(err.urn(), "urn:chio:error:weights:card-mismatch");
assert!(matches!(err, WeightsError::CardMismatch { .. }));
}
#[test]
fn rejects_scope_not_subset() {
let card = good_card();
let scopes = StringSet::new(["tool:read", "tool:admin"]);
let tools = StringSet::default();
let req = WeightsBindingRequest {
loaded_weights_hash: "0000000000000000000000000000000000000000000000000000000000000001",
requested_scopes: &scopes,
requested_tools: &tools,
};
let err = match evaluate_weights_binding(&card, &req) {
Err(e) => e,
Ok(()) => panic!("must reject"),
};
assert_eq!(err.urn(), "urn:chio:error:weights:scope-not-subset");
match err {
WeightsError::ScopeNotSubset { scope } => {
assert_eq!(scope, "tool:admin");
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn rejects_banned_tool() {
let card = good_card();
let scopes = StringSet::new(["tool:read"]);
let tools = StringSet::new(["tool:read", "tool:exec"]);
let req = WeightsBindingRequest {
loaded_weights_hash: "0000000000000000000000000000000000000000000000000000000000000001",
requested_scopes: &scopes,
requested_tools: &tools,
};
let err = match evaluate_weights_binding(&card, &req) {
Err(e) => e,
Ok(()) => panic!("must reject"),
};
assert_eq!(err.urn(), "urn:chio:error:weights:tool-banned");
match err {
WeightsError::ToolBanned { tool } => assert_eq!(tool, "tool:exec"),
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn gate_order_is_card_mismatch_first() {
let card = good_card();
let scopes = StringSet::new(["tool:admin"]);
let tools = StringSet::new(["tool:exec"]);
let req = WeightsBindingRequest {
loaded_weights_hash: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
requested_scopes: &scopes,
requested_tools: &tools,
};
let err = match evaluate_weights_binding(&card, &req) {
Err(e) => e,
Ok(()) => panic!("must reject"),
};
assert!(matches!(err, WeightsError::CardMismatch { .. }));
}
#[test]
fn empty_scopes_and_tools_pass_when_hash_matches() {
let card = good_card();
let scopes = StringSet::default();
let tools = StringSet::default();
let req = WeightsBindingRequest {
loaded_weights_hash: "0000000000000000000000000000000000000000000000000000000000000001",
requested_scopes: &scopes,
requested_tools: &tools,
};
assert!(evaluate_weights_binding(&card, &req).is_ok());
}
}