use vti_common::error::AppError;
use vti_common::store::KeyspaceHandle;
use super::storage;
use super::types::PolicyModule;
pub const DEFAULT_POLICY_ID: &str = "default";
pub const DEFAULT_POLICY_REGO: &str = include_str!("../policies/default.rego");
pub async fn install_default_policy(
policy_ks: &KeyspaceHandle,
now_rfc3339: &str,
) -> Result<(), AppError> {
if !storage::list_policies(policy_ks).await?.is_empty() {
return Ok(());
}
super::engine::compile(DEFAULT_POLICY_REGO, DEFAULT_POLICY_ID)?;
let baseline = PolicyModule {
id: DEFAULT_POLICY_ID.to_string(),
name: "Default baseline".to_string(),
description: Some(
"Boot-installed permissive baseline; operators layer higher-priority \
policies to tighten. See policies/default.rego."
.to_string(),
),
module: DEFAULT_POLICY_REGO.to_string(),
applies_to: Vec::new(), priority: 0,
enabled: true,
version: 1,
created_at: now_rfc3339.to_string(),
updated_at: now_rfc3339.to_string(),
ext: serde_json::Value::Null,
};
storage::store_policy(policy_ks, &baseline).await?;
tracing::info!(
policy = DEFAULT_POLICY_ID,
"installed default PDP baseline policy"
);
Ok(())
}
pub async fn seed_declarative_approvals(
policy_ks: &KeyspaceHandle,
rules: &[vta_sdk::approvals::ApprovalRule],
approver_sets: &std::collections::HashMap<String, Vec<String>>,
now_rfc3339: &str,
) -> Result<(), AppError> {
if rules.is_empty() && approver_sets.is_empty() {
return Ok(());
}
if storage::get_policy(policy_ks, vta_sdk::approvals::DECLARATIVE_POLICY_ID)
.await?
.is_some()
{
tracing::debug!(
"declarative approvals row already exists; leaving it alone (config is a seed, \
not the source of truth)"
);
return Ok(());
}
let model = super::approvals::DeclarativeModel {
rules: rules.to_vec(),
approver_sets: approver_sets
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
};
vta_sdk::approvals::validate(&model.rules, &model.approver_sets)
.map_err(|e| AppError::Validation(format!("[policy] approvals seed is invalid: {e}")))?;
let row = super::approvals::declarative_row(&model, 1, now_rfc3339, now_rfc3339);
super::engine::compile(&row.module, vta_sdk::approvals::DECLARATIVE_POLICY_ID)?;
storage::store_policy(policy_ks, &row).await?;
tracing::info!(
rules = model.rules.len(),
approver_sets = model.approver_sets.len(),
"seeded the declarative approvals row from config (first boot without one)"
);
Ok(())
}
pub const CONFIG_CONSENT_POLICY_ID: &str = "config:require-consent";
pub async fn remove_stale_config_consent_policy(
policy_ks: &KeyspaceHandle,
) -> Result<(), AppError> {
storage::delete_policy(policy_ks, CONFIG_CONSENT_POLICY_ID).await
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct UnenforcedPolicies {
pub approval_rules: usize,
pub operator_policies: Vec<String>,
}
impl UnenforcedPolicies {
pub fn is_empty(&self) -> bool {
self.approval_rules == 0 && self.operator_policies.is_empty()
}
}
pub async fn unenforced_policies(
policy_ks: &KeyspaceHandle,
) -> Result<UnenforcedPolicies, AppError> {
let mut out = UnenforcedPolicies::default();
for row in storage::list_policies(policy_ks).await? {
if !row.enabled || row.id == DEFAULT_POLICY_ID {
continue;
}
if row.id == vta_sdk::approvals::DECLARATIVE_POLICY_ID {
out.approval_rules = super::approvals::model_from_ext(&row.ext)?.rules.len();
} else {
out.operator_policies.push(row.id);
}
}
out.operator_policies.sort();
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use vta_config::StoreConfig;
use vti_common::store::Store;
async fn temp_ks() -> (KeyspaceHandle, tempfile::TempDir) {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(&StoreConfig {
data_dir: dir.path().to_path_buf(),
})
.unwrap();
(store.keyspace(vta_keyspaces::POLICY).unwrap(), dir)
}
#[test]
fn embedded_default_compiles() {
super::super::engine::compile(DEFAULT_POLICY_REGO, "default")
.expect("default.rego compiles");
}
#[tokio::test]
async fn installs_when_empty_and_is_idempotent() {
let (ks, _dir) = temp_ks().await;
install_default_policy(&ks, "2026-01-01T00:00:00Z")
.await
.unwrap();
let after_first = storage::list_policies(&ks).await.unwrap();
assert_eq!(after_first.len(), 1);
assert_eq!(after_first[0].id, DEFAULT_POLICY_ID);
install_default_policy(&ks, "2026-02-02T00:00:00Z")
.await
.unwrap();
assert_eq!(storage::list_policies(&ks).await.unwrap().len(), 1);
}
#[tokio::test]
async fn does_not_clobber_an_operator_policy() {
let (ks, _dir) = temp_ks().await;
let op = PolicyModule {
id: "operator".into(),
name: "op".into(),
description: None,
module: "package vta.policy\nimport rego.v1\ndecision := {\"decision\": \"deny\"}"
.into(),
applies_to: vec![],
priority: 100,
enabled: true,
version: 1,
created_at: "x".into(),
updated_at: "x".into(),
ext: serde_json::Value::Null,
};
storage::store_policy(&ks, &op).await.unwrap();
install_default_policy(&ks, "2026-01-01T00:00:00Z")
.await
.unwrap();
let all = storage::list_policies(&ks).await.unwrap();
assert_eq!(all.len(), 1);
assert_eq!(all[0].id, "operator");
}
use crate::types::{
Consumer, Discloses, Disposition, Exposure, PolicyInput, PolicyRequest, SideEffectLevel,
};
const UPDATE_URI: &str = "https://trusttasks.org/spec/vta/webvh/dids/update/1.0";
fn input_for(type_uri: &str) -> PolicyInput {
PolicyInput {
request: PolicyRequest {
type_uri: type_uri.to_string(),
kind: None,
subject: None,
payload_digest: None,
side_effects: SideEffectLevel::Destructive,
exposure: Exposure {
discloses: Discloses::None,
acts_as_subject: false,
},
},
site: None,
context_id: "default".to_string(),
consumer: Consumer {
did: "did:key:zRequester".to_string(),
kind: None,
device_id: None,
last_user_verification_at: None,
network_class: None,
acr: Some("aal1".to_string()),
amr: vec![],
},
}
}
async fn decide_for(ks: &KeyspaceHandle, type_uri: &str) -> crate::PolicyDecision {
let policies = storage::load_active_for_context(ks, "default")
.await
.unwrap();
crate::decide(&policies, &input_for(type_uri))
}
#[tokio::test]
async fn an_upgrade_drops_a_row_a_previous_release_synthesized() {
let (ks, _d) = temp_ks().await;
install_default_policy(&ks, "2026-07-15T00:00:00Z")
.await
.unwrap();
storage::store_policy(
&ks,
&PolicyModule {
id: CONFIG_CONSENT_POLICY_ID.to_string(),
name: "Config-declared consent".to_string(),
description: None,
module: format!(
"package vta.policy\n\nimport rego.v1\n\n\
decision := {{\"decision\": \"requireConsent\", \"requireConsent\": \
{{\"approverSet\": \"ops\"}}}} if input.request.typeUri == \"{UPDATE_URI}\"\n"
),
applies_to: Vec::new(),
priority: 100,
enabled: true,
version: 1,
created_at: "2026-07-15T00:00:00Z".to_string(),
updated_at: "2026-07-15T00:00:00Z".to_string(),
ext: serde_json::Value::Null,
},
)
.await
.unwrap();
assert_eq!(
decide_for(&ks, UPDATE_URI).await.decision,
Disposition::RequireConsent,
"precondition: the stale row is in force before the upgrade boot"
);
remove_stale_config_consent_policy(&ks).await.unwrap();
assert!(
storage::get_policy(&ks, CONFIG_CONSENT_POLICY_ID)
.await
.unwrap()
.is_none(),
"the stale row must be gone, not merely disabled"
);
assert_eq!(
decide_for(&ks, UPDATE_URI).await.decision,
Disposition::Allow,
"and the task must decide on the baseline alone"
);
remove_stale_config_consent_policy(&ks).await.unwrap();
}
#[tokio::test]
async fn the_cleanup_leaves_an_operators_own_policies_alone() {
let (ks, _d) = temp_ks().await;
install_default_policy(&ks, "2026-07-15T00:00:00Z")
.await
.unwrap();
let before = storage::list_policies(&ks).await.unwrap().len();
remove_stale_config_consent_policy(&ks).await.unwrap();
assert_eq!(
storage::list_policies(&ks).await.unwrap().len(),
before,
"a VTA that never had the config block loses nothing"
);
}
fn seed_rules() -> Vec<vta_sdk::approvals::ApprovalRule> {
vec![vta_sdk::approvals::ApprovalRule::reauth(
"https://trusttasks.org/spec/acl/grant/0.1",
)]
}
#[tokio::test]
async fn seeds_the_declarative_row_on_a_fresh_vta() {
let (ks, _d) = temp_ks().await;
seed_declarative_approvals(
&ks,
&seed_rules(),
&Default::default(),
"2026-08-09T00:00:00Z",
)
.await
.unwrap();
let row = storage::get_policy(&ks, vta_sdk::approvals::DECLARATIVE_POLICY_ID)
.await
.unwrap()
.expect("row seeded");
let model = crate::approvals::verify_declarative_row(&row.ext, &row.module)
.expect("seeded row must verify against its own rules");
assert_eq!(model.rules.len(), 1);
}
#[tokio::test]
async fn a_runtime_edit_survives_a_restart() {
let (ks, _d) = temp_ks().await;
seed_declarative_approvals(
&ks,
&seed_rules(),
&Default::default(),
"2026-08-09T00:00:00Z",
)
.await
.unwrap();
let edited = crate::approvals::DeclarativeModel {
rules: vec![vta_sdk::approvals::ApprovalRule::reauth(
"https://trusttasks.org/spec/keys/revoke/0.1",
)],
approver_sets: Default::default(),
};
let row = crate::approvals::declarative_row(
&edited,
2,
"2026-08-09T01:00:00Z",
"2026-08-09T00:00:00Z",
);
storage::store_policy(&ks, &row).await.unwrap();
seed_declarative_approvals(
&ks,
&seed_rules(),
&Default::default(),
"2026-08-09T02:00:00Z",
)
.await
.unwrap();
let after = crate::approvals::load(&ks).await.unwrap();
assert_eq!(
after.rules, edited.rules,
"the config seed clobbered a runtime edit on restart"
);
}
#[tokio::test]
async fn an_unsatisfiable_seed_fails_at_boot() {
let (ks, _d) = temp_ks().await;
let rules = vec![vta_sdk::approvals::ApprovalRule::consent(
"https://trusttasks.org/spec/acl/grant/0.1",
"nobody",
)];
let err =
seed_declarative_approvals(&ks, &rules, &Default::default(), "2026-08-09T00:00:00Z")
.await
.expect_err("a rule naming an undefined approver set must not seat");
assert!(
matches!(err, AppError::Validation(ref s) if s.contains("not defined")),
"got {err:?}"
);
assert!(
storage::get_policy(&ks, vta_sdk::approvals::DECLARATIVE_POLICY_ID)
.await
.unwrap()
.is_none(),
"nothing should have been written"
);
}
#[tokio::test]
async fn an_empty_seed_writes_nothing() {
let (ks, _d) = temp_ks().await;
seed_declarative_approvals(&ks, &[], &Default::default(), "2026-08-09T00:00:00Z")
.await
.unwrap();
assert!(
storage::get_policy(&ks, vta_sdk::approvals::DECLARATIVE_POLICY_ID)
.await
.unwrap()
.is_none()
);
}
fn operator_row(id: &str, enabled: bool) -> PolicyModule {
PolicyModule {
id: id.into(),
name: id.into(),
description: None,
module: "package vta.policy\nimport rego.v1\ndecision := {\"decision\": \"deny\"}"
.into(),
applies_to: vec![],
priority: 100,
enabled,
version: 1,
created_at: "x".into(),
updated_at: "x".into(),
ext: serde_json::Value::Null,
}
}
#[tokio::test]
async fn kr22_the_baseline_alone_is_not_unenforced_policy() {
let (ks, _d) = temp_ks().await;
install_default_policy(&ks, "2026-09-22T00:00:00Z")
.await
.unwrap();
assert!(unenforced_policies(&ks).await.unwrap().is_empty());
}
#[tokio::test]
async fn kr22_reports_approval_rules_and_enabled_operator_rows() {
let (ks, _d) = temp_ks().await;
install_default_policy(&ks, "2026-09-22T00:00:00Z")
.await
.unwrap();
seed_declarative_approvals(
&ks,
&seed_rules(),
&Default::default(),
"2026-09-22T00:00:00Z",
)
.await
.unwrap();
storage::store_policy(&ks, &operator_row("after-hours", true))
.await
.unwrap();
storage::store_policy(&ks, &operator_row("parked", false))
.await
.unwrap();
let found = unenforced_policies(&ks).await.unwrap();
assert_eq!(found.approval_rules, 1);
assert_eq!(found.operator_policies, vec!["after-hours".to_string()]);
assert!(!found.is_empty());
}
#[tokio::test]
async fn kr22_approver_sets_without_rules_are_not_reported() {
let (ks, _d) = temp_ks().await;
let sets = std::collections::HashMap::from([(
"ops".to_string(),
vec!["did:key:z6MkOps".to_string()],
)]);
seed_declarative_approvals(&ks, &[], &sets, "2026-09-22T00:00:00Z")
.await
.unwrap();
assert!(unenforced_policies(&ks).await.unwrap().is_empty());
}
}