use std::collections::HashMap;
use std::sync::{Arc, RwLock, Weak};
use cpex_core::cmf::constants::{
ENTITY_HTTP, ENTITY_LLM, ENTITY_NAME_GLOBAL, ENTITY_PROMPT, ENTITY_RESOURCE, ENTITY_TOOL,
HOOK_CMF_HTTP_REQUEST, HOOK_CMF_LLM_INPUT, HOOK_CMF_LLM_OUTPUT, HOOK_CMF_PROMPT_POST_INVOKE,
HOOK_CMF_PROMPT_PRE_INVOKE, HOOK_CMF_RESOURCE_POST_FETCH, HOOK_CMF_RESOURCE_PRE_FETCH,
HOOK_CMF_TOOL_POST_INVOKE, HOOK_CMF_TOOL_PRE_INVOKE,
};
use cpex_core::config::RouteEntry;
use cpex_core::manager::PluginManager;
use cpex_core::plugin::PluginConfig;
use cpex_core::visitor::{ConfigVisitor, VisitorError};
use apl_core::parser::compile_policy_block_value;
use apl_core::plugin_decl::{PluginDeclaration, PluginRegistry};
use apl_core::rules::{CompiledRoute, DenyResponse};
use apl_core::step::{PdpFactory, PdpResolver};
use crate::dispatch_plan::DispatchCache;
use crate::pdp_router::PdpRouter;
use crate::route_handler::{AplRouteHandler, Phase};
use crate::session_store::{SessionStore, SessionStoreFactory};
pub const HOOK_PRE: &str = HOOK_CMF_TOOL_PRE_INVOKE;
pub const HOOK_POST: &str = HOOK_CMF_TOOL_POST_INVOKE;
fn hook_pair_for_entity(entity_type: &str) -> Option<(&'static str, &'static str)> {
match entity_type {
ENTITY_TOOL => Some((HOOK_CMF_TOOL_PRE_INVOKE, HOOK_CMF_TOOL_POST_INVOKE)),
ENTITY_LLM => Some((HOOK_CMF_LLM_INPUT, HOOK_CMF_LLM_OUTPUT)),
ENTITY_PROMPT => Some((HOOK_CMF_PROMPT_PRE_INVOKE, HOOK_CMF_PROMPT_POST_INVOKE)),
ENTITY_RESOURCE => Some((HOOK_CMF_RESOURCE_PRE_FETCH, HOOK_CMF_RESOURCE_POST_FETCH)),
_ => None,
}
}
#[derive(Default)]
struct VisitorState {
plugin_registry: PluginRegistry,
global_layer: Option<CompiledRoute>,
default_layers: HashMap<String, CompiledRoute>,
tag_layers: HashMap<String, CompiledRoute>,
pdp_router: PdpRouter,
}
pub struct AplConfigVisitor {
state: RwLock<VisitorState>,
dispatch_cache: Arc<DispatchCache>,
session_store: RwLock<Arc<dyn SessionStore>>,
manager: Weak<PluginManager>,
base_capabilities: std::collections::HashSet<String>,
pdp_factories: HashMap<String, Arc<dyn PdpFactory>>,
session_store_factories: HashMap<String, Arc<dyn SessionStoreFactory>>,
}
impl AplConfigVisitor {
pub fn new(
dispatch_cache: Arc<DispatchCache>,
session_store: Arc<dyn SessionStore>,
manager: Weak<PluginManager>,
) -> Self {
Self {
state: RwLock::new(VisitorState::default()),
dispatch_cache,
session_store: RwLock::new(session_store),
manager,
base_capabilities: default_base_capabilities(),
pdp_factories: HashMap::new(),
session_store_factories: HashMap::new(),
}
}
pub fn register_pdp(&self, resolver: Arc<dyn PdpResolver>) {
let mut state = self.state.write().unwrap_or_else(|p| p.into_inner());
state.pdp_router.register(resolver);
}
pub fn register_pdp_factory(&mut self, factory: Arc<dyn PdpFactory>) {
self.pdp_factories
.insert(factory.kind().to_string(), factory);
}
pub fn register_session_store_factory(&mut self, factory: Arc<dyn SessionStoreFactory>) {
self.session_store_factories
.insert(factory.kind().to_string(), factory);
}
fn build_session_store_from_config(
&self,
block: &serde_yaml::Value,
) -> Result<(), VisitorError> {
let map = block.as_mapping().ok_or_else(|| {
"global.apl.session_store must be a mapping with a `kind:` field".to_string()
})?;
let kind = map
.get(serde_yaml::Value::String("kind".to_string()))
.and_then(|v| v.as_str())
.ok_or_else(|| "global.apl.session_store missing required `kind:` field".to_string())?;
let factory = self.session_store_factories.get(kind).ok_or_else(|| {
format!(
"global.apl.session_store declared kind='{}' but no factory is registered for that \
kind — host must call register_session_store_factory(...) before load_config_yaml",
kind
)
})?;
let store = factory.build(block).map_err(|e| {
format!(
"global.apl.session_store (kind='{}') failed to build: {}",
kind, e
)
})?;
*self
.session_store
.write()
.unwrap_or_else(|p| p.into_inner()) = store;
Ok(())
}
pub fn with_base_capabilities(mut self, caps: std::collections::HashSet<String>) -> Self {
self.base_capabilities = caps;
self
}
fn build_pdp_from_config(
&self,
entry: &serde_yaml::Value,
index: usize,
) -> Result<(), VisitorError> {
let map = entry.as_mapping().ok_or_else(|| {
format!(
"global.apl.pdp[{}] must be a mapping with a `kind:` field",
index
)
})?;
let kind = map
.get(serde_yaml::Value::String("kind".to_string()))
.and_then(|v| v.as_str())
.ok_or_else(|| format!("global.apl.pdp[{}] missing required `kind:` field", index))?;
let factory = self.pdp_factories.get(kind).ok_or_else(|| {
format!(
"global.apl.pdp[{}] declared kind='{}' but no factory is registered for that kind — \
host must call register_pdp_factory(...) before load_config_yaml",
index, kind
)
})?;
let resolver = factory.build(entry).map_err(|e| {
format!(
"global.apl.pdp[{}] (kind='{}') failed to build: {}",
index, kind, e
)
})?;
let mut state = self.state.write().unwrap_or_else(|p| p.into_inner());
state.pdp_router.register(resolver);
Ok(())
}
fn snapshot_dispatch_state(
&self,
) -> (
Arc<PluginRegistry>,
Arc<dyn PdpResolver>,
Arc<dyn SessionStore>,
) {
let (plugin_registry, pdp_router_arc) = {
let state = self.state.read().unwrap_or_else(|p| p.into_inner());
(
Arc::new(state.plugin_registry.clone()),
Arc::new(state.pdp_router.clone()) as Arc<dyn PdpResolver>,
)
};
let session_store = self
.session_store
.read()
.unwrap_or_else(|p| p.into_inner())
.clone();
(plugin_registry, pdp_router_arc, session_store)
}
}
fn default_base_capabilities() -> std::collections::HashSet<String> {
[
"read_subject",
"read_roles",
"read_permissions",
"read_teams",
"read_claims",
"read_labels",
"read_delegation",
"read_agent",
"read_meta",
]
.iter()
.map(|s| s.to_string())
.collect()
}
impl ConfigVisitor for AplConfigVisitor {
fn name(&self) -> &str {
"apl"
}
fn visit_plugins(
&self,
_mgr: &Arc<PluginManager>,
plugins: &[PluginConfig],
) -> Result<(), VisitorError> {
let mut state = self.state.write().unwrap_or_else(|p| p.into_inner());
state.plugin_registry.clear();
for cfg in plugins {
let decl = PluginDeclaration {
name: cfg.name.clone(),
kind: cfg.kind.clone(),
hooks: cfg.hooks.clone(),
capabilities: cfg.capabilities.iter().cloned().collect(),
config: plugin_config_to_yaml(&cfg.config),
on_error: Some(on_error_to_string(&cfg.on_error)),
extra: HashMap::new(),
};
state.plugin_registry.insert(cfg.name.clone(), decl);
}
Ok(())
}
fn visit_global(
&self,
mgr: &Arc<PluginManager>,
yaml: &serde_yaml::Value,
) -> Result<(), VisitorError> {
reject_legacy_apl_keys("global", yaml)?;
let Some(apl_block) = apl_subblock(yaml) else {
if response_yaml_block(yaml).is_some_and(|v| !v.is_null()) {
tracing::warn!(
"APL visitor: global.response is set but global.apl has no policy/args block \
— the entity-less HTTP catch-all handler will not install, so this response can never fire",
);
}
return Ok(());
};
if let Some(pdp_entries) = apl_block.get("pdp").and_then(|v| v.as_sequence()) {
for (i, entry) in pdp_entries.iter().enumerate() {
self.build_pdp_from_config(entry, i)?;
}
}
if let Some(block) = apl_block.get("session_store") {
self.build_session_store_from_config(block)?;
}
let policy_only = strip_non_dsl_keys(&apl_block);
let mut compiled = compile_policy_block_value("global.apl", &policy_only)
.map_err(|e| Box::new(e) as VisitorError)?;
compiled.response = response_subblock(yaml, "global");
let installs_pre_handler = http_catchall_should_install(&compiled);
if !installs_pre_handler && compiled.response.is_some() {
tracing::warn!(
"APL visitor: global.response is set but global.apl has no `args:`/`policy:` steps \
— the entity-less HTTP catch-all handler will not install, so this response can never fire",
);
}
if installs_pre_handler {
let (plugin_registry, pdp_router_arc, session_store) = self.snapshot_dispatch_state();
let mut caps = self.base_capabilities.clone();
caps.insert("read_headers".to_string());
install_handler(
mgr,
ENTITY_HTTP,
ENTITY_NAME_GLOBAL,
None,
HOOK_CMF_HTTP_REQUEST,
Phase::Pre,
Arc::new(compiled.clone()),
&plugin_registry,
&self.dispatch_cache,
&session_store,
&self.manager,
Some(pdp_router_arc),
&caps,
);
}
self.state
.write()
.unwrap_or_else(|p| p.into_inner())
.global_layer = Some(compiled);
Ok(())
}
fn visit_default(
&self,
_mgr: &Arc<PluginManager>,
entity_type: &str,
yaml: &serde_yaml::Value,
) -> Result<(), VisitorError> {
let source = format!("global.defaults.{}.apl", entity_type);
reject_legacy_apl_keys(&source, yaml)?;
warn_if_response_at_unsupported_scope(yaml, &format!("global.defaults.{entity_type}"));
let Some(apl_block) = apl_subblock(yaml) else {
return Ok(());
};
warn_if_global_only_key_at_nonglobal_scope(&source, &apl_block);
let compiled = compile_policy_block_value(&source, &apl_block)
.map_err(|e| Box::new(e) as VisitorError)?;
self.state
.write()
.unwrap_or_else(|p| p.into_inner())
.default_layers
.insert(entity_type.to_string(), compiled);
Ok(())
}
fn visit_policy_bundle(
&self,
_mgr: &Arc<PluginManager>,
tag: &str,
yaml: &serde_yaml::Value,
) -> Result<(), VisitorError> {
let source = format!("global.policies.{}.apl", tag);
reject_legacy_apl_keys(&source, yaml)?;
warn_if_response_at_unsupported_scope(yaml, &format!("global.policies.{tag}"));
let Some(apl_block) = apl_subblock(yaml) else {
return Ok(());
};
warn_if_global_only_key_at_nonglobal_scope(&source, &apl_block);
let compiled = compile_policy_block_value(&source, &apl_block)
.map_err(|e| Box::new(e) as VisitorError)?;
self.state
.write()
.unwrap_or_else(|p| p.into_inner())
.tag_layers
.insert(tag.to_string(), compiled);
Ok(())
}
fn visit_route(
&self,
mgr: &Arc<PluginManager>,
yaml: &serde_yaml::Value,
parsed: &RouteEntry,
) -> Result<(), VisitorError> {
reject_legacy_apl_keys("route", yaml)?;
let route_apl = apl_subblock(yaml);
let (entity_type, entity_names) = match entity_identity(parsed) {
Some(e) => e,
None => {
tracing::warn!(
"APL visitor: route has no tool/resource/prompt/llm match — skipping",
);
return Ok(());
},
};
if let Some(block) = &route_apl {
warn_if_global_only_key_at_nonglobal_scope(&format!("routes.{entity_type}"), block);
}
let scope = parsed.meta.as_ref().and_then(|m| m.scope.clone());
let tags: Vec<String> = parsed
.meta
.as_ref()
.map(|m| m.tags.clone())
.unwrap_or_default();
let (plugin_registry, pdp_router_arc, session_store) = self.snapshot_dispatch_state();
let route_response = response_subblock(yaml, &format!("routes.{entity_type}"));
for (idx, entity_name) in entity_names.iter().enumerate() {
let route_key = match &scope {
Some(s) => format!("{}:{}@{}", entity_type, entity_name, s),
None => format!("{}:{}", entity_type, entity_name),
};
let state = self.state.read().unwrap_or_else(|p| p.into_inner());
let mut effective = CompiledRoute::new(&route_key);
if let Some(layer) = state.global_layer.clone() {
effective.apply_layer(layer);
}
if let Some(layer) = state.default_layers.get(entity_type).cloned() {
effective.apply_layer(layer);
}
for tag in &tags {
if let Some(layer) = state.tag_layers.get(tag).cloned() {
effective.apply_layer(layer);
}
}
drop(state);
if let Some(block) = &route_apl {
let source = format!("routes.{}.apl", route_key);
let route_layer = compile_policy_block_value(&source, block)
.map_err(|e| Box::new(e) as VisitorError)?;
effective.apply_layer(route_layer);
}
effective.response = route_response.clone();
if idx == 0 {
warn_unreferenced_plugin_overrides(&effective);
}
if effective.declared_phases().is_empty() {
continue;
}
if let Err(msg) =
crate::parallel_safety::validate_parallel_plugin_modes(&effective, mgr.as_ref())
{
let err_msg = format!("route '{}': parallel-safety: {}", route_key, msg);
return Err(err_msg.into());
}
let route_arc = Arc::new(effective);
let (hook_pre, hook_post) = match hook_pair_for_entity(entity_type) {
Some(pair) => pair,
None => {
tracing::warn!(
entity_type,
entity_name,
"APL visitor: no CMF hook pair for entity_type — skipping route",
);
continue;
},
};
install_handler(
mgr,
entity_type,
entity_name,
scope.clone(),
hook_pre,
Phase::Pre,
Arc::clone(&route_arc),
&plugin_registry,
&self.dispatch_cache,
&session_store,
&self.manager,
Some(Arc::clone(&pdp_router_arc)),
&self.base_capabilities,
);
install_handler(
mgr,
entity_type,
entity_name,
scope.clone(),
hook_post,
Phase::Post,
route_arc,
&plugin_registry,
&self.dispatch_cache,
&session_store,
&self.manager,
Some(Arc::clone(&pdp_router_arc)),
&self.base_capabilities,
);
}
Ok(())
}
}
#[allow(clippy::too_many_arguments)]
fn install_handler(
mgr: &Arc<PluginManager>,
entity_type: &str,
entity_name: &str,
scope: Option<String>,
hook_name: &str,
phase: Phase,
route: Arc<CompiledRoute>,
plugin_registry: &Arc<PluginRegistry>,
dispatch_cache: &Arc<DispatchCache>,
session_store: &Arc<dyn SessionStore>,
manager: &Weak<PluginManager>,
pdp: Option<Arc<dyn PdpResolver>>,
base_capabilities: &std::collections::HashSet<String>,
) {
let mut capabilities = base_capabilities.clone();
capabilities.extend(crate::dispatch_plan::route_capability_union(
&route,
plugin_registry,
));
let plugin_config = PluginConfig {
name: format!(
"apl::{}::{}::{}",
entity_type,
entity_name,
if phase == Phase::Pre { "pre" } else { "post" }
),
kind: "builtin".to_string(),
hooks: vec![hook_name.to_string()],
capabilities,
..Default::default()
};
let mut handler = AplRouteHandler::new(
plugin_config.clone(),
route,
phase,
Arc::clone(plugin_registry),
Arc::clone(dispatch_cache),
Arc::clone(session_store),
manager.clone(),
);
if let Some(pdp) = pdp {
handler = handler.with_pdp(pdp);
}
mgr.annotate_route(
entity_type.to_string(),
entity_name.to_string(),
scope,
hook_name.to_string(),
Arc::new(handler),
plugin_config,
);
}
fn entity_identity(route: &RouteEntry) -> Option<(&'static str, Vec<String>)> {
if let Some(t) = &route.tool {
return Some(("tool", names_of(t)));
}
if let Some(r) = &route.resource {
return Some(("resource", names_of(r)));
}
if let Some(p) = &route.prompt {
return Some(("prompt", names_of(p)));
}
if let Some(l) = &route.llm {
return Some(("llm", names_of(l)));
}
None
}
fn names_of(sol: &cpex_core::config::StringOrList) -> Vec<String> {
match sol {
cpex_core::config::StringOrList::Single(p) => vec![p.as_str().to_string()],
cpex_core::config::StringOrList::List(v) => v.clone(),
}
}
fn warn_if_global_only_key_at_nonglobal_scope(scope: &str, apl_block: &serde_yaml::Value) {
for key in GLOBAL_ONLY_NON_DSL_KEYS {
if apl_block.get(key).is_some() {
tracing::warn!(
scope,
key,
"APL visitor: this key is only honored under the top-level `global:` block; \
the declaration at this scope is ignored",
);
}
}
}
fn warn_unreferenced_plugin_overrides(route: &CompiledRoute) {
if route.plugin_overrides.is_empty() {
return;
}
let mut referenced: std::collections::HashSet<String> =
crate::dispatch_plan::collect_plugin_names(route)
.into_iter()
.collect();
referenced.extend(crate::dispatch_plan::collect_delegate_plugin_names(route));
for name in route.plugin_overrides.keys() {
if !referenced.contains(name) {
tracing::warn!(
plugin = %name,
route = %route.route_key,
"APL `plugins:` override declared for a plugin no policy step references \
— the override has no effect (the `plugins:` map configures; policy steps activate)",
);
}
}
}
const GLOBAL_ONLY_NON_DSL_KEYS: [&str; 2] = ["pdp", "session_store"];
const RENAMED_APL_KEYS: [(&str, &str); 2] = [
(
"policy",
"authorization.pre_invocation (or flat pre_invocation)",
),
(
"post_policy",
"authorization.post_invocation (or flat post_invocation)",
),
];
fn reject_legacy_apl_keys(scope: &str, yaml: &serde_yaml::Value) -> Result<(), VisitorError> {
let Some(map) = yaml.as_mapping() else {
return Ok(());
};
for (old, new) in RENAMED_APL_KEYS {
if map.contains_key(serde_yaml::Value::String(old.to_string())) {
return Err(format!(
"in `{scope}`: config field `{old}` was renamed to `{new}` — update your config",
)
.into());
}
}
Ok(())
}
fn strip_non_dsl_keys(apl_block: &serde_yaml::Value) -> serde_yaml::Value {
let Some(map) = apl_block.as_mapping() else {
return apl_block.clone();
};
let mut cloned = map.clone();
for key in GLOBAL_ONLY_NON_DSL_KEYS {
cloned.remove(serde_yaml::Value::String(key.to_string()));
}
serde_yaml::Value::Mapping(cloned)
}
fn plugin_config_to_yaml(cfg: &Option<serde_json::Value>) -> Option<serde_yaml::Value> {
cfg.as_ref().and_then(|v| serde_yaml::to_value(v).ok())
}
fn on_error_to_string(on_err: &cpex_core::plugin::OnError) -> String {
on_err.to_string()
}
const FLAT_APL_KEYS: [&str; 7] = [
"pre_invocation",
"post_invocation",
"authorization",
"args",
"result",
"pdp",
"session_store",
];
fn apl_subblock(yaml: &serde_yaml::Value) -> Option<serde_yaml::Value> {
if let Some(block) = yaml.get("apl") {
return if block.is_null() {
None
} else {
Some(block.clone())
};
}
let mut block = serde_yaml::Mapping::new();
for key in FLAT_APL_KEYS {
if let Some(value) = yaml.get(key) {
block.insert(serde_yaml::Value::String(key.to_string()), value.clone());
}
}
if let Some(value) = yaml.get("plugins") {
if value.is_mapping() {
block.insert(
serde_yaml::Value::String("plugins".to_string()),
value.clone(),
);
}
}
if block.is_empty() {
None
} else {
Some(serde_yaml::Value::Mapping(block))
}
}
fn http_catchall_should_install(compiled: &CompiledRoute) -> bool {
let declared = compiled.declared_phases();
declared.contains(apl_core::rules::Phase::Args)
|| declared.contains(apl_core::rules::Phase::Policy)
}
fn response_yaml_block(yaml: &serde_yaml::Value) -> Option<&serde_yaml::Value> {
yaml.get("response")
.or_else(|| yaml.get("apl").and_then(|apl| apl.get("response")))
}
fn warn_if_response_at_unsupported_scope(yaml: &serde_yaml::Value, scope: &str) {
if response_yaml_block(yaml).is_some_and(|v| !v.is_null()) {
tracing::warn!(
scope,
"APL visitor: `response:` is honored only at `global` or route scope; ignoring here",
);
}
}
fn response_subblock(yaml: &serde_yaml::Value, route_key: &str) -> Option<DenyResponse> {
let block = response_yaml_block(yaml)?;
if block.is_null() {
return None;
}
match serde_yaml::from_value::<DenyResponse>(block.clone()) {
Ok(resp) => Some(resp),
Err(e) => {
tracing::warn!(route = route_key, error = %e, "APL visitor: ignoring malformed route `response:` block");
None
},
}
}
#[cfg(test)]
mod tests {
use super::{apl_subblock, http_catchall_should_install, response_subblock};
use apl_core::pipeline::{FieldRule, Pipeline, Stage, TypeCheck};
use apl_core::rules::{CompiledRoute, Effect};
fn yaml(s: &str) -> serde_yaml::Value {
serde_yaml::from_str(s).expect("valid yaml")
}
fn deny_effect() -> Effect {
Effect::Deny {
reason: None,
code: None,
}
}
fn field_rule(field: &str) -> FieldRule {
FieldRule {
field: field.to_string(),
pipeline: Pipeline {
stages: vec![Stage::Type(TypeCheck::Str)],
},
source: "test".to_string(),
}
}
#[test]
fn http_catchall_installs_for_args_only_global_block() {
let mut route = CompiledRoute::new("global");
route.args.push(field_rule("http.method"));
assert!(
http_catchall_should_install(&route),
"an args-only global block must still install the catch-all handler"
);
}
#[test]
fn http_catchall_installs_for_policy_only_global_block() {
let mut route = CompiledRoute::new("global");
route.policy.push(deny_effect());
assert!(http_catchall_should_install(&route));
}
#[test]
fn http_catchall_does_not_install_for_empty_or_post_only_global_block() {
let empty = CompiledRoute::new("global");
assert!(
!http_catchall_should_install(&empty),
"an empty global block has nothing to evaluate; installing would be a no-op handler"
);
let mut post_only = CompiledRoute::new("global");
post_only.post_policy.push(deny_effect());
assert!(
!http_catchall_should_install(&post_only),
"post_policy never runs on the Pre-phase-only catch-all, so it must not gate installation"
);
}
#[test]
fn response_subblock_parses_denywith() {
let v = yaml(
"tool: \"*\"\nresponse:\n status: 403\n body: \"{\\\"error\\\":\\\"forbidden\\\"}\"\n headers:\n WWW-Authenticate: \"Bearer\"\n",
);
let resp = response_subblock(&v, "tool:*").expect("response present");
assert_eq!(resp.status, Some(403));
assert_eq!(resp.body.as_deref(), Some("{\"error\":\"forbidden\"}"));
assert_eq!(
resp.headers.get("WWW-Authenticate").map(String::as_str),
Some("Bearer")
);
}
#[test]
fn response_subblock_absent_is_none() {
let v = yaml("tool: \"*\"\npolicy:\n - \"deny\"\n");
assert!(response_subblock(&v, "tool:*").is_none());
}
#[test]
fn response_subblock_nested_under_apl_wrapper_is_read() {
let v =
yaml("tool: \"*\"\napl:\n policy:\n - \"deny\"\n response:\n status: 401\n");
let resp = response_subblock(&v, "tool:*").expect("nested response present");
assert_eq!(resp.status, Some(401));
}
#[test]
fn response_subblock_top_level_wins_over_nested_apl_form() {
let v = yaml(
"tool: \"*\"\napl:\n policy:\n - \"deny\"\n response:\n status: 401\nresponse:\n status: 403\n",
);
let resp = response_subblock(&v, "tool:*").expect("response present");
assert_eq!(
resp.status,
Some(403),
"top-level sibling response takes precedence over the nested apl: form"
);
}
#[test]
fn response_subblock_malformed_is_none_not_propagated() {
let v = yaml("tool: \"*\"\nresponse:\n status: \"not-a-number\"\n");
assert!(
response_subblock(&v, "tool:*").is_none(),
"malformed response: block must be ignored, not panic or propagate an error"
);
}
#[test]
fn warn_if_response_at_unsupported_scope_is_a_safe_noop() {
use super::warn_if_response_at_unsupported_scope;
let with_response = yaml("policy:\n - \"deny\"\nresponse:\n status: 403\n");
let without = yaml("policy:\n - \"deny\"\n");
warn_if_response_at_unsupported_scope(&with_response, "global.defaults.tool");
warn_if_response_at_unsupported_scope(&with_response, "global.policies.some-tag");
warn_if_response_at_unsupported_scope(&without, "global.defaults.tool");
}
#[test]
fn apl_wrapper_is_returned_as_is() {
let v = yaml("apl:\n pre_invocation:\n - \"deny\"\n");
let block = apl_subblock(&v).expect("wrapper present");
assert!(
block.get("pre_invocation").is_some(),
"wrapper block exposes pre_invocation"
);
}
#[test]
fn null_apl_wrapper_is_none() {
let v = yaml("apl: null\n");
assert!(
apl_subblock(&v).is_none(),
"explicit null apl => no contribution"
);
}
#[test]
fn flat_pre_invocation_without_wrapper_is_collected() {
let v = yaml("tool: get_weather\npre_invocation:\n - \"deny\"\n");
let block = apl_subblock(&v).expect("flat pre_invocation recognized");
assert!(
block.get("pre_invocation").is_some(),
"flat pre_invocation lifted into the block"
);
assert!(
block.get("tool").is_none(),
"structural keys must not leak into the apl block",
);
}
#[test]
fn flat_session_store_without_wrapper_is_collected() {
let v = yaml("session_store:\n kind: valkey\n endpoint: localhost:6379\n");
let block = apl_subblock(&v).expect("flat session_store recognized");
let ss = block
.get("session_store")
.expect("session_store lifted into the block");
assert_eq!(
ss.get("kind").and_then(|k| k.as_str()),
Some("valkey"),
"the session_store mapping is preserved intact",
);
}
#[test]
fn flat_plugins_map_included_but_list_excluded() {
let m = yaml("plugins:\n audit:\n on_error: ignore\n");
let block = apl_subblock(&m).expect("plugins map is an apl term");
assert!(block.get("plugins").is_some(), "plugins map is kept");
let l = yaml("plugins:\n - audit\n");
assert!(
apl_subblock(&l).is_none(),
"structural plugins list must not be treated as an apl block",
);
}
#[test]
fn section_without_apl_terms_is_none() {
let v = yaml("tool: get_weather\n");
assert!(
apl_subblock(&v).is_none(),
"no APL terms => no contribution"
);
}
#[test]
fn explicit_wrapper_wins_over_flat_keys() {
let v = yaml("apl:\n pre_invocation:\n - \"allow\"\npre_invocation:\n - \"deny\"\n");
let block = apl_subblock(&v).expect("wrapper present");
let pre_invocation = block
.get("pre_invocation")
.and_then(|p| p.as_sequence())
.expect("pre_invocation sequence");
assert_eq!(pre_invocation.len(), 1);
assert_eq!(
pre_invocation[0].as_str(),
Some("allow"),
"the explicit apl wrapper takes precedence over flat top-level keys",
);
}
#[test]
fn warn_if_global_only_key_at_nonglobal_scope_is_a_safe_noop() {
use super::warn_if_global_only_key_at_nonglobal_scope;
let with_pdp = yaml("pre_invocation:\n - \"deny\"\npdp:\n - kind: cel\n");
let with_session_store =
yaml("pre_invocation:\n - \"deny\"\nsession_store:\n kind: valkey\n");
let without = yaml("pre_invocation:\n - \"deny\"\n");
warn_if_global_only_key_at_nonglobal_scope("route", &with_pdp);
warn_if_global_only_key_at_nonglobal_scope("routes.tool", &with_session_store);
warn_if_global_only_key_at_nonglobal_scope("global.defaults.tool.apl", &without);
}
#[test]
fn unreferenced_plugin_override_is_detectable_and_lint_is_safe() {
use super::{compile_policy_block_value, warn_unreferenced_plugin_overrides};
let block = yaml(
"pre_invocation:\n - \"plugin(used)\"\n\
plugins:\n used:\n on_error: ignore\n unused:\n on_error: ignore\n",
);
let route = compile_policy_block_value("test", &block).expect("compiles");
let referenced = crate::dispatch_plan::collect_plugin_names(&route);
assert!(
referenced.contains(&"used".to_string()),
"pre_invocation step is referenced"
);
assert!(
!referenced.contains(&"unused".to_string()),
"config-only override is not a reference",
);
assert!(
route.plugin_overrides.contains_key("unused"),
"override was compiled in"
);
warn_unreferenced_plugin_overrides(&route);
}
}