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(),
};
storage::store_policy(policy_ks, &baseline).await?;
tracing::info!(
policy = DEFAULT_POLICY_ID,
"installed default PDP baseline policy"
);
Ok(())
}
pub const CONFIG_CONSENT_POLICY_ID: &str = "config:require-consent";
const CONFIG_CONSENT_PRIORITY: i32 = 100;
pub async fn reconcile_config_consent_policy(
policy_ks: &KeyspaceHandle,
rules: &[vta_config::RequireConsentRule],
now_rfc3339: &str,
) -> Result<(), AppError> {
if rules.is_empty() {
storage::delete_policy(policy_ks, CONFIG_CONSENT_POLICY_ID).await?;
return Ok(());
}
let rego = synthesize_consent_rego(rules);
super::engine::compile(®o, CONFIG_CONSENT_POLICY_ID)?;
let module = PolicyModule {
id: CONFIG_CONSENT_POLICY_ID.to_string(),
name: "Config-declared consent".to_string(),
description: Some(
"Synthesized from [policy.require_consent]; reconciled every boot. \
Edit config and restart, do not edit this row."
.to_string(),
),
module: rego,
applies_to: Vec::new(),
priority: CONFIG_CONSENT_PRIORITY,
enabled: true,
version: 1,
created_at: now_rfc3339.to_string(),
updated_at: now_rfc3339.to_string(),
};
storage::store_policy(policy_ks, &module).await?;
tracing::info!(
policy = CONFIG_CONSENT_POLICY_ID,
rules = rules.len(),
"reconciled config-declared consent policy"
);
Ok(())
}
fn synthesize_consent_rego(rules: &[vta_config::RequireConsentRule]) -> String {
let mut out = String::from("package vta.policy\n\nimport rego.v1\n\n");
out.push_str(
"# Generated from [policy.require_consent] in config.toml. Do not edit — \
this row is reconciled on every boot.\n\n",
);
for rule in rules {
let min = rule.min_approvals.unwrap_or(1).max(1);
let exclude = rule.exclude_requester.unwrap_or(false);
out.push_str(&format!(
"decision := {{\n\t\"decision\": \"requireConsent\",\n\t\"requireConsent\": \
{{\"approverSet\": {set}, \"minApprovals\": {min}, \"excludeRequester\": {exclude}}},\n\
}} if input.request.typeUri == {task}\n\n",
set = rego_string(&rule.approver_set),
task = rego_string(&rule.task_type),
));
}
out
}
fn rego_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
_ => out.push(c),
}
}
out.push('"');
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(),
};
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,
};
use vta_config::RequireConsentRule;
const UPDATE_URI: &str = "https://trusttasks.org/spec/vta/webvh/dids/update/1.0";
const OTHER_URI: &str = "https://trusttasks.org/spec/vault/release/0.1";
fn rule(task: &str) -> RequireConsentRule {
RequireConsentRule {
task_type: task.into(),
approver_set: "ops".into(),
min_approvals: Some(2),
exclude_requester: Some(true),
}
}
fn input_for(type_uri: &str) -> PolicyInput {
PolicyInput {
request: PolicyRequest {
type_uri: type_uri.into(),
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".into(),
consumer: Consumer {
did: "did:key:zReq".into(),
kind: None,
device_id: None,
last_user_verification_at: None,
network_class: None,
acr: None,
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 a_config_rule_requires_consent_for_its_task() {
let (ks, _d) = temp_ks().await;
install_default_policy(&ks, "2026-07-15T00:00:00Z")
.await
.unwrap();
reconcile_config_consent_policy(&ks, &[rule(UPDATE_URI)], "2026-07-15T00:00:00Z")
.await
.unwrap();
let d = decide_for(&ks, UPDATE_URI).await;
assert_eq!(d.decision, Disposition::RequireConsent);
let rc = d.require_consent.expect("requireConsent carrier");
assert_eq!(rc.approver_set, "ops");
assert_eq!(rc.min_approvals, 2);
assert!(rc.exclude_requester);
}
#[tokio::test]
async fn an_unnamed_task_falls_through_to_the_baseline() {
let (ks, _d) = temp_ks().await;
install_default_policy(&ks, "2026-07-15T00:00:00Z")
.await
.unwrap();
reconcile_config_consent_policy(&ks, &[rule(UPDATE_URI)], "2026-07-15T00:00:00Z")
.await
.unwrap();
assert_eq!(
decide_for(&ks, OTHER_URI).await.decision,
Disposition::Allow
);
}
#[tokio::test]
async fn removing_the_rule_turns_consent_back_off() {
let (ks, _d) = temp_ks().await;
install_default_policy(&ks, "2026-07-15T00:00:00Z")
.await
.unwrap();
reconcile_config_consent_policy(&ks, &[rule(UPDATE_URI)], "2026-07-15T00:00:00Z")
.await
.unwrap();
assert_eq!(
decide_for(&ks, UPDATE_URI).await.decision,
Disposition::RequireConsent
);
reconcile_config_consent_policy(&ks, &[], "2026-07-15T00:00:00Z")
.await
.unwrap();
assert_eq!(
decide_for(&ks, UPDATE_URI).await.decision,
Disposition::Allow
);
assert!(
storage::get_policy(&ks, CONFIG_CONSENT_POLICY_ID)
.await
.unwrap()
.is_none()
);
}
#[tokio::test]
async fn reconcile_is_idempotent_and_leaves_operator_policies_alone() {
let (ks, _d) = temp_ks().await;
install_default_policy(&ks, "2026-07-15T00:00:00Z")
.await
.unwrap();
let op = PolicyModule {
id: "operator-custom".into(),
name: "op".into(),
description: None,
module:
"package vta.policy\nimport rego.v1\ndecision := {\"decision\": \"deny\"} if false"
.into(),
applies_to: vec![],
priority: 5,
enabled: true,
version: 1,
created_at: "2026-07-15T00:00:00Z".into(),
updated_at: "2026-07-15T00:00:00Z".into(),
};
storage::store_policy(&ks, &op).await.unwrap();
for _ in 0..3 {
reconcile_config_consent_policy(&ks, &[rule(UPDATE_URI)], "2026-07-15T00:00:00Z")
.await
.unwrap();
}
assert!(
storage::get_policy(&ks, "operator-custom")
.await
.unwrap()
.is_some()
);
assert!(
storage::get_policy(&ks, CONFIG_CONSENT_POLICY_ID)
.await
.unwrap()
.is_some()
);
assert_eq!(
decide_for(&ks, UPDATE_URI).await.decision,
Disposition::RequireConsent
);
}
#[tokio::test]
async fn synthesis_escapes_operator_strings() {
let injected = r#"ops", "decision": "allow"#;
let nasty = RequireConsentRule {
task_type: "https://trusttasks.org/spec/vta/x/1.0".into(),
approver_set: injected.into(),
min_approvals: None,
exclude_requester: None,
};
let (ks, _d) = temp_ks().await;
install_default_policy(&ks, "2026-07-15T00:00:00Z")
.await
.unwrap();
reconcile_config_consent_policy(&ks, std::slice::from_ref(&nasty), "2026-07-15T00:00:00Z")
.await
.unwrap();
let d = decide_for(&ks, "https://trusttasks.org/spec/vta/x/1.0").await;
assert_eq!(
d.decision,
Disposition::RequireConsent,
"the injection must not turn the decision into allow"
);
assert_eq!(
d.require_consent.unwrap().approver_set,
injected,
"the crafted quote stayed inside the string — it did not break out"
);
}
}