use std::collections::BTreeMap;
use std::sync::{Arc, RwLock};
use meerkat_core::{
CompiledApplicationToolPolicy, CompiledMemberToolAction, PolicyDigest,
PolicyEvaluationProvenance, PolicyId, PolicyProviderGeneration, PolicyProviderId,
ToolConsequenceDenial, ToolConsequenceFailure, ToolConsequenceNarrowingPolicy,
ToolConsequencePolicySnapshot, ToolConsequenceRequest, ToolConsequenceVerdict,
};
pub const DENIAL_CODE_NO_GRANT: &str = "application_tool_policy_no_grant";
#[derive(Debug)]
pub struct CompiledPolicySnapshot {
policy: CompiledApplicationToolPolicy,
}
impl CompiledPolicySnapshot {
pub fn new(policy: CompiledApplicationToolPolicy) -> Self {
Self { policy }
}
pub fn policy(&self) -> &CompiledApplicationToolPolicy {
&self.policy
}
}
impl ToolConsequencePolicySnapshot for CompiledPolicySnapshot {
fn provenance(&self) -> PolicyEvaluationProvenance {
PolicyEvaluationProvenance {
revision: self.policy.revision,
digest: self.policy.policy_digest.clone(),
}
}
fn evaluate(&self, request: &ToolConsequenceRequest) -> ToolConsequenceVerdict {
let grant = self
.policy
.members
.iter()
.find(|member| member.member_identity == request.member.member)
.and_then(|member| {
member
.grants
.iter()
.find(|grant| grant.tool_name == request.tool_name.as_str())
});
match grant {
Some(grant) => match grant.action {
CompiledMemberToolAction::Invoke => ToolConsequenceVerdict::Allow,
},
None if self.policy.default_deny => {
ToolConsequenceVerdict::Deny(ToolConsequenceDenial::new(
DENIAL_CODE_NO_GRANT,
format!(
"no exact grant for tool '{}' and member '{}' in policy '{}' revision {}",
request.tool_name.as_str(),
request.member.member,
self.policy.policy_id,
self.policy.revision.0
),
))
}
None => ToolConsequenceVerdict::Allow,
}
}
}
#[derive(Debug)]
struct AcceptedPolicy {
revision: u64,
digest: PolicyDigest,
snapshot: Arc<CompiledPolicySnapshot>,
}
#[derive(Debug)]
pub struct CompiledPolicyProvider {
provider_id: PolicyProviderId,
generation: PolicyProviderGeneration,
accepted: RwLock<BTreeMap<PolicyId, AcceptedPolicy>>,
}
impl CompiledPolicyProvider {
pub fn new(provider_id: PolicyProviderId, generation: PolicyProviderGeneration) -> Self {
Self {
provider_id,
generation,
accepted: RwLock::new(BTreeMap::new()),
}
}
pub fn accept(
&self,
policy: CompiledApplicationToolPolicy,
) -> Result<(), ToolConsequenceFailure> {
if policy.provider_id != self.provider_id {
return Err(ToolConsequenceFailure::EvaluationFailed {
reason: format!(
"compiled policy names provider '{}' but this provider is '{}'",
policy.provider_id, self.provider_id
),
});
}
let mut accepted = self
.accepted
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(current) = accepted.get(&policy.policy_id) {
if policy.revision.0 < current.revision {
return Err(ToolConsequenceFailure::RevisionRollback {
provider_id: self.provider_id.clone(),
policy_id: policy.policy_id.clone(),
accepted_revision: current.revision,
observed_revision: policy.revision.0,
});
}
if policy.revision.0 == current.revision && policy.policy_digest != current.digest {
return Err(ToolConsequenceFailure::RevisionDigestConflict {
provider_id: self.provider_id.clone(),
policy_id: policy.policy_id.clone(),
revision: policy.revision.0,
});
}
}
let policy_id = policy.policy_id.clone();
let revision = policy.revision.0;
let digest = policy.policy_digest.clone();
accepted.insert(
policy_id,
AcceptedPolicy {
revision,
digest,
snapshot: Arc::new(CompiledPolicySnapshot::new(policy)),
},
);
Ok(())
}
pub fn accept_canonical_json(&self, bytes: &[u8]) -> Result<(), ToolConsequenceFailure> {
let policy =
CompiledApplicationToolPolicy::parse_canonical_json(bytes).map_err(|error| {
ToolConsequenceFailure::EvaluationFailed {
reason: format!("compiled application tool policy rejected: {error}"),
}
})?;
self.accept(policy)
}
pub fn accepted_revision(&self, policy_id: &PolicyId) -> Option<u64> {
self.accepted
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(policy_id)
.map(|accepted| accepted.revision)
}
}
pub fn compiled_policy_payloads_from_init_params(
params: &serde_json::Value,
) -> Result<Vec<String>, ToolConsequenceFailure> {
match params.get("application_tool_policies") {
None => Ok(Vec::new()),
Some(value) => serde_json::from_value(value.clone()).map_err(|error| {
ToolConsequenceFailure::EvaluationFailed {
reason: format!("application_tool_policies is malformed: {error}"),
}
}),
}
}
pub fn providers_from_canonical_payloads<I, S>(
payloads: I,
) -> Result<Vec<Arc<CompiledPolicyProvider>>, ToolConsequenceFailure>
where
I: IntoIterator<Item = S>,
S: AsRef<[u8]>,
{
let mut by_provider: BTreeMap<PolicyProviderId, Arc<CompiledPolicyProvider>> = BTreeMap::new();
for payload in payloads {
let policy = CompiledApplicationToolPolicy::parse_canonical_json(payload.as_ref())
.map_err(|error| ToolConsequenceFailure::EvaluationFailed {
reason: format!("compiled application tool policy rejected: {error}"),
})?;
let provider_id = policy.provider_id.clone();
let provider = by_provider.entry(provider_id.clone()).or_insert_with(|| {
Arc::new(CompiledPolicyProvider::new(
provider_id,
PolicyProviderGeneration(1),
))
});
provider.accept(policy)?;
}
Ok(by_provider.into_values().collect())
}
impl ToolConsequenceNarrowingPolicy for CompiledPolicyProvider {
fn provider_id(&self) -> &PolicyProviderId {
&self.provider_id
}
fn generation(&self) -> PolicyProviderGeneration {
self.generation
}
fn snapshot(
&self,
policy_id: &PolicyId,
) -> Result<Arc<dyn ToolConsequencePolicySnapshot>, ToolConsequenceFailure> {
let accepted = self
.accepted
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
match accepted.get(policy_id) {
Some(accepted) => Ok(Arc::clone(&accepted.snapshot) as Arc<_>),
None => Err(ToolConsequenceFailure::PolicyMissing {
provider_id: self.provider_id.clone(),
policy_id: policy_id.clone(),
}),
}
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use meerkat_core::{
CompiledMemberToolGrant, CompiledMemberToolGrants, CompiledPolicySourceProvenance,
CompiledToolConsequence, MobMemberBinding, PolicyEvaluationSupervisorConfig,
PolicyRevision, ToolConsequencePolicyRegistry, ToolName,
};
fn provider_id() -> PolicyProviderId {
PolicyProviderId::new("mobkit-test-provider").expect("provider id")
}
fn policy_id() -> PolicyId {
PolicyId::new("fleet-baseline").expect("policy id")
}
fn source() -> CompiledPolicySourceProvenance {
CompiledPolicySourceProvenance {
source_id: "mobkit-test-source".to_string(),
source_digest: PolicyDigest::from_canonical_bytes(b"mobkit-test-source"),
}
}
fn grants_for(member: &str, tool: &str) -> Vec<CompiledMemberToolGrants> {
vec![CompiledMemberToolGrants {
member_identity: member.to_string(),
grants: vec![CompiledMemberToolGrant {
tool_name: tool.to_string(),
action: CompiledMemberToolAction::Invoke,
consequence: CompiledToolConsequence::R2,
}],
}]
}
fn policy(revision: u64, member: &str, tool: &str) -> CompiledApplicationToolPolicy {
CompiledApplicationToolPolicy::new(
provider_id(),
policy_id(),
PolicyRevision(revision),
source(),
grants_for(member, tool),
)
.expect("compiled policy should validate")
}
fn request(member: &str, tool: &str) -> ToolConsequenceRequest {
ToolConsequenceRequest {
member: MobMemberBinding {
mob_id: "mob".to_string(),
role: "worker".to_string(),
member: member.to_string(),
},
tool_name: ToolName::new(tool),
arguments_json: "{}".to_string(),
arguments_digest: "sha256:test".to_string(),
run_id: None,
tool_call_id: "call-1".to_string(),
provider_id: provider_id(),
policy_id: policy_id(),
}
}
fn installed(policy: CompiledApplicationToolPolicy) -> Arc<CompiledPolicyProvider> {
let provider = Arc::new(CompiledPolicyProvider::new(
provider_id(),
PolicyProviderGeneration(1),
));
provider.accept(policy).expect("first accept");
provider
}
#[test]
fn an_exact_member_and_tool_grant_allows() {
let provider = installed(policy(1, "member-a", "shell"));
let snapshot = provider.snapshot(&policy_id()).expect("snapshot");
assert!(matches!(
snapshot.evaluate(&request("member-a", "shell")),
ToolConsequenceVerdict::Allow
));
}
#[test]
fn a_missing_grant_denies_under_the_artifacts_own_default_deny() {
let policy = policy(1, "member-a", "shell");
assert!(
policy.default_deny,
"a policy minted through new() must be default-deny"
);
let provider = installed(policy);
let snapshot = provider.snapshot(&policy_id()).expect("snapshot");
match snapshot.evaluate(&request("member-a", "network")) {
ToolConsequenceVerdict::Deny(denial) => {
assert_eq!(denial.code, DENIAL_CODE_NO_GRANT);
assert!(denial.message.contains("network"), "{}", denial.message);
}
other => panic!("expected a typed denial, got {other:?}"),
}
assert!(matches!(
snapshot.evaluate(&request("member-b", "shell")),
ToolConsequenceVerdict::Deny(_)
));
}
#[test]
fn the_provider_refuses_a_revision_rollback() {
let provider = installed(policy(7, "member-a", "shell"));
let error = provider
.accept(policy(6, "member-a", "shell"))
.expect_err("a lower revision must be refused");
match error {
ToolConsequenceFailure::RevisionRollback {
accepted_revision,
observed_revision,
..
} => {
assert_eq!(accepted_revision, 7);
assert_eq!(observed_revision, 6);
}
other => panic!("expected RevisionRollback, got {other:?}"),
}
assert_eq!(provider.accepted_revision(&policy_id()), Some(7));
}
#[test]
fn the_provider_refuses_a_reused_revision_whose_content_changed() {
let provider = installed(policy(7, "member-a", "shell"));
let error = provider
.accept(policy(7, "member-a", "network"))
.expect_err("same revision with different bytes must be refused");
assert!(
matches!(
error,
ToolConsequenceFailure::RevisionDigestConflict { revision: 7, .. }
),
"expected RevisionDigestConflict, got {error:?}"
);
let snapshot = provider.snapshot(&policy_id()).expect("snapshot");
assert!(matches!(
snapshot.evaluate(&request("member-a", "shell")),
ToolConsequenceVerdict::Allow
));
}
#[test]
fn a_forward_revision_replaces_the_snapshot() {
let provider = installed(policy(1, "member-a", "shell"));
provider
.accept(policy(2, "member-a", "network"))
.expect("a forward revision is accepted");
assert_eq!(provider.accepted_revision(&policy_id()), Some(2));
let snapshot = provider.snapshot(&policy_id()).expect("snapshot");
assert!(matches!(
snapshot.evaluate(&request("member-a", "network")),
ToolConsequenceVerdict::Allow
));
assert!(matches!(
snapshot.evaluate(&request("member-a", "shell")),
ToolConsequenceVerdict::Deny(_)
));
}
#[test]
fn canonical_bytes_install_and_non_canonical_bytes_do_not() {
let canonical = policy(3, "member-a", "shell")
.canonical_json()
.expect("canonical json");
let provider = Arc::new(CompiledPolicyProvider::new(
provider_id(),
PolicyProviderGeneration(1),
));
provider
.accept_canonical_json(&canonical)
.expect("canonical bytes install");
assert_eq!(provider.accepted_revision(&policy_id()), Some(3));
let mut mangled = canonical;
assert_eq!(mangled.pop(), Some(b'\n'));
provider
.accept_canonical_json(&mangled)
.expect_err("non-canonical bytes must be refused");
}
#[test]
fn an_unknown_policy_id_is_a_typed_miss() {
let provider = installed(policy(1, "member-a", "shell"));
let other = PolicyId::new("not-installed").expect("policy id");
let error = match provider.snapshot(&other) {
Ok(_) => panic!("an unknown policy must not resolve"),
Err(error) => error,
};
assert!(
matches!(error, ToolConsequenceFailure::PolicyMissing { .. }),
"expected PolicyMissing, got {error:?}"
);
}
fn policy_for(provider: &str, policy: &str, member: &str, tool: &str) -> Vec<u8> {
CompiledApplicationToolPolicy::new(
PolicyProviderId::new(provider).expect("provider id"),
PolicyId::new(policy).expect("policy id"),
PolicyRevision(1),
source(),
grants_for(member, tool),
)
.expect("compiled policy should validate")
.canonical_json()
.expect("canonical json")
}
#[test]
fn providers_are_derived_from_the_ids_the_artifacts_carry() {
let payloads = vec![
policy_for("homecore", "household-tools", "member-a", "shell"),
policy_for("some-other-author", "fleet-baseline", "member-b", "network"),
];
let providers =
providers_from_canonical_payloads(&payloads).expect("both artifacts install");
let ids: Vec<&str> = providers
.iter()
.map(|provider| provider.provider_id().as_str())
.collect();
assert_eq!(ids, vec!["homecore", "some-other-author"]);
}
#[test]
fn a_foreign_provider_id_is_served_rather_than_refused() {
let payloads = vec![policy_for(
"homecore",
"household-tools",
"member-a",
"shell",
)];
let providers = providers_from_canonical_payloads(&payloads).expect("artifact installs");
assert_eq!(providers.len(), 1);
let snapshot = providers[0]
.snapshot(&PolicyId::new("household-tools").expect("policy id"))
.expect("the carried policy id resolves");
let mut request = request("member-a", "shell");
request.provider_id = PolicyProviderId::new("homecore").expect("provider id");
request.policy_id = PolicyId::new("household-tools").expect("policy id");
assert!(matches!(
snapshot.evaluate(&request),
ToolConsequenceVerdict::Allow
));
}
#[test]
fn two_policies_from_one_author_share_a_single_provider() {
let payloads = vec![
policy_for("homecore", "household-tools", "member-a", "shell"),
policy_for("homecore", "guest-tools", "member-b", "network"),
];
let providers = providers_from_canonical_payloads(&payloads).expect("both install");
assert_eq!(providers.len(), 1, "one author means one provider");
for policy_id in ["household-tools", "guest-tools"] {
providers[0]
.snapshot(&PolicyId::new(policy_id).expect("policy id"))
.unwrap_or_else(|error| panic!("{policy_id} should resolve: {error}"));
}
}
#[test]
fn a_registry_built_from_the_derived_providers_binds_the_carried_identity() {
let payloads = vec![policy_for(
"homecore",
"household-tools",
"member-a",
"shell",
)];
let derived = providers_from_canonical_payloads(&payloads).expect("artifact installs");
let providers: Vec<Arc<dyn ToolConsequenceNarrowingPolicy>> = derived
.into_iter()
.map(|provider| provider as Arc<dyn ToolConsequenceNarrowingPolicy>)
.collect();
let registry = Arc::new(
ToolConsequencePolicyRegistry::new(
providers,
PolicyEvaluationSupervisorConfig::default(),
None,
)
.expect("registry builds from the derived providers"),
);
let member = MobMemberBinding {
mob_id: "mob".to_string(),
role: "worker".to_string(),
member: "member-a".to_string(),
};
registry
.bind(
member.clone(),
PolicyProviderId::new("homecore").expect("provider id"),
PolicyId::new("household-tools").expect("policy id"),
)
.expect("the carried provider and policy identity must bind");
let error = match registry.bind(
member,
PolicyProviderId::new("mobkit-gateway").expect("provider id"),
PolicyId::new("household-tools").expect("policy id"),
) {
Ok(_) => panic!("a provider no artifact declared must not bind"),
Err(error) => error,
};
assert!(
matches!(error, ToolConsequenceFailure::ProviderMissing { .. }),
"expected ProviderMissing, got {error:?}"
);
}
#[test]
fn the_committed_wire_fixture_installs_its_carried_provider() {
let fixture_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/application_tool_policies_init_params.json");
let canonical = String::from_utf8(policy_for(
"homecore",
"household-tools",
"member-a",
"shell",
))
.expect("canonical json is utf-8");
if std::env::var_os("MOBKIT_WRITE_FIXTURE").is_some() {
let params = serde_json::json!({
"has_roster_provider": true,
"application_tool_policies": [canonical],
});
let mut rendered = serde_json::to_string_pretty(¶ms).expect("render fixture");
rendered.push('\n');
std::fs::write(&fixture_path, rendered).expect("write fixture");
}
let raw = std::fs::read_to_string(&fixture_path).unwrap_or_else(|error| {
panic!(
"missing {}: regenerate with MOBKIT_WRITE_FIXTURE=1 ({error})",
fixture_path.display()
)
});
let params: serde_json::Value = serde_json::from_str(&raw).expect("fixture is valid JSON");
let payloads = compiled_policy_payloads_from_init_params(¶ms)
.expect("the committed wire payload parses through the gateway's own extraction");
assert_eq!(
payloads,
vec![canonical],
"the committed fixture must carry the canonical bytes this build produces"
);
let providers = providers_from_canonical_payloads(&payloads)
.expect("the committed wire payload installs");
assert_eq!(providers.len(), 1);
assert_eq!(providers[0].provider_id().as_str(), "homecore");
providers[0]
.snapshot(&PolicyId::new("household-tools").expect("policy id"))
.expect("the carried policy id resolves");
}
#[test]
fn an_absent_parameter_arms_nothing_but_an_explicit_null_is_refused() {
let absent = serde_json::json!({ "has_roster_provider": true });
assert!(
compiled_policy_payloads_from_init_params(&absent)
.expect("absent is not an error")
.is_empty()
);
for malformed in [
serde_json::json!({ "application_tool_policies": serde_json::Value::Null }),
serde_json::json!({ "application_tool_policies": "not-an-array" }),
serde_json::json!({ "application_tool_policies": [1, 2] }),
serde_json::json!({ "application_tool_policies": { "a": "b" } }),
] {
let error = compiled_policy_payloads_from_init_params(&malformed)
.expect_err("a present non-array value must be refused");
assert!(
matches!(error, ToolConsequenceFailure::EvaluationFailed { .. }),
"expected EvaluationFailed, got {error:?}"
);
}
}
#[test]
fn a_policy_from_another_provider_is_refused() {
let provider = Arc::new(CompiledPolicyProvider::new(
PolicyProviderId::new("mobkit-other").expect("provider id"),
PolicyProviderGeneration(1),
));
provider
.accept(policy(1, "member-a", "shell"))
.expect_err("a policy naming another provider must be refused");
}
}