pub mod admission;
pub mod assemble;
pub mod authority_reduced_notice;
pub mod authority_reduction_pending_notice;
pub mod effects;
pub mod evaluate;
pub mod execute;
pub mod facts;
pub mod invariant;
pub mod orchestrate;
pub mod removal_notice;
pub mod step_up_passkey_notice;
pub mod verdict;
pub mod verify;
pub use assemble::{FactsInputs, assemble_facts, load_actor_role, member_state};
pub use effects::{EffectPlan, plan};
pub use evaluate::evaluate;
pub(crate) use execute::lock_admin_set;
pub use execute::{AdmitOutcome, DepartOutcome, EffectOutcome, RemintOutcome, apply};
pub use facts::{
Actor, Context, Credential, CredentialStatus, Evidence, Facts, Invitation, MemberState,
Presentation, Purpose, State, Subject,
};
pub use invariant::{Invariant, InvariantViolation};
pub use orchestrate::{
LeaveOutcome, RoleChangeOutcome, RoleChangeResult, StepUpSource, purge_member, remove_inner,
role_change_via_bound_step_up, role_change_via_pipeline,
};
pub use verdict::{Allow, Deny, Refer, RequestMore, Verdict};
pub use verify::{VerifiedFacts, VerifyError};
use crate::policy::engine::CompiledPolicy;
use vti_common::error::AppError;
pub fn decide(verified: &VerifiedFacts, policy: &CompiledPolicy) -> Result<Verdict, AppError> {
let proposed = evaluate(verified, policy)?;
let (proposed, rewritten) = invariant::hold_to_criterion(verified.facts(), proposed);
if let Some(why) = rewritten {
tracing::info!(
purpose = verified.purpose().as_str(),
rule = why,
criterion = verified
.facts()
.evidence
.criterion
.as_ref()
.map_or("<none>", |c| c.id.as_str()),
"join verdict held to its criterion",
);
}
match invariant::enforce(verified.facts(), proposed) {
Ok(verdict) => Ok(verdict),
Err(violation) => {
tracing::warn!(
purpose = verified.purpose().as_str(),
invariant = violation.invariant.code(),
detail = %violation.detail,
"ceremony policy decision vetoed by host invariant",
);
Ok(violation.into_deny())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::policy::engine::compile;
use serde_json::json;
use uuid::Uuid;
fn join_facts() -> VerifiedFacts {
let facts = Facts {
purpose: Purpose::Join,
now: "2026-05-30T12:00:00Z".parse().unwrap(),
actor: Actor {
did: "did:key:zHuman".into(),
role: None,
authenticated: true,
},
subject: Subject {
did: "did:key:zHuman".into(),
},
context: Context {
community_did: "did:webvh:acme.example".into(),
channel: "rest".into(),
member_count: 10,
thread_id: None,
},
evidence: Evidence {
criterion: Some(crate::join::criteria::CriterionFact {
id: "open".into(),
requirements_digest: "zOpen".into(),
admission: crate::schemas::accepts::Admission::Automatic,
met: true,
needs: vec![],
cited: false,
superseded: false,
}),
..Evidence::default()
},
state: State::default(),
};
VerifiedFacts::assemble(facts).expect("verified")
}
#[test]
fn decide_vetoes_a_join_policy_that_grants_admin() {
const ROGUE_JOIN: &str = r#"
package vtc.join
import future.keywords.if
default decision := {"effect": "deny", "with": {"code": "no-matching-route"}}
# A misconfigured policy that tries to hand out admin on join.
decision := {"effect": "allow", "with": {"role": "admin"}} if {
true
}
"#;
let policy = compile(ROGUE_JOIN, Uuid::new_v4()).unwrap();
let verdict = decide(&join_facts(), &policy).expect("decide");
match verdict {
Verdict::Deny(d) => assert_eq!(d.code, "privilege-ceiling"),
other => panic!("expected host-vetoed deny, got {other:?}"),
}
}
#[test]
fn decide_passes_a_well_formed_member_grant() {
const GOOD_JOIN: &str = r#"
package vtc.join
import future.keywords.if
default decision := {"effect": "deny", "with": {"code": "no-matching-route"}}
decision := {"effect": "allow", "with": {"role": "member"}} if {
input.actor.authenticated == true
}
"#;
let policy = compile(GOOD_JOIN, Uuid::new_v4()).unwrap();
let verdict = decide(&join_facts(), &policy).expect("decide");
assert_eq!(
verdict,
Verdict::Allow(Allow {
role: Some("member".into()),
..Default::default()
})
);
}
#[test]
fn decide_returns_policy_deny_untouched() {
const DENY_JOIN: &str = r#"
package vtc.join
default decision := {"effect": "deny", "with": {"code": "closed"}}
"#;
let policy = compile(DENY_JOIN, Uuid::new_v4()).unwrap();
let verdict = decide(&join_facts(), &policy).expect("decide");
assert_eq!(
verdict,
Verdict::from_decision(json!({ "effect": "deny", "with": { "code": "closed" } }))
.unwrap()
);
}
}